@@ -463,54 +463,98 @@ static BOOL ComputeInitialStorageReadable(void) {
463463 if (hasPriorSession) { // returning user during prewarm; defer
464464 return NO ;
465465 }
466- if ([NSThread isMainThread ]) {
467- return UIApplication. sharedApplication . isProtectedDataAvailable ;
466+ if (! [NSThread isMainThread ]) {
467+ return YES ; // off-main: can't safely read UIApplication
468468 }
469- return YES ; // off-main: can't safely read UIApplication
469+ UIApplication *sharedApp = UIApplication.sharedApplication ;
470+ if (sharedApp) {
471+ return sharedApp.isProtectedDataAvailable ;
472+ }
473+ // sharedApplication is nil: we are running before UIApplicationMain, e.g. a SwiftUI
474+ // App.init().
475+ // A user-initiated launch implies the device is unlocked, so storage is readable.
476+ // Only a prewarm-created process can reach this point while storage may still be
477+ // locked; iOS marks those processes with the ActivePrewarm environment variable,
478+ // which is cleared after launch completes, but pre-UIApplicationMain is always
479+ // before that, so the read is reliable here.
480+ BOOL isPrewarm = [NSProcessInfo .processInfo.environment[@" ActivePrewarm" ] isEqualToString: @" 1" ];
481+ [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" ]];
482+ return !isPrewarm;
470483}
471484
472485// / One-time setup of the protected-data readiness check that matters during iOS app
473486// / prewarm, when reads from shared App Group UserDefaults silently return nil
474487// /
475488// / The cached flag is a one-way latch (NO → YES, never reverses). It is:
476489// / * Seeded by `ComputeInitialStorageReadable` (case table below).
477- // / * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`.
490+ // / * Flipped to YES on `UIApplicationProtectedDataDidBecomeAvailable`, or on
491+ // / `didFinishLaunching`/`didBecomeActive` when `isProtectedDataAvailable` verifies YES.
478492// / * Read via `OneSignalConfig.isProtectedDataAvailableProvider`
479493// /
480494// / 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)
495+ // / 1. pushModels populated → YES (UD is readable)
496+ // / 2. `keyHasPriorSession` set, no UD → NO (returning user during prewarm; defer)
497+ // / 3. neither + off-main thread → YES (can't safely read UIApplication)
498+ // / 4. neither + sharedApplication exists → `UIApplication.isProtectedDataAvailable`
499+ // / 5. neither + sharedApplication nil → NO only when `ActivePrewarm=1` (pre-UIApplicationMain:
500+ // / a user-initiated launch implies unlocked storage; only prewarm can mean locked storage)
485501// /
486- // / `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 3 's
502+ // / `keyHasPriorSession` is the only reliable "SDK previously ran here" sentinel, and case 4 's
487503// / `isProtectedDataAvailable` tiebreaker also protects SDK upgraders who never wrote `keyHasPriorSession`.
488504// /
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).
505+ // / `gObserverShouldRecover` is set when init defers (storage isn't yet readable) and cleared when
506+ // / recovery runs once. Recovery triggers, whichever verifies first:
507+ // / * `UIApplicationProtectedDataDidBecomeAvailable` posts on unlock, including the
508+ // / first unlock after boot (the locked-prewarm case). Does NOT post if storage was
509+ // / never locked, so it cannot be the only trigger.
510+ // / * `didFinishLaunching` / `didBecomeActive` with a live `isProtectedDataAvailable` == YES
511+ // / re-check covers deferrals where storage was readable all along (e.g. a prewarm-created
512+ // / process the user later foregrounds, when the unlock notification never fires).
491513+ (void )setupProtectedDataObserverOnce {
492514 static _Atomic (BOOL ) gProtectedDataAvailable = NO ;
493515 static _Atomic (BOOL ) gObserverShouldRecover = NO ;
494516 static dispatch_once_t protectedDataOnce;
495517 dispatch_once (&protectedDataOnce, ^{
496- [ NSNotificationCenter .defaultCenter addObserverForName: UIApplicationProtectedDataDidBecomeAvailable
497- object: nil
498- queue: nil
499- usingBlock: ^( NSNotification * _Nonnull note) {
518+ // Marks storage readable, then re-drives the SDK components that init skipped
519+ // (at most once). If `gObserverShouldRecover == YES`, atomically swap to NO and
520+ // proceed; otherwise bail (already consumed, or init never deferred).
521+ void (^recoverIfDeferred)( void ) = ^ {
500522 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).
503523 BOOL shouldRecover = YES ;
504524 if (!atomic_compare_exchange_strong (&gObserverShouldRecover , &shouldRecover, NO )) {
505525 return ;
506526 }
527+ [OneSignalLog onesignalLog: ONE_S_LL_DEBUG message: @" Device storage became readable; starting deferred OneSignal components" ];
507528 [OneSignalUserManagerImpl.sharedInstance start ];
508529 [OSNotificationsManager sendPushTokenToDelegate ];
509530 [OneSignal startLiveActivitiesManager ];
510531 [OneSignal startInAppMessages ];
511532 [OneSignal startNewSession: YES ];
533+ };
534+
535+ // Authoritative signal: the OS says protected data just became available.
536+ [NSNotificationCenter .defaultCenter addObserverForName: UIApplicationProtectedDataDidBecomeAvailable
537+ object: nil
538+ queue: nil
539+ usingBlock: ^(NSNotification * _Nonnull note) {
540+ recoverIfDeferred ();
512541 }];
513542
543+ // Lifecycle signals: if init deferred while storage was actually readable,
544+ // the unlock notification never posts. Once UIApplicationMain has run,
545+ // `sharedApplication` exists and we can consult the real `isProtectedDataAvailable`.
546+ for (NSNotificationName name in @[UIApplicationDidFinishLaunchingNotification,
547+ UIApplicationDidBecomeActiveNotification ]) {
548+ [NSNotificationCenter .defaultCenter addObserverForName: name
549+ object: nil
550+ queue: NSOperationQueue .mainQueue
551+ usingBlock: ^(NSNotification * _Nonnull note) {
552+ if (UIApplication.sharedApplication .isProtectedDataAvailable ) {
553+ recoverIfDeferred ();
554+ }
555+ }];
556+ }
557+
514558 OneSignalConfig.isProtectedDataAvailableProvider = ^BOOL {
515559 return atomic_load (&gProtectedDataAvailable );
516560 };
0 commit comments