Skip to content

Commit c112f46

Browse files
nan-licursoragent
andcommitted
fix: [SDK-4757] recover startup when initialized before UIApplicationMain (#1677)
Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit 28f7907)
1 parent be973cf commit c112f46

1 file changed

Lines changed: 61 additions & 17 deletions

File tree

iOS_SDK/OneSignalSDK/Source/OneSignal.m

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)