Skip to content

Commit 28f7907

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

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
@@ -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

Comments
 (0)