Skip to content

Commit 2ff157b

Browse files
committed
wip
1 parent 898cde1 commit 2ff157b

15 files changed

Lines changed: 283 additions & 22 deletions

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */; };
181181
3CC9A6342AFA1FDE008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */; };
182182
3CC9A6362AFA26E7008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */; };
183+
3CCC48042FCD619400D77E94 /* OSResilientStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCC48032FCD619400D77E94 /* OSResilientStorage.swift */; };
183184
3CCF44BE299B17290021964D /* OneSignalWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CCF44BC299B17290021964D /* OneSignalWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; };
184185
3CCF44BF299B17290021964D /* OneSignalWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CCF44BD299B17290021964D /* OneSignalWrapper.m */; };
185186
3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */; };
@@ -1416,6 +1417,7 @@
14161417
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConcurrencyTests.swift; sourceTree = "<group>"; };
14171418
3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
14181419
3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
1420+
3CCC48032FCD619400D77E94 /* OSResilientStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSResilientStorage.swift; sourceTree = "<group>"; };
14191421
3CCF44BC299B17290021964D /* OneSignalWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalWrapper.h; sourceTree = "<group>"; };
14201422
3CCF44BD299B17290021964D /* OneSignalWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalWrapper.m; sourceTree = "<group>"; };
14211423
3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalUserTests-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -2231,6 +2233,7 @@
22312233
3C115163289A259500565C41 /* OneSignalOSCore.h */,
22322234
3C115188289ADEA300565C41 /* OSModelStore.swift */,
22332235
3C5C6FFB2FCB8DED00102E2C /* OneSignalIdentifiers.swift */,
2236+
3CCC48032FCD619400D77E94 /* OSResilientStorage.swift */,
22342237
3C115186289ADE7700565C41 /* OSModelStoreListener.swift */,
22352238
3C115184289ADE4F00565C41 /* OSModel.swift */,
22362239
3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */,
@@ -4381,6 +4384,7 @@
43814384
5BC1DE5C2C90B7E600CA8807 /* OSConsistencyManager.swift in Sources */,
43824385
3C5C6FFC2FCB8DED00102E2C /* OneSignalIdentifiers.swift in Sources */,
43834386
3C115189289ADEA300565C41 /* OSModelStore.swift in Sources */,
4387+
3CCC48042FCD619400D77E94 /* OSResilientStorage.swift in Sources */,
43844388
3C115185289ADE4F00565C41 /* OSModel.swift in Sources */,
43854389
3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */,
43864390
3C448BA22936B474002F96BC /* OSBackgroundTaskManager.swift in Sources */,

iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalReceiveReceiptsController.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@
3535
@implementation OneSignalReceiveReceiptsController
3636

3737
- (BOOL)isReceiveReceiptsEnabled {
38-
return [OneSignalUserDefaults.initShared getSavedBoolForKey:OSUD_RECEIVE_RECEIPTS_ENABLED defaultValue:NO];
38+
BOOL enabled = [OneSignalUserDefaults.initShared getSavedBoolForKey:OSUD_RECEIVE_RECEIPTS_ENABLED defaultValue:NO];
39+
if (enabled) {
40+
return YES;
41+
}
42+
// UserDefaults can return NO for two reasons: the flag is genuinely off, or the shared
43+
// UserDefaults file isn't readable right now (NSE running while device is locked under
44+
// NSFileProtectionCompleteUntilFirstUserAuthentication). Fall back to the unencrypted cache.
45+
NSString *cached = [OSResilientStorage stringForKey:OSResilientStorage.keyReceiveReceiptsEnabled];
46+
return [cached isEqualToString:@"1"];
3947
}
4048

4149
- (void)sendReceiveReceiptWithNotificationId:(NSString *)notificationId {

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityClicked.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class OSRequestLiveActivityClicked: OneSignalRequest, OSLiveActivityRequest {
4545
return false
4646
}
4747

48-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
48+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4949
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the OSRequestLiveActivityClicked due to null subscription ID.")
5050
return false
5151
}

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestLiveActivityReceiveReceipts.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class OSRequestLiveActivityReceiveReceipts: OneSignalRequest, OSLiveActivityRequ
4444
return false
4545
}
4646

47-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
47+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4848
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the OSRequestLiveActivityReceiveReceipts due to null subscription ID.")
4949
return false
5050
}

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestRemoveStartToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class OSRequestRemoveStartToken: OneSignalRequest, OSLiveActivityRequest, OSLive
4242
return false
4343
}
4444

45-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
45+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4646
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the remove start token request due to null subscription ID.")
4747
return false
4848
}

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestRemoveUpdateToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class OSRequestRemoveUpdateToken: OneSignalRequest, OSLiveActivityRequest, OSLiv
4242
return false
4343
}
4444

45-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
45+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4646
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the remove update token request due to null subscription ID.")
4747
return false
4848
}

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetStartToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class OSRequestSetStartToken: OneSignalRequest, OSLiveActivityRequest, OSLiveAct
4343
return false
4444
}
4545

46-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
46+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4747
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the set start token request due to null subscription ID.")
4848
return false
4949
}

iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetUpdateToken.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class OSRequestSetUpdateToken: OneSignalRequest, OSLiveActivityRequest, OSLiveAc
4343
return false
4444
}
4545

46-
guard let subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId else {
46+
guard let subscriptionId = OneSignalIdentifiers.subscriptionId else {
4747
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the set update token request due to null subscription ID.")
4848
return false
4949
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2026 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import Foundation
29+
import OneSignalCore
30+
31+
/// File-backed mirror of OneSignal SDK identifiers, written with `NSFileProtectionNone`
32+
/// so it's readable before first unlock, when shared `UserDefaults` reads silently return nil.
33+
///
34+
/// Stored in the App Group container, so it's shared across any targets (main app, NSE, etc.)
35+
/// configured with the same App Group entitlement. Opaque identifiers only: no PII or credentials.
36+
@objc(OSResilientStorage)
37+
public final class OSResilientStorage: NSObject {
38+
39+
// MARK: - Public key constants
40+
41+
@objc public static let keyAppId = "app_id"
42+
@objc public static let keySubscriptionId = "subscription_id"
43+
@objc public static let keyOneSignalId = "onesignal_id"
44+
/// Needed because the NSE reads this flag from shared UserDefaults while the device may be locked
45+
/// and the read silently returns the default (NO). Stored as "1" / "0".
46+
@objc public static let keyReceiveReceiptsEnabled = "receive_receipts_enabled"
47+
48+
// MARK: - Internal
49+
50+
private static let fileName = "onesignal_identity.json"
51+
52+
/// Serial queue used to serialize all file reads/writes.
53+
private static let queue = DispatchQueue(label: "com.onesignal.resilient-storage")
54+
55+
/// Resolve a writable container URL. App Group container is preferred so
56+
/// the NSE can read the same file. Falls back to the app's private
57+
/// Application Support directory when no App Group is entitled.
58+
private static func fileURL() -> URL? {
59+
let fm = FileManager.default
60+
61+
let groupName = OneSignalUserDefaults.appGroupName()
62+
if let container = fm.containerURL(forSecurityApplicationGroupIdentifier: groupName) {
63+
return container.appendingPathComponent(fileName)
64+
}
65+
66+
do {
67+
let support = try fm.url(
68+
for: .applicationSupportDirectory,
69+
in: .userDomainMask,
70+
appropriateFor: nil,
71+
create: true
72+
)
73+
return support.appendingPathComponent(fileName)
74+
} catch {
75+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSResilientStorage could not resolve a container URL: \(error)")
76+
return nil
77+
}
78+
}
79+
80+
/// Reads the cache file. Caller is responsible for queue-serialization.
81+
/// Returns an empty dict if the file is missing or unreadable.
82+
private static func loadUnsafe() -> [String: String] {
83+
guard let url = fileURL() else { return [:] }
84+
guard FileManager.default.fileExists(atPath: url.path) else { return [:] }
85+
86+
do {
87+
let data = try Data(contentsOf: url)
88+
if data.isEmpty { return [:] }
89+
let object = try JSONSerialization.jsonObject(with: data, options: [])
90+
return (object as? [String: String]) ?? [:]
91+
} catch {
92+
OneSignalLog.onesignalLog(.LL_WARN, message: "OSResilientStorage could not read file: \(error)")
93+
return [:]
94+
}
95+
}
96+
97+
/// Writes the cache file atomically with `.none` file protection.
98+
/// Caller is responsible for queue-serialization.
99+
private static func writeUnsafe(_ contents: [String: String]) {
100+
guard let url = fileURL() else { return }
101+
102+
do {
103+
let data = try JSONSerialization.data(withJSONObject: contents, options: [])
104+
try data.write(to: url, options: [.atomic, .noFileProtection])
105+
106+
// Explicitly re-apply protection class. The atomic write performs a rename which
107+
// has been observed to reset attributes on some iOS versions.
108+
try FileManager.default.setAttributes(
109+
[.protectionKey: FileProtectionType.none],
110+
ofItemAtPath: url.path
111+
)
112+
} catch {
113+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSResilientStorage write failed: \(error)")
114+
}
115+
}
116+
117+
// MARK: - Public API
118+
119+
/// Returns the full current contents of the cache. Empty dict if absent.
120+
@objc public static func snapshot() -> [String: String] {
121+
return queue.sync { loadUnsafe() }
122+
}
123+
124+
/// Reads a single value. Returns nil when missing or unreadable.
125+
@objc public static func string(forKey key: String) -> String? {
126+
let dict = snapshot()
127+
guard let value = dict[key], !value.isEmpty else { return nil }
128+
return value
129+
}
130+
131+
/// Atomically updates a single value. Passing nil or an empty string removes the key.
132+
@objc public static func setString(_ value: String?, forKey key: String) {
133+
queue.async {
134+
var current = loadUnsafe()
135+
if let value = value, !value.isEmpty {
136+
current[key] = value
137+
} else {
138+
current.removeValue(forKey: key)
139+
}
140+
writeUnsafe(current)
141+
}
142+
}
143+
144+
/// Atomically updates multiple values, preserving keys not in `values`.
145+
/// An empty-string value removes the corresponding key.
146+
@objc public static func setStrings(_ values: [String: String]) {
147+
guard !values.isEmpty else { return }
148+
queue.async {
149+
var current = loadUnsafe()
150+
for (key, value) in values {
151+
if value.isEmpty {
152+
current.removeValue(forKey: key)
153+
} else {
154+
current[key] = value
155+
}
156+
}
157+
writeUnsafe(current)
158+
}
159+
}
160+
}

iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OneSignalConfig.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,21 @@
2828
import Foundation
2929
import OneSignalCore
3030

31-
/// SDK-level configuration / readiness predicates. Replaces
31+
/// SDK-level configuration / readiness predicates. Replaces `OneSignalConfigManager`.
3232
@objc(OneSignalConfig)
3333
public final class OneSignalConfig: NSObject {
3434

35-
/// Returns true when the SDK shouldn't perform an operation yet because either:
35+
/// Optional readability check for device-protected storage. The main app sets this to
36+
/// `{ UIApplication.shared.isProtectedDataAvailable }` at initialize. Left nil in
37+
/// app-extension contexts (NSE) since `UIApplication` is unavailable there.
38+
/// When nil the predicate treats protected data as available — the right default for NSE,
39+
/// which reads identifiers through `OSResilientStorage` (file-backed, bypasses cfprefsd).
40+
@objc public static var isProtectedDataAvailableProvider: (() -> Bool)?
41+
42+
/// Returns true when the SDK shouldn't perform an operation yet because:
3643
/// * `app_id` hasn't been set via `OneSignal.initialize`, or
37-
/// * the host app hasn't granted privacy consent (per `OSPrivacyConsentController`).
44+
/// * the host app hasn't granted privacy consent, or
45+
/// * device storage isn't readable yet (iOS prewarm before first unlock).
3846
@objc public static func shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod methodName: String?) -> Bool {
3947
var shouldAwait = false
4048
if OneSignalIdentifiers.currentAppId == nil {
@@ -46,6 +54,9 @@ public final class OneSignalConfig: NSObject {
4654
if OSPrivacyConsentController.shouldLogMissingPrivacyConsentError(withMethodName: methodName) {
4755
shouldAwait = true
4856
}
57+
if let provider = isProtectedDataAvailableProvider, !provider() {
58+
shouldAwait = true
59+
}
4960
return shouldAwait
5061
}
5162
}

0 commit comments

Comments
 (0)