Skip to content

Commit d6395bc

Browse files
committed
wire protected-data gate + observer recovery + mirror writes
Extract protected-data setup into +setupProtectedDataObserverOnce and ComputeInitialStorageReadable, called from +init. Owns the atomic-BOOL latch, DidBecomeAvailable observer (CAS-gated so it runs at most once per process), provider closure, and seed. Gate startLiveActivitiesManager / startInAppMessages on the predicate so their singletons don't load empty UD state during prewarm; observer re-drives them post-unlock. Switch startNewSessionInternal to the full predicate so it defers during prewarm. handleAppIdChange: nil-guard prevAppId; extend clear set with OSUD_RECEIVE_RECEIPTS_ENABLED, the push-sub model store UD entry, and three OSResilientStorage keys; mirror app_id on every setAppId. downloadIOSParamsWithAppId mirrors receive_receipts to OSResilientStorage for the NSE fallback.
1 parent d5786ea commit d6395bc

1 file changed

Lines changed: 99 additions & 10 deletions

File tree

iOS_SDK/OneSignalSDK/Source/OneSignal.m

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* THE SOFTWARE.
2626
*/
2727

28+
#import <stdatomic.h>
2829
#import "OneSignalFramework.h"
2930
#import <OneSignalOSCore/OneSignalOSCore-Swift.h>
3031
#import "OneSignalInternal.h"
@@ -353,9 +354,9 @@ + (void)startNewSession:(BOOL)fromInit {
353354

354355
+ (void)startNewSessionInternal {
355356
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"OneSignal.startNewSessionInternal"];
356-
357-
// return if the user has not granted privacy permissions
358-
if ([OSPrivacyConsentController shouldLogMissingPrivacyConsentErrorWithMethodName:nil])
357+
358+
// return if the user has not granted consent, or device-protected storage isn't readable yet
359+
if ([OneSignalConfig shouldAwaitAppIdAndLogMissingPrivacyConsentForMethod:nil])
359360
return;
360361

361362
[OSOutcomes.sharedController clearOutcomes];
@@ -451,12 +452,83 @@ + (void)delayInitializationForPrivacyConsent {
451452
[OneSignalIdentifiers setCurrentAppId:nil];
452453
}
453454

455+
/// Computes the initial value for `gProtectedDataAvailable` (see the case table in
456+
/// `+setupProtectedDataObserverOnce`).
457+
static BOOL ComputeInitialStorageReadable(void) {
458+
NSDictionary *pushModels = [OneSignalUserDefaults.initShared getSavedCodeableDataForKey:OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY defaultValue:@{}];
459+
BOOL hasPriorSession = [OSResilientStorage stringForKey:OSResilientStorage.keyHasPriorSession] != nil;
460+
if (pushModels.count > 0) { // UD is readable
461+
return YES;
462+
}
463+
if (hasPriorSession) { // returning user during prewarm; defer
464+
return NO;
465+
}
466+
if ([NSThread isMainThread]) {
467+
return UIApplication.sharedApplication.isProtectedDataAvailable;
468+
}
469+
return YES; // off-main: can't safely read UIApplication
470+
}
471+
472+
/// One-time setup of the protected-data readiness check that matters during iOS app
473+
/// prewarm, when reads from shared App Group UserDefaults silently return nil
474+
///
475+
/// The cached flag is a one-way latch (NO → YES, never reverses). It is:
476+
/// * Seeded by `ComputeInitialStorageReadable` (case table below).
477+
/// * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`.
478+
/// * Read via `OneSignalConfig.isProtectedDataAvailableProvider`
479+
///
480+
/// Seed case table:
481+
/// 1. pushModels populated → YES (UD is readable)
482+
/// 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer)
483+
/// 3. neither + main thread → fall back to `UIApplication.isProtectedDataAvailable`
484+
/// 4. neither + off-main thread → YES (can't safely read UIApplication)
485+
///
486+
/// `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 3's
487+
/// `isProtectedDataAvailable` tiebreaker also protects SDK upgraders who never wrote `keyHasPriorSession`.
488+
///
489+
/// `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared by the
490+
/// observer's first fire (tracked since `DidBecomeAvailable` posts on every device unlock).
491+
+ (void)setupProtectedDataObserverOnce {
492+
static _Atomic(BOOL) gProtectedDataAvailable = NO;
493+
static _Atomic(BOOL) gObserverShouldRecover = NO;
494+
static dispatch_once_t protectedDataOnce;
495+
dispatch_once(&protectedDataOnce, ^{
496+
[NSNotificationCenter.defaultCenter addObserverForName:UIApplicationProtectedDataDidBecomeAvailable
497+
object:nil
498+
queue:nil
499+
usingBlock:^(NSNotification * _Nonnull note) {
500+
atomic_store(&gProtectedDataAvailable, YES);
501+
// Only run the recovery if init deferred. If `gObserverShouldRecover == YES`,
502+
// atomically swap to NO and proceed; otherwise bail (already consumed, or never set).
503+
BOOL shouldRecover = YES;
504+
if (!atomic_compare_exchange_strong(&gObserverShouldRecover, &shouldRecover, NO)) {
505+
return;
506+
}
507+
[OneSignalUserManagerImpl.sharedInstance start];
508+
[OSNotificationsManager sendPushTokenToDelegate];
509+
[OneSignal startLiveActivitiesManager];
510+
[OneSignal startInAppMessages];
511+
[OneSignal startNewSession:YES];
512+
}];
513+
514+
OneSignalConfig.isProtectedDataAvailableProvider = ^BOOL {
515+
return atomic_load(&gProtectedDataAvailable);
516+
};
517+
518+
BOOL initialState = ComputeInitialStorageReadable();
519+
atomic_store(&gProtectedDataAvailable, initialState);
520+
atomic_store(&gObserverShouldRecover, !initialState);
521+
});
522+
}
523+
454524
/*
455525
Called after setAppId and setLaunchOptions, depending on which one is called last (order does not matter)
456526
*/
457527
+ (void)init {
458528
[OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"launchOptions is set and appId of %@ is set, initializing OneSignal...", OneSignalIdentifiers.currentAppId]];
459-
529+
530+
[self setupProtectedDataObserverOnce];
531+
460532
// TODO: We moved this check to the top of this method, we should test this.
461533
if (initDone) {
462534
return;
@@ -505,8 +577,12 @@ + (void)init {
505577
[self startLifecycleObserver];
506578
//TODO: Should these be started in Dependency order? e.g. IAM depends on User Manager shared instance
507579
[self startUserManager]; // By here, app_id exists, and consent is granted.
508-
[self startLiveActivitiesManager];
509-
[self startInAppMessages];
580+
// Defer LA and IAM init during prewarm: both eagerly read UserDefaults at first access and would
581+
// overwrite the on-disk state with empty caches on the next save. The observer re-drives them post-unlock.
582+
if (![OneSignalConfig shouldAwaitAppIdAndLogMissingPrivacyConsentForMethod:nil]) {
583+
[self startLiveActivitiesManager];
584+
[self startInAppMessages];
585+
}
510586
[self startNewSession:YES];
511587

512588
initializationTime = [[NSDate date] timeIntervalSince1970];
@@ -528,8 +604,7 @@ + (void)handleAppIdChange:(NSString*)appId {
528604
NSString *prevAppId = OneSignalIdentifiers.storedAppId;
529605

530606
// Handle changes to the app id, this might happen on a developer's device when testing
531-
// Will also run the first time OneSignal is initialized
532-
if (appId && ![appId isEqualToString:prevAppId]) {
607+
if (appId && prevAppId && ![appId isEqualToString:prevAppId]) {
533608
initDone = false;
534609
_downloadedParameters = false;
535610
_didCallDownloadParameters = false;
@@ -541,12 +616,22 @@ + (void)handleAppIdChange:(NSString*)appId {
541616
[sharedUserDefaults removeValueForKey:OSUD_PUSH_SUBSCRIPTION_ID];
542617
[standardUserDefaults removeValueForKey:OSUD_LEGACY_PLAYER_ID];
543618
[sharedUserDefaults removeValueForKey:OSUD_LEGACY_PLAYER_ID];
619+
[sharedUserDefaults removeValueForKey:OSUD_RECEIVE_RECEIPTS_ENABLED];
620+
[sharedUserDefaults removeValueForKey:OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY];
621+
622+
// Drop cached identifiers — a real app-id change invalidates them.
623+
[OSResilientStorage setStrings:@{
624+
OSResilientStorage.keySubscriptionId: @"",
625+
OSResilientStorage.keyReceiveReceiptsEnabled: @"",
626+
OSResilientStorage.keyHasPriorSession: @""
627+
}];
544628

545629
// Clear all cached data, does not start User Module nor call logout.
546630
[OneSignalUserManagerImpl.sharedInstance clearAllModelsFromStores];
547631
}
548632

549633
[OneSignalUserDefaults.initShared saveStringForKey:OSUD_APP_ID withValue:appId];
634+
[OSResilientStorage setString:appId forKey:OSResilientStorage.keyAppId];
550635
}
551636

552637
+ (void)registerForAPNsToken {
@@ -596,8 +681,12 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId {
596681
[OSNotificationsManager checkProvisionalAuthorizationStatus];
597682
}
598683

599-
if (result[IOS_RECEIVE_RECEIPTS_ENABLE] != (id)[NSNull null])
600-
[OneSignalUserDefaults.initShared saveBoolForKey:OSUD_RECEIVE_RECEIPTS_ENABLED withValue:[result[IOS_RECEIVE_RECEIPTS_ENABLE] boolValue]];
684+
if (result[IOS_RECEIVE_RECEIPTS_ENABLE] != (id)[NSNull null]) {
685+
BOOL enabled = [result[IOS_RECEIVE_RECEIPTS_ENABLE] boolValue];
686+
[OneSignalUserDefaults.initShared saveBoolForKey:OSUD_RECEIVE_RECEIPTS_ENABLED withValue:enabled];
687+
// Mirror to the unencrypted cache so the NSE can read this flag
688+
[OSResilientStorage setString:enabled ? @"1" : @"0" forKey:OSResilientStorage.keyReceiveReceiptsEnabled];
689+
}
601690

602691
[[OSRemoteParamController sharedController] saveRemoteParams:result];
603692
if ([[OSRemoteParamController sharedController] hasLocationKey]) {

0 commit comments

Comments
 (0)