Add custom AI onboarding referrer parser and plumbing to enable the onboarding plan#8914
Conversation
4802bfc to
79ddaf8
Compare
79ddaf8 to
d6d1e4a
Compare
| runCatching { | ||
| if (referrerParams[REFERRAL_KEY] == REFERRAL_VALUE_AI) { | ||
| logcat(INFO) { "Custom AI onboarding referral detected" } | ||
| preferences.edit { putBoolean(PREFS_KEY_REFERRAL_PARAM_PRESENT, true) } |
There was a problem hiding this comment.
Is process() running on a background thread? I suspect we may be initializing lazy preferences instance on main thread here.
If on a background thread / once moved to background thread, better to use edit(commit = true) to surface errors that may occur while writing to shared prefs.
There was a problem hiding this comment.
Good catch 👍 yes, process() runs on the main thread.
It has to stay synchronous, though. Consumers poll the result via PlayStoreAppReferrerStateListener#waitForReferrerCode(), which unblocks once parsing finishes. If we moved only the plugin processing to a worker thread, waitForReferrerCode() could unblock before the write landed, causing a race. Moving off the main thread would mean refactoring the whole PlayStoreAppReferrerStateListener, which I'd rather avoid here. We can look at it as a follow-up though.
Since we're on the main thread, apply is the right choice as it updates the in-memory map synchronously (which is what the later resolve() read needs) and flushes to disk async. The write and the read both happen in the same first-launch session, so the in-memory value is mostly what we actually need. Synchronous disk write would only cover a case where app dies mid-onbaording but before the async write finished (plus the additional failure notification). Covering that narrow edge doesn't seem worth it.
| val brandDesignEnabled = brandDesignUpdateToggles.brandDesignUpdate().isEnabled() | ||
|
|
||
| val resolution = referrerExists && customAiOnboardingEnabled && orchestratorEnabled && brandDesignEnabled | ||
| preferences.edit { putBoolean(PREFS_KEY_ENABLED, resolution) } |
There was a problem hiding this comment.
| preferences.edit { putBoolean(PREFS_KEY_ENABLED, resolution) } | |
| preferences.edit(commit = true) { putBoolean(PREFS_KEY_ENABLED, resolution) } |
…re/lpaczos/referrer-onboarding-parser
…re/lpaczos/referrer-onboarding-parser # Conflicts: # app/src/main/java/com/duckduckgo/app/cta/ui/DaxEndBrandDesignUpdateBubbleCta.kt
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1215753390280440?focus=true
Tech Design URL (if applicable):
Description
Stacked on #8852.
CustomAiOnboardingStore/CustomAiOnboardingStoreImpl— a single component that is both the referrer parser plugin (writes theonboarding=aiflag on first launch) and the reader of the custom AI onboarding decision. The decision is made once and then frozen.resolve()waits for the install referrer to resolve, then returns true only when the referral flag is set and thecustomDuckAiOnboarding,linearOnboardingOrchestratorandbrandDesignUpdateflags are all enabled, and persists that result.isEnabled()is a cheap read of the persisted decision and does not re-evaluate, so the run is chosen once at plan build time and every later reader (onboarding pages, end-of-journey CTAs) sees the same value rather than recomputing and possibly diverging from the chosen plan. The two roles are split acrossCustomAiOnboardingResolver(decide and write) andCustomAiOnboardingStore(read).NewUserOnboardingPlanProvider— picks the custom AI plan fromresolve(), arms the in-context Duck.ai demo up front, and gates the input screen preview and Duck.ai demo steps behind thesingleTabFireDialogflag as this feature is required for these steps to be executed.Steps to test this PR
See this for context on how to test referrals.
Also see Figma.
PlayDebugPlayDebug)+ Duck.aion the intro animation and a regular onboarding flow launchesPlayDebug)+ Duck.aion the intro animation and a regular onboarding flow launches.PlayDebug)+ Duck.aion the intro animation and a custom AI onboarding flow launchesNote
Medium Risk
Changes first-launch onboarding plan selection and install-referrer timing, with broad wiring in browser CTAs and the linear orchestrator; mitigated by persisted frozen decisions and extensive unit tests.
Overview
Introduces
CustomAiOnboardingStoreas the single place for the Play Install Referreronboarding=aisignal, a one-timeresolve()decision (referrer + feature toggles), and a frozenisEnabled()read so the whole app agrees on whether the custom AI plan ran. Custom-AI helpers are removed fromOnboardingStore(no more stubisCustomAiOnboardingFlow()).NewUserOnboardingPlanProvidernow branches oncustomAiOnboardingResolver.resolve(), arms the in-context Duck.ai demo at plan build, and gates the input preview / Duck.ai demo steps onsingleTabFireDialog. Finish/skip of the custom path arms a one-shot open input on Duck.ai via the new store.Browser and CTA layers switch to
customAiOnboardingStorefor chat-tab launch, Privacy ProfeaturePage=duckai, deferred fire-button dismissal in the custom flow, and custom copy on brand-design end/subscription bubbles (via an explicitisCustomAiOnboardingFlowflag).BrandDesignUpdatePageViewModelloads that flag asynchronously before orchestrator UI.NewUserBrowserOnboardingViewModelno longer arms the demo locally—arming stays in the plan provider.Tests move from
OnboardingStoreImpltoCustomAiOnboardingStoreImplTestand update mocks across CTA/browser/plan/orchestrator tests.Reviewed by Cursor Bugbot for commit d5dbb20. Bugbot is set up for automated code reviews on this repo. Configure here.