@@ -490,54 +490,98 @@ static BOOL ComputeInitialStorageReadable(void) {
490490 if (hasPriorSession) { // returning user during prewarm; defer
491491 return NO ;
492492 }
493- if ([NSThread isMainThread ]) {
494- return UIApplication. sharedApplication . isProtectedDataAvailable ;
493+ if (! [NSThread isMainThread ]) {
494+ return YES ; // off-main: can't safely read UIApplication
495495 }
496- return YES ; // off-main: can't safely read UIApplication
496+ UIApplication *sharedApp = UIApplication.sharedApplication ;
497+ if (sharedApp) {
498+ return sharedApp.isProtectedDataAvailable ;
499+ }
500+ // sharedApplication is nil: we are running before UIApplicationMain, e.g. a SwiftUI
501+ // App.init().
502+ // A user-initiated launch implies the device is unlocked, so storage is readable.
503+ // Only a prewarm-created process can reach this point while storage may still be
504+ // locked; iOS marks those processes with the ActivePrewarm environment variable,
505+ // which is cleared after launch completes, but pre-UIApplicationMain is always
506+ // before that, so the read is reliable here.
507+ BOOL isPrewarm = [NSProcessInfo .processInfo.environment[@" ActivePrewarm" ] isEqualToString: @" 1" ];
508+ [OneSignalLog onesignalLog: ONE_S_LL_DEBUG message: [NSString stringWithFormat: @" OneSignal initialized before UIApplicationMain (ActivePrewarm=%d ); %@ " , isPrewarm, isPrewarm ? @" deferring storage reads until launch completes" : @" treating storage as readable" ]];
509+ return !isPrewarm;
497510}
498511
499512// / One-time setup of the protected-data readiness check that matters during iOS app
500513// / prewarm, when reads from shared App Group UserDefaults silently return nil
501514// /
502515// / The cached flag is a one-way latch (NO → YES, never reverses). It is:
503516// / * Seeded by `ComputeInitialStorageReadable` (case table below).
504- // / * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`.
517+ // / * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`, or on
518+ // / `didFinishLaunching`/`didBecomeActive` when `isProtectedDataAvailable` verifies YES.
505519// / * Read via `OneSignalConfig.isProtectedDataAvailableProvider`
506520// /
507521// / Seed case table:
508- // / 1. pushModels populated → YES (UD is readable)
509- // / 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer)
510- // / 3. neither + main thread → fall back to `UIApplication.isProtectedDataAvailable`
511- // / 4. neither + off-main thread → YES (can't safely read UIApplication)
522+ // / 1. pushModels populated → YES (UD is readable)
523+ // / 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer)
524+ // / 3. neither + off-main thread → YES (can't safely read UIApplication)
525+ // / 4. neither + sharedApplication exists → `UIApplication.isProtectedDataAvailable`
526+ // / 5. neither + sharedApplication nil → NO only when `ActivePrewarm=1` (pre-UIApplicationMain:
527+ // / a user-initiated launch implies unlocked storage; only prewarm can mean locked storage)
512528// /
513- // / `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 3 's
529+ // / `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 4 's
514530// / `isProtectedDataAvailable` tiebreaker also protects SDK upgraders who never wrote `keyHasPriorSession`.
515531// /
516- // / `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared by the
517- // / observer's first fire (tracked since `DidBecomeAvailable` posts on every device unlock).
532+ // / `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared when
533+ // / recovery runs once. Recovery triggers, whichever verifies first:
534+ // / * `UIApplicationProtectedDataDidBecomeAvailable` posts on unlock, including the
535+ // / first unlock after boot (the locked-prewarm case). Does NOT post if storage was
536+ // / never locked, so it cannot be the only trigger.
537+ // / * `didFinishLaunching` / `didBecomeActive` with a live `isProtectedDataAvailable` == YES
538+ // / re-check covers deferrals where storage was readable all along (e.g. a prewarm-created
539+ // / process the user later foregrounds, when the unlock notification never fires).
518540+ (void )setupProtectedDataObserverOnce {
519541 static _Atomic (BOOL ) gProtectedDataAvailable = NO ;
520542 static _Atomic (BOOL ) gObserverShouldRecover = NO ;
521543 static dispatch_once_t protectedDataOnce;
522544 dispatch_once (&protectedDataOnce, ^{
523- [ NSNotificationCenter .defaultCenter addObserverForName: UIApplicationProtectedDataDidBecomeAvailable
524- object: nil
525- queue: nil
526- usingBlock: ^( NSNotification * _Nonnull note) {
545+ // Marks storage readable, then re-drives the SDK components that init skipped
546+ // (at most once). If `gObserverShouldRecover == YES`, atomically swap to NO and
547+ // proceed; otherwise bail (already consumed, or init never deferred).
548+ void (^recoverIfDeferred)( void ) = ^ {
527549 atomic_store (&gProtectedDataAvailable , YES );
528- // Only run the recovery if init deferred. If `gObserverShouldRecover == YES`,
529- // atomically swap to NO and proceed; otherwise bail (already consumed, or never set).
530550 BOOL shouldRecover = YES ;
531551 if (!atomic_compare_exchange_strong (&gObserverShouldRecover , &shouldRecover, NO )) {
532552 return ;
533553 }
554+ [OneSignalLog onesignalLog: ONE_S_LL_DEBUG message: @" Device storage became readable; starting deferred OneSignal components" ];
534555 [OneSignalUserManagerImpl.sharedInstance start ];
535556 [OSNotificationsManager sendPushTokenToDelegate ];
536557 [OneSignal startLiveActivitiesManager ];
537558 [OneSignal startInAppMessages ];
538559 [OneSignal startNewSession: YES ];
560+ };
561+
562+ // Authoritative signal: the OS says protected data just became available.
563+ [NSNotificationCenter .defaultCenter addObserverForName: UIApplicationProtectedDataDidBecomeAvailable
564+ object: nil
565+ queue: nil
566+ usingBlock: ^(NSNotification * _Nonnull note) {
567+ recoverIfDeferred ();
539568 }];
540569
570+ // Lifecycle signals: if init deferred while storage was actually readable,
571+ // the unlock notification never posts. Once UIApplicationMain has run,
572+ // `sharedApplication` exists and we can consult the real `isProtectedDataAvailable`.
573+ for (NSNotificationName name in @[UIApplicationDidFinishLaunchingNotification,
574+ UIApplicationDidBecomeActiveNotification ]) {
575+ [NSNotificationCenter .defaultCenter addObserverForName: name
576+ object: nil
577+ queue: NSOperationQueue .mainQueue
578+ usingBlock: ^(NSNotification * _Nonnull note) {
579+ if (UIApplication.sharedApplication .isProtectedDataAvailable ) {
580+ recoverIfDeferred ();
581+ }
582+ }];
583+ }
584+
541585 OneSignalConfig.isProtectedDataAvailableProvider = ^BOOL {
542586 return atomic_load (&gProtectedDataAvailable );
543587 };
0 commit comments