Skip to content

Wire the onboarding password import step to the Google import flow - #9624

Merged
catalinradoiu merged 23 commits into
developfrom
feature/cradoiu/onboarding-password-import-flow
Sep 3, 2026
Merged

catalinradoiu merged 23 commits into
developfrom
feature/cradoiu/onboarding-password-import-flow

Conversation

@catalinradoiu

@catalinradoiu catalinradoiu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Task/Issue URL: https://app.asana.com/1/137249556945/project/1201621853593513/task/1217265967787215?focus=true
Tech Design URL (if applicable): https://app.asana.com/1/137249556945/project/481882893211075/task/1217143698674642
API Proposals URL(s) (if applicable): https://app.asana.com/1/137249556945/project/1211724162604201/task/1217824836952478?focus=true

Description

Wire the password import step UI in the linaer onboarding to the existing Google password import web flow.

Add the new -api surface, ImportPasswordsFromGoogle:

  • isSupported() - whether the WebView on this device can run the import at all. Used as an enrolment prerequisite so users who could never reach the step are kept out of the experiment.
  • parseResult(data) - reads the outcome out of the result intent.
  • importStatus() - progress of the import itself, which continues after the web flow returns Success. It replays its most recent value, which is load-bearing here: the onboarding collector attaches after the activity result, so without replay a fast import is missed entirely.

Steps to test this PR

Apply the following patch:

Onboarding + password import test patch
Index: app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt b/app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
--- a/app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
+++ b/app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
@@ -196,7 +196,7 @@
                     add(widgetPromptStep(ctx))
                     add(addWidgetStep(ctx))
                 }
-                if (showPasswordImport) {
+                if (true) {
                     add(passwordImportStep(ctx))
                     add(passwordImportLaunchStep(ctx))
                     add(passwordImportCompleteStep(ctx))
Index: autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/RealImportPasswordsFromGoogle.kt
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/RealImportPasswordsFromGoogle.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/RealImportPasswordsFromGoogle.kt
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/RealImportPasswordsFromGoogle.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/RealImportPasswordsFromGoogle.kt
@@ -38,9 +38,16 @@
     private val credentialImporter: CredentialImporter,
 ) : ImportPasswordsFromGoogle {
 
+    // TODO: LOCAL ONLY - REVERT BEFORE COMMITTING.
+    // Forces every returned web flow to report this result.
+    // null for the real result; Error.Transient for the retry alert; Error.Permanent for the failed outcome card.
+    private val simulatedResult: ImportPasswordsResult? = null //ImportPasswordsResult.Error.Transient
+
     override suspend fun isSupported(): Boolean = capabilityChecker.webViewCapableOfImporting()
 
     override fun parseResult(data: Intent?): ImportPasswordsResult {
+        simulatedResult?.let { return it }
+
         val result = data?.let {
             IntentCompat.getParcelableExtra(it, ImportGooglePasswordResult.RESULT_KEY_DETAILS, ImportGooglePasswordResult::class.java)
         }
Index: autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt
diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt
--- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt
+++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt
@@ -28,11 +28,13 @@
 import com.squareup.anvil.annotations.ContributesBinding
 import dagger.SingleInstanceIn
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.launch
 import kotlinx.parcelize.Parcelize
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 
 interface CredentialImporter {
     suspend fun import(
@@ -89,6 +91,7 @@
         // mark that the user has imported passwords at least once, regardless of the number of credentials imported
         autofillStore.hasEverImportedPasswords = true
 
+        delay(200.milliseconds)
         _importStatus.emit(Finished(savedCredentials = insertedIds.size, numberSkipped = skippedCredentials))
     }
 

Successful import

  • Run onboarding to the import step and tap "Import From Google"
  • Complete the Google flow and confirm the outcome card shows the shimmer, then the imported count (and the skipped row when any were skipped)
  • Confirm the imported passwords are present in the Passwords & autofill in settings

Cancel

  • Tap "Import From Google", then back out of the Google flow
  • Confirm you return to the prompt card with no error dialog

Error retry

  • Update RealImportPasswordsFromGoogle and set the value of simulatedResult to ImportPasswordsResult.Error.Transient
  • "Try again" relaunches the flow; "Cancel" advances to the next onboarding step

Permanent error

  • Update RealImportPasswordsFromGoogle and set the value of simulatedResult to ImportPasswordsResult.Error.Permanent
  • Go through the import flow
  • The error takes you to the import failed step without any retry dialog (this happens in case of a CSV parsing failure)

Skip

  • Tap "Skip" and confirm onboarding continues without launching the flow

Existing flow

  • Import passwords from the existing in-app entry point from settings and confirm it is unaffected

UI changes

No UI changes, this only wires the import step to the web flow and shows the error dialog in case of a transient error.


Note

Medium Risk
Touches onboarding flow state, activity-result handling, and autofill import plumbing; mistakes could strand users on import steps or mis-report experiment pixels, but changes are scoped with substantial test coverage.

Overview
Connects linear onboarding’s password import steps to the existing Google Password Manager web flow via a new autofill ImportPasswordsFromGoogle API (isSupported, parseResult, replaying importStatus).

The config-driven onboarding ViewModel launches AutofillImportPasswordsScreen, maps web-flow results to orchestrator events, shows a retry alert on transient errors (survives rotation), and waits on importStatus (20s timeout) to populate the outcome card. The plan orchestrator tracks passwordImportResult instead of a boolean success flag: transient failures GoBack to the import prompt; permanent failures advance to a failed outcome step; success shows parsing then counts.

Experiment enrollment now also requires isSupported() so unsupported devices are excluded. Existing settings/password-management import entry points switch to the shared AutofillImportPasswordsScreen params; Onboarding is added as a launch source. Telemetry uses explicit outcome value strings (both error kinds report "error").

Reviewed by Cursor Bugbot for commit a432797. Bugbot is set up for automated code reviews on this repo. Configure here.

catalinradoiu and others added 2 commits August 26, 2026 16:55
Adds the onboarding cards for the password import experiment: the prompt card,
and the outcome card with its parsing, finished and failed states. The step
indicator gains a CONTINUES_PREVIOUS mode so the prompt and outcome cards read
as one product step.

The import flow itself is not wired up yet: the launch step is a no-op with a
TODO, and the outcome card stays on its parsing state until the follow-up lands.

Task: https://app.asana.com/1/137249556945/project/72649045549333/task/1216462243759588

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingEvent.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/BrandDesignUpdatePageViewModel.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ContentConfig.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/DialogConfigResolver.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/OnboardingDialogShownPixels.kt
#	app/src/main/res/layout/pre_onboarding_dax_dialog_cta_brand_design_update.xml
#	app/src/test/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProviderTest.kt
Adds the ImportPasswordsFromGoogle API so onboarding can start the Google
password import web flow, read its result, and follow the import status for the
counts. The web flow's ActivityParams move to AutofillScreens and gain a launch
source so the flow can attribute itself.

Replaces the TODO left by the UI change: the launch step now starts the flow,
and the outcome card is driven by the real result.

Task: https://app.asana.com/1/137249556945/project/72649045549333/task/1216462243759588

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

catalinradoiu commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

catalinradoiu and others added 15 commits August 27, 2026 15:35
…rt-step

# Conflicts:
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingEvent.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ContentConfig.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/DialogConfigResolver.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/OnboardingDialogShownPixels.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/engine/ContentController.kt
#	app/src/main/res/layout/pre_onboarding_dax_dialog_cta_brand_design_update.xml
#	app/src/test/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProviderTest.kt
#	app/src/test/java/com/duckduckgo/app/onboarding/ui/page/configdriven/DialogConfigResolverTest.kt
…feature/cradoiu/onboarding-password-import-flow
Report whether a failed Google passwords import can be retried, so the
onboarding step offers a retry alert for a WebView crash and reports a
CSV parsing failure straight on the outcome card. Dismissing the retry
alert now skips the import and its outcome step.

https://app.asana.com/1/137249556945/project/1212087397361015/task/1217824836952478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…feature/cradoiu/onboarding-password-import-flow
The manager was returning TREATMENT unconditionally with the real
enrollment commented out for local testing, which forced the import
step on every config-driven onboarding user.

https://app.asana.com/1/137249556945/project/1212087397361015/task/1217824836952478

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CDRussell CDRussell self-assigned this Aug 31, 2026
Base automatically changed from feature/cradoiu/onboarding-passwords-import-step to develop August 31, 2026 20:54
…t-flow

# Conflicts:
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingEvent.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanContext.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProvider.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/OnboardingBackgroundAnimator.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/OnboardingPixelSender.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModel.kt
#	app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/DialogConfigResolver.kt
#	app/src/test/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProviderTest.kt
…t-flow

# Conflicts:
#	app/src/test/java/com/duckduckgo/app/onboarding/orchestrator/NewUserOnboardingPlanProviderTest.kt
A transient import error left the flow on the card-less launch step with
retry only in an AlertDialog, which is torn down on configuration change.
The still-visible import card emitted PasswordImportRequested into a step
that ignored it, so only Skip could move onboarding forward.

Go back to the import step on a transient error so its Import and Skip
actions stay live, and drive the retry alert from retained view state
instead of a one-shot command so a recreated view puts it back.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95a31f7. Configure here.

@CDRussell CDRussell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@catalinradoiu Haven't been through the code; only testing out the functionality. But there's one thing that's definitely needing fixed:

  1. go through the import flow
  2. on the success screen, hit back button (app backgrounds)
  3. when you launch the app again you're shown a in-progress shimmer that never completes (user is stuck here)
Image

catalinradoiu and others added 2 commits September 3, 2026 14:55
Pressing back on the outcome card finishes the onboarding activity, but the
run keeps its place in the app-scoped orchestrator. On relaunch the card was
rebuilt on its parsing state with no activity result left to come, so it
showed a shimmer that never completed and a permanently disabled CTA.

Record what the run knows about the import in the plan context, so the card
is seeded from it rather than from a default that claims an import is in
flight, and have the card ask for an outcome whenever it binds without one.

https://app.asana.com/1/137249556945/project/1201621853593513/task/1217265967787215

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reporting the bind through the content controller keeps the outcome card a
renderer: it no longer has to raise a signal for state it cannot reach, and
ContentInteraction goes back to carrying user interactions only. Binding is
also the one event every card gets, first render and rebuild alike, so the
step id comes from it and the hardcoded content-state key goes away.

https://app.asana.com/1/137249556945/project/1201621853593513/task/1217265967787215

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
private fun PasswordImportResult.toOutcome(): PasswordImportOutcome = when (this) {
is PasswordImportResult.Imported -> PasswordImportOutcome.SUCCESS
PasswordImportResult.Failed,
PasswordImportResult.InProgress,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

curious, why InProgress leads to PERMANENT_ERROR?

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.

When we get the outcome we shouldn't be InProgress anymore. But I agree this is confusing and I will fix this.

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.

Fixed it to avoid the confusion

toOutcome had to answer for InProgress even though it can never arrive: the
parsed event only ever carries a resolved import. Nesting the two resolved
outcomes under Terminal and typing the event as that removes the branch
rather than mapping an import in flight to a permanent error.

https://app.asana.com/1/137249556945/project/1201621853593513/task/1217265967787215

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
content: ContentConfig,
scope: BindScope,
): ContentHandle {
onContentBound(stepId, content)

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.

This is called before the content is actually bound. Can we call it at the end of this block? Or if the timing matter, rename the callback?

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.

I will rename it in the next PR as we discussed

@catalinradoiu
catalinradoiu added this pull request to the merge queue Sep 3, 2026
Merged via the queue into develop with commit 6429a93 Sep 3, 2026
29 checks passed
@catalinradoiu
catalinradoiu deleted the feature/cradoiu/onboarding-password-import-flow branch September 3, 2026 16:16
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.

3 participants