V1.0 of Freely was submitted to the App Store on February 20th. I was cautiously optimistic. The app worked. Tests passed. I'd tested on real devices. What could go wrong?
A race condition. A race condition could go wrong.
The Rejection
Apple's review team hit Guideline 2.1: Performance. The app got stuck on an infinite splash screen after sign-in. Just a logo, forever, mocking the reviewer who was trying to evaluate my carefully crafted allergen detection features.
The Root Cause
Freely's launch flow checks three things in sequence: authentication state, subscription status, and onboarding completion. The splash screen stays visible until all three resolve.
The problem: when a user signs in during the initial .task block, the subscriptionChecked flag wasn't being set. The auth state updated, the onboarding state updated, but the subscription check never completed. The splash screen was waiting for a signal that would never come.
In my testing, this never happened because I was already signed in. The race condition only triggered on a fresh install where the user signs in for the first time during the splash screen's initialization. Which is exactly what an App Store reviewer does.
The Fix
Two changes:
Safety timeout. If the splash screen hasn't resolved after 10 seconds, force-dismiss it and proceed to the appropriate screen. No user should stare at a splash screen for more than a few seconds regardless of what async work is happening underneath.
Subscription retry. The PaywallView now retries the product fetch if the product list is empty when the user taps "Subscribe." This handles the case where StoreKit products haven't loaded yet, which was the second half of the race condition.
// The 10-second safety net
.task {
try? await Task.sleep(for: .seconds(10))
if !hasResolved {
hasResolved = true
// Force proceed - something is stuck
}
}What I Learned
Test the fresh install flow. Every time. I had tested on devices where I was already signed in. The happy path worked perfectly. The first-time user path, the one that every App Store reviewer will take, was broken.
Add timeouts to sequential async gates. If your app's launch depends on multiple async checks completing, any one of them can silently fail and leave the user stranded. Timeouts are ugly but they're better than infinity.
Reviewers are users too. They don't know your app, they don't have saved credentials, and they don't have patience for loading screens. If the first 10 seconds of your app don't work flawlessly for a complete stranger, you'll get rejected. Rightfully so.
V1.2 shipped with both fixes plus a redesigned onboarding funnel (cut from 12 screens to 4 pre-paywall). The rejection was frustrating in the moment but forced improvements I should have made before submitting.
Sometimes Apple's review process is the beta tester you forgot to invite.