Skip to content

Commit 7d5b6a3

Browse files
authored
Add source to import flow pixels (#6310)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1210421589932466 ### Description Updates the pixels around password import prompts/buttons/dialogs, namely to include a `source` parameter indicating where the import was launched from. Possible values: - `password_management_promo` - `password_management_empty_state` - `password_management_overflow` - `autofill_settings_button` - `unknown` (should never happen in reality) ### Steps to test this PR Suggested logcat filter: `message~:"Pixel sent.*source="` #### Password management view - [x] Fresh install, and visit Passwords from the browser overflow menu - [x] Verify: `autofill_import_google_passwords_import_button_shown with params: {source=password_management_empty_state}` - [x] Verify: `autofill_import_google_passwords_preimport_prompt_displayed with params: {source=password_management_empty_state}` - [x] Verify: `autofill_import_google_passwords_import_button_tapped with params: {source=password_management_empty_state}` - [x] Dismiss the dialog. Verify: `autofill_import_google_passwords_result_user_cancelled with params: {stage=pre-import-dialog, source=password_management_empty_state}` - [x] Manually add a password and return to the list - [x] Verify you see the password import promo, and `autofill_import_google_passwords_import_button_shown with params: {source=password_management_promo}` - [x] Tap the promo CTA; verify: `autofill_import_google_passwords_import_button_tapped with params: {source=password_management_promo}` - [x] Complete or cancel the import flow to return to the password list. Tap on overflow and choose `Import Passwords From Google`. - [x] Verify: `autofill_import_google_passwords_overflow_menu_tapped with params: {source=password_management_overflow}` - [x] Verify: `autofill_import_google_passwords_preimport_prompt_displayed with params: {source=password_management_overflow}` #### Password settings screen - [x] Leave management screen, and visit app settings, then `Passwords & Autofill` - [x] Verify: `autofill_import_google_passwords_import_button_shown with params: {source=autofill_settings_button}`
1 parent 3f1ab23 commit 7d5b6a3

File tree

14 files changed

+155
-95
lines changed

14 files changed

+155
-95
lines changed

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/AutofillImportLaunchSource.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616

1717
package com.duckduckgo.autofill.impl.importing
1818

19-
enum class AutofillImportLaunchSource(val value: String) {
20-
PasswordManagementPromo("passwords_management_promo"),
21-
AutofillSettings("autofill_settings"),
22-
PasswordManagementEmpty("passwords_management_empty"),
19+
import android.os.Parcelable
20+
import kotlinx.parcelize.Parcelize
21+
22+
@Parcelize
23+
enum class AutofillImportLaunchSource(val value: String) : Parcelable {
24+
PasswordManagementPromo("password_management_promo"),
25+
PasswordManagementEmptyState("password_management_empty_state"),
26+
PasswordManagementOverflow("password_management_overflow"),
27+
AutofillSettings("autofill_settings_button"),
28+
Unknown("unknown"),
2329
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsPromotionViewModel.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
2020
import androidx.lifecycle.viewModelScope
2121
import com.duckduckgo.anvil.annotations.ContributesViewModel
2222
import com.duckduckgo.app.statistics.pixels.Pixel
23-
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource
23+
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo
2424
import com.duckduckgo.autofill.impl.importing.promo.ImportInPasswordsPromotionViewModel.Command.DismissImport
2525
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
2626
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED
@@ -56,7 +56,7 @@ class ImportInPasswordsPromotionViewModel @Inject constructor(
5656
fun onPromoShown() {
5757
if (!promoDisplayedPixelSent) {
5858
promoDisplayedPixelSent = true
59-
val params = mapOf("source" to AutofillImportLaunchSource.PasswordManagementPromo.value)
59+
val params = mapOf("source" to PasswordManagementPromo.value)
6060
pixel.fire(AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN, params)
6161
}
6262
}
@@ -65,10 +65,10 @@ class ImportInPasswordsPromotionViewModel @Inject constructor(
6565
viewModelScope.launch(dispatchers.io()) {
6666
promoEventDispatcher.emit(
6767
AutofillEffect.LaunchImportPasswords(
68-
source = AutofillImportLaunchSource.PasswordManagementPromo,
68+
source = PasswordManagementPromo,
6969
),
7070
)
71-
val params = mapOf("source" to AutofillImportLaunchSource.PasswordManagementPromo.value)
71+
val params = mapOf("source" to PasswordManagementPromo.value)
7272
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED, params)
7373
}
7474
}
@@ -77,6 +77,11 @@ class ImportInPasswordsPromotionViewModel @Inject constructor(
7777
viewModelScope.launch(dispatchers.io()) {
7878
importInPasswordsVisibility.onPromoDismissed()
7979
command.send(DismissImport)
80+
pixel.fire(
81+
pixel = AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_DISMISSED,
82+
parameters = mapOf("source" to PasswordManagementPromo.value),
83+
encodedParameters = emptyMap(),
84+
)
8085
}
8186
}
8287
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName
161161

162162
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED("autofill_import_google_passwords_import_button_tapped"),
163163
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN("autofill_import_google_passwords_import_button_shown"),
164+
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_DISMISSED("autofill_import_google_passwords_import_button_dismissed"),
164165
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU("autofill_import_google_passwords_overflow_menu_tapped"),
165166
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED("autofill_import_google_passwords_preimport_prompt_displayed"),
166167
AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED("autofill_import_google_passwords_preimport_prompt_confirmed"),

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillPasswordsManagementViewModel.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.duckduckgo.autofill.impl.asString
3434
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator
3535
import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration
3636
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource
37+
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo
3738
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames
3839
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DELETE_LOGIN
3940
import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED
@@ -454,7 +455,7 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
454455
viewModelScope.launch(dispatchers.io()) {
455456
autofillEffectDispatcher.effects.collect { effect ->
456457
when {
457-
effect is LaunchImportPasswords -> addCommand(LaunchImportGooglePasswords(showImportInstructions = false))
458+
effect is LaunchImportPasswords -> addCommand(LaunchImportGooglePasswords(PasswordManagementPromo))
458459
}
459460
}
460461
}
@@ -738,9 +739,9 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
738739
}
739740
}
740741

741-
fun onImportPasswordsFromGooglePasswordManager() {
742+
fun onImportPasswordsFromGooglePasswordManager(importSource: AutofillImportLaunchSource) {
742743
viewModelScope.launch(dispatchers.io()) {
743-
addCommand(LaunchImportGooglePasswords(showImportInstructions = true))
744+
addCommand(LaunchImportGooglePasswords(importSource))
744745
}
745746
}
746747

@@ -814,7 +815,7 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
814815
fun recordImportGooglePasswordButtonShown() {
815816
if (!importGooglePasswordButtonShownPixelSent) {
816817
importGooglePasswordButtonShownPixelSent = true
817-
val params = mapOf("source" to AutofillImportLaunchSource.PasswordManagementEmpty.value)
818+
val params = mapOf("source" to AutofillImportLaunchSource.PasswordManagementEmptyState.value)
818819
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN, params)
819820
}
820821
}
@@ -904,7 +905,7 @@ class AutofillPasswordsManagementViewModel @Inject constructor(
904905
data object LaunchResetNeverSaveListConfirmation : ListModeCommand()
905906
data class LaunchDeleteAllPasswordsConfirmation(val numberToDelete: Int) : ListModeCommand()
906907
data class PromptUserToAuthenticateMassDeletion(val authConfiguration: AuthConfiguration) : ListModeCommand()
907-
data class LaunchImportGooglePasswords(val showImportInstructions: Boolean) : ListModeCommand()
908+
data class LaunchImportGooglePasswords(val importSource: AutofillImportLaunchSource) : ListModeCommand()
908909
data class LaunchReportAutofillBreakageConfirmation(val eTldPlusOne: String) : ListModeCommand()
909910
data object ShowUserReportSentMessage : ListModeCommand()
910911
data object ReevalutePromotions : ListModeCommand()

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsPixelSender.kt

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ import com.squareup.anvil.annotations.ContributesBinding
3535
import javax.inject.Inject
3636

3737
interface ImportPasswordsPixelSender {
38-
fun onImportPasswordsDialogDisplayed()
39-
fun onImportPasswordsDialogImportButtonClicked()
40-
fun onUserCancelledImportPasswordsDialog()
41-
fun onUserCancelledImportWebFlow(stage: String)
42-
fun onImportSuccessful(savedCredentials: Int, numberSkipped: Int)
43-
fun onImportFailed(reason: UserCannotImportReason)
38+
fun onImportPasswordsDialogDisplayed(source: AutofillImportLaunchSource)
39+
fun onImportPasswordsDialogImportButtonClicked(source: AutofillImportLaunchSource)
40+
fun onUserCancelledImportPasswordsDialog(source: AutofillImportLaunchSource)
41+
fun onUserCancelledImportWebFlow(stage: String, source: AutofillImportLaunchSource)
42+
fun onImportSuccessful(savedCredentials: Int, numberSkipped: Int, source: AutofillImportLaunchSource)
43+
fun onImportFailed(reason: UserCannotImportReason, source: AutofillImportLaunchSource)
4444
fun onImportPasswordsButtonTapped(launchSource: AutofillImportLaunchSource)
4545
fun onImportPasswordsOverflowMenuTapped()
4646
fun onImportPasswordsViaDesktopSyncButtonTapped()
@@ -53,39 +53,49 @@ class ImportPasswordsPixelSenderImpl @Inject constructor(
5353
private val engagementBucketing: AutofillEngagementBucketing,
5454
) : ImportPasswordsPixelSender {
5555

56-
override fun onImportPasswordsDialogDisplayed() {
57-
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED)
56+
override fun onImportPasswordsDialogDisplayed(source: AutofillImportLaunchSource) {
57+
val params = mapOf(SOURCE_KEY to source.value)
58+
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_DISPLAYED, params)
5859
}
5960

60-
override fun onImportPasswordsDialogImportButtonClicked() {
61-
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED)
61+
override fun onImportPasswordsDialogImportButtonClicked(source: AutofillImportLaunchSource) {
62+
val params = mapOf(SOURCE_KEY to source.value)
63+
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_PREIMPORT_PROMPT_CONFIRMED, params)
6264
}
6365

64-
override fun onUserCancelledImportPasswordsDialog() {
65-
val params = mapOf(CANCELLATION_STAGE_KEY to PRE_IMPORT_DIALOG_STAGE)
66+
override fun onUserCancelledImportPasswordsDialog(source: AutofillImportLaunchSource) {
67+
val params = mapOf(
68+
CANCELLATION_STAGE_KEY to PRE_IMPORT_DIALOG_STAGE,
69+
SOURCE_KEY to source.value,
70+
)
6671
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_USER_CANCELLED, params)
6772
}
6873

69-
override fun onUserCancelledImportWebFlow(stage: String) {
70-
val params = mapOf(CANCELLATION_STAGE_KEY to stage)
74+
override fun onUserCancelledImportWebFlow(stage: String, source: AutofillImportLaunchSource) {
75+
val params = mapOf(
76+
CANCELLATION_STAGE_KEY to stage,
77+
SOURCE_KEY to source.value,
78+
)
7179
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_USER_CANCELLED, params)
7280
}
7381

74-
override fun onImportSuccessful(savedCredentials: Int, numberSkipped: Int) {
82+
override fun onImportSuccessful(savedCredentials: Int, numberSkipped: Int, source: AutofillImportLaunchSource) {
7583
val savedCredentialsBucketed = engagementBucketing.bucketNumberOfCredentials(savedCredentials)
7684
val skippedCredentialsBucketed = engagementBucketing.bucketNumberOfCredentials(numberSkipped)
7785
val params = mapOf(
7886
"saved_credentials" to savedCredentialsBucketed,
7987
"skipped_credentials" to skippedCredentialsBucketed,
88+
SOURCE_KEY to source.value,
8089
)
8190
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_SUCCESS, params)
8291
}
8392

84-
override fun onImportFailed(reason: UserCannotImportReason) {
93+
override fun onImportFailed(reason: UserCannotImportReason, source: AutofillImportLaunchSource) {
8594
val pixelName = when (reason) {
8695
ErrorParsingCsv -> AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_FAILURE_ERROR_PARSING
8796
}
88-
pixel.fire(pixelName)
97+
val params = mapOf(SOURCE_KEY to source.value)
98+
pixel.fire(pixelName, params)
8999
}
90100

91101
override fun onImportPasswordsButtonTapped(launchSource: AutofillImportLaunchSource) {
@@ -94,7 +104,8 @@ class ImportPasswordsPixelSenderImpl @Inject constructor(
94104
}
95105

96106
override fun onImportPasswordsOverflowMenuTapped() {
97-
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU)
107+
val params = mapOf(SOURCE_KEY to AutofillImportLaunchSource.PasswordManagementOverflow.value)
108+
pixel.fire(AUTOFILL_IMPORT_GOOGLE_PASSWORDS_OVERFLOW_MENU, params)
98109
}
99110

100111
override fun onImportPasswordsViaDesktopSyncButtonTapped() {

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialog.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import android.view.ViewGroup
2727
import androidx.activity.result.contract.ActivityResultContracts
2828
import androidx.core.content.ContextCompat
2929
import androidx.core.content.IntentCompat
30+
import androidx.core.os.BundleCompat
3031
import androidx.lifecycle.Lifecycle
3132
import androidx.lifecycle.ViewModel
3233
import androidx.lifecycle.ViewModelProvider
@@ -37,6 +38,9 @@ import com.duckduckgo.app.browser.favicon.FaviconManager
3738
import com.duckduckgo.autofill.impl.R
3839
import com.duckduckgo.autofill.impl.databinding.ContentImportFromGooglePasswordDialogBinding
3940
import com.duckduckgo.autofill.impl.deviceauth.AutofillAuthorizationGracePeriod
41+
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource
42+
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo
43+
import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.Unknown
4044
import com.duckduckgo.autofill.impl.importing.CredentialImporter
4145
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePassword
4246
import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult
@@ -102,18 +106,26 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
102106
if (activityResult.resultCode == Activity.RESULT_OK) {
103107
lifecycleScope.launch {
104108
activityResult.data?.let { data ->
105-
processImportFlowResult(data)
109+
val launchSource = getLaunchSource()
110+
processImportFlowResult(data, launchSource)
106111
}
107112
}
108113
}
109114
}
110115

111-
private fun ImportFromGooglePasswordsDialog.processImportFlowResult(data: Intent) {
116+
private fun getLaunchSource() =
117+
BundleCompat.getParcelable(arguments ?: Bundle(), KEY_LAUNCH_SOURCE, AutofillImportLaunchSource::class.java) ?: Unknown
118+
119+
private fun ImportFromGooglePasswordsDialog.processImportFlowResult(data: Intent, launchSource: AutofillImportLaunchSource) {
112120
(IntentCompat.getParcelableExtra(data, ImportGooglePasswordResult.RESULT_KEY_DETAILS, ImportGooglePasswordResult::class.java)).let {
113121
when (it) {
114-
is ImportGooglePasswordResult.Success -> viewModel.onImportFlowFinishedSuccessfully()
115-
is ImportGooglePasswordResult.Error -> viewModel.onImportFlowFinishedWithError(it.reason)
116-
is ImportGooglePasswordResult.UserCancelled -> viewModel.onImportFlowCancelledByUser(it.stage, canShowPreImportDialog())
122+
is ImportGooglePasswordResult.Success -> viewModel.onImportFlowFinishedSuccessfully(launchSource)
123+
is ImportGooglePasswordResult.Error -> viewModel.onImportFlowFinishedWithError(it.reason, launchSource)
124+
is ImportGooglePasswordResult.UserCancelled -> viewModel.onImportFlowCancelledByUser(
125+
it.stage,
126+
canShowPreImportDialog(launchSource),
127+
launchSource,
128+
)
117129
else -> {}
118130
}
119131
}
@@ -203,14 +215,20 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
203215
}
204216

205217
// check if we should show the initial instructional prompt
206-
if (canShowPreImportDialog()) {
207-
viewModel.shouldShowInitialInstructionalPrompt()
218+
val launchSource = getLaunchSource()
219+
if (canShowPreImportDialog(launchSource)) {
220+
viewModel.shouldShowInitialInstructionalPrompt(launchSource)
208221
} else {
209222
startImportWebFlow()
210223
}
211224
}
212225

213-
private fun canShowPreImportDialog() = arguments?.getBoolean(KEY_SHOW_INITIAL_INSTRUCTIONAL_PROMPT, true) ?: true
226+
private fun canShowPreImportDialog(launchSource: AutofillImportLaunchSource): Boolean {
227+
return when (launchSource) {
228+
PasswordManagementPromo -> false
229+
else -> true
230+
}
231+
}
214232

215233
override fun onCreateView(
216234
inflater: LayoutInflater,
@@ -263,7 +281,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
263281

264282
private fun onImportGcmButtonClicked() {
265283
startImportWebFlow()
266-
importPasswordsPixelSender.onImportPasswordsDialogImportButtonClicked()
284+
importPasswordsPixelSender.onImportPasswordsDialogImportButtonClicked(getLaunchSource())
267285
}
268286

269287
private fun startImportWebFlow() {
@@ -285,7 +303,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
285303
return
286304
}
287305

288-
importPasswordsPixelSender.onUserCancelledImportPasswordsDialog()
306+
importPasswordsPixelSender.onUserCancelledImportPasswordsDialog(getLaunchSource())
289307

290308
dismiss()
291309
}
@@ -298,12 +316,12 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() {
298316

299317
companion object {
300318

301-
private const val KEY_SHOW_INITIAL_INSTRUCTIONAL_PROMPT = "showInitialInstructionalPrompt"
319+
private const val KEY_LAUNCH_SOURCE = "launchSource"
302320

303-
fun instance(showInitialInstructionalPrompt: Boolean): ImportFromGooglePasswordsDialog {
321+
fun instance(importSource: AutofillImportLaunchSource): ImportFromGooglePasswordsDialog {
304322
val fragment = ImportFromGooglePasswordsDialog()
305323
fragment.arguments = Bundle().apply {
306-
putBoolean(KEY_SHOW_INITIAL_INSTRUCTIONAL_PROMPT, showInitialInstructionalPrompt)
324+
putParcelable(KEY_LAUNCH_SOURCE, importSource)
307325
}
308326
return fragment
309327
}

0 commit comments

Comments
 (0)