Skip to content

Add custom AI onboarding referrer parser and plumbing to enable the onboarding plan#8914

Merged
LukasPaczos merged 7 commits into
developfrom
feature/lpaczos/referrer-onboarding-parser
Jun 22, 2026
Merged

Add custom AI onboarding referrer parser and plumbing to enable the onboarding plan#8914
LukasPaczos merged 7 commits into
developfrom
feature/lpaczos/referrer-onboarding-parser

Conversation

@LukasPaczos

@LukasPaczos LukasPaczos commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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 the onboarding=ai flag 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 the customDuckAiOnboarding, linearOnboardingOrchestrator and brandDesignUpdate flags 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 across CustomAiOnboardingResolver (decide and write) and CustomAiOnboardingStore (read).
  • NewUserOnboardingPlanProvider — picks the custom AI plan from resolve(), arms the in-context Duck.ai demo up front, and gates the input screen preview and Duck.ai demo steps behind the singleTabFireDialog flag 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.

  • Uninstall the app
  • Apply this patch:
diff --git a/app/build.gradle b/app/build.gradle
index 541dfe1a58..5f5570725f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -142,7 +142,7 @@ android {
     }
     buildTypes {
         debug {
-            applicationIdSuffix ".debug"
+//            applicationIdSuffix ".debug"
             pseudoLocalesEnabled false
             manifestPlaceholders = [
                     appIcon     : "@mipmap/ic_launcher_blue",
 }
diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt b/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
index 8ccb40d67a..d862242281 100644
--- a/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
+++ b/app/src/main/java/com/duckduckgo/app/onboarding/CustomDuckAiOnboardingFeature.kt
@@ -27,6 +27,6 @@ import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
 )
 interface CustomDuckAiOnboardingFeature {
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun self(): Toggle

Note

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 CustomAiOnboardingStore as the single place for the Play Install Referrer onboarding=ai signal, a one-time resolve() decision (referrer + feature toggles), and a frozen isEnabled() read so the whole app agrees on whether the custom AI plan ran. Custom-AI helpers are removed from OnboardingStore (no more stub isCustomAiOnboardingFlow()).

NewUserOnboardingPlanProvider now branches on customAiOnboardingResolver.resolve(), arms the in-context Duck.ai demo at plan build, and gates the input preview / Duck.ai demo steps on singleTabFireDialog. 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 customAiOnboardingStore for chat-tab launch, Privacy Pro featurePage=duckai, deferred fire-button dismissal in the custom flow, and custom copy on brand-design end/subscription bubbles (via an explicit isCustomAiOnboardingFlow flag). BrandDesignUpdatePageViewModel loads that flag asynchronously before orchestrator UI. NewUserBrowserOnboardingViewModel no longer arms the demo locally—arming stays in the plan provider.

Tests move from OnboardingStoreImpl to CustomAiOnboardingStoreImplTest and 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.

@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/referrer-onboarding-parser branch from 4802bfc to 79ddaf8 Compare June 18, 2026 09:04
Comment thread app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
Base automatically changed from feature/lpaczos/custom-ai-onboarding/integrate-flow to develop June 18, 2026 09:11
@LukasPaczos LukasPaczos force-pushed the feature/lpaczos/referrer-onboarding-parser branch from 79ddaf8 to d6d1e4a Compare June 18, 2026 09:12

@lmac012 lmac012 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a few nits.

runCatching {
if (referrerParams[REFERRAL_KEY] == REFERRAL_VALUE_AI) {
logcat(INFO) { "Custom AI onboarding referral detected" }
preferences.edit { putBoolean(PREFS_KEY_REFERRAL_PARAM_PRESENT, true) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
preferences.edit { putBoolean(PREFS_KEY_ENABLED, resolution) }
preferences.edit(commit = true) { putBoolean(PREFS_KEY_ENABLED, resolution) }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread app/src/main/java/com/duckduckgo/app/onboarding/CustomAiOnboardingStore.kt Outdated
@LukasPaczos LukasPaczos merged commit af9b2c2 into develop Jun 22, 2026
14 checks passed
@LukasPaczos LukasPaczos deleted the feature/lpaczos/referrer-onboarding-parser branch June 22, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants