Second 28 of Onboarding
The user taps “Finish Onboarding.”
They have just spent 28 seconds answering questions, tapping through screens, and building intent. This is the peak “yes” moment. They are ready to see the price.
Instead of a paywall, they see a loading spinner.
The retention engine needs to fetch the config. It needs to know which flow to run, check active offers, and load the localized prices. That network request takes one or two seconds. In a mobile session, two seconds is an eternity. It is exactly enough time for the adrenaline of the onboarding to fade, and for the rational brain to kick in and say, “Maybe I don’t need this.”
By the time the spinner stops and the paywall renders, the “yes” is gone.
The Cost of the Spinner
We had a rule in the engine: everything is data-driven. The entry flow (flow.entry.full) isn’t hardcoded. It’s evaluated at runtime based on user state, A/B variants, and active offers.
When onboarding.finished fired, the engine naturally said, “Okay, let me fetch the latest catalog and policy rules from the backend.”
The math was brutal. Every 500ms of latency on the entry paywall caused a measurable drop in conversion. We were losing paying subscribers not because the product was bad or the price was wrong, but because we gave them a moment of silence to reconsider.
You cannot block the peak moment on a network call.
The Pre-Warm Strategy
The solution was not to make the network request faster. The solution was to make the network request invisible. We introduced pre-warming.
When the app opens for the first time (app.first_open), the onboarding sequence begins. The user is on screen 1, answering “What is your goal?”
At that exact moment, in the background, the engine fires the config fetch.
The user takes 20-30 seconds to click through the remaining 5 to 10 onboarding screens. While they are busy, the background thread downloads the catalog, the capping rules, and the triggers.
But downloading isn’t enough. We go one step further. The engine pre-evaluates the policy: “If onboarding.finished were to happen right now, what flow would we run?”
It figures out the exact paywall ID, prices, and copy, and caches that decision in memory.
The Instant Entry
By the time the user reaches the final onboarding screen and taps “Finish”, the work is already done.
The engine instantly matches the onboarding.finished event against the pre-warmed cache. The entry paywall (flow.entry.full) launches instantly, seamlessly transitioning from the onboarding UI to the purchase UI with zero loading states.
We preserved the momentum. The user stays in the flow state.
The Offline Fallback
What if the network is so bad that the 28 seconds of onboarding weren’t enough to fetch the config?
The engine still refuses to wait. We enforce a snapshot-first rule. The app binary bundles a fallback snapshot of the config—the last known good state from when the app was compiled.
If the live config isn’t ready when the user finishes onboarding, the engine falls back to the bundled snapshot.
This is an honest trade-off. A bundled snapshot might have slightly outdated prices or old copy. It might not reflect the A/B test we launched yesterday. But a sub-optimal paywall shown instantly converts vastly better than a perfect paywall that makes the user wait 3 seconds.
You trade perfect synchronization for perfect timing.
Takeaway
Timing is a feature. The end of onboarding is the highest-intent moment in a user’s lifecycle. A loading spinner at this precise second destroys conversion.
Never wait for the network when the user is ready. Pre-warm your config in the background, pre-evaluate the decision, and fall back to a bundled snapshot if you have to. Protect the momentum, because you can’t get it back.
This concludes the core AMORE series. Next, we will cover follow-ups and system refinements.