Skip to content

Commit bda8bef

Browse files
committed
Add in-browser prompt to import Google Passwords
1 parent 359c78a commit bda8bef

33 files changed

+717
-842
lines changed

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ import com.duckduckgo.autoconsent.api.AutoconsentCallback
209209
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
210210
import com.duckduckgo.autofill.api.AutofillEventListener
211211
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
212+
import com.duckduckgo.autofill.api.AutofillImportLaunchSource.InBrowserPromo
213+
import com.duckduckgo.autofill.api.AutofillPrompt
214+
import com.duckduckgo.autofill.api.AutofillPrompt.ImportPasswords
212215
import com.duckduckgo.autofill.api.AutofillScreenLaunchSource
213216
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreenWithSuggestions
214217
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementViewCredential
@@ -755,6 +758,20 @@ class BrowserTabFragment :
755758
viewModel.onShowUserCredentialsSaved(savedCredentials)
756759
}
757760

761+
override suspend fun promptUserTo(event: AutofillPrompt) {
762+
withContext(dispatchers.main()) {
763+
when (event) {
764+
is ImportPasswords -> {
765+
showDialogHidingPrevious(
766+
credentialAutofillDialogFactory.autofillImportPasswordsDialog(importSource = InBrowserPromo, tabId),
767+
AUTOFILL_DIALOG_TAB,
768+
event.currentUrl,
769+
)
770+
}
771+
}
772+
}
773+
}
774+
758775
override suspend fun onCredentialsAvailableToSave(
759776
currentUrl: String,
760777
credentials: LoginCredentials,
@@ -1769,6 +1786,12 @@ class BrowserTabFragment :
17691786
viewModel.onRefreshRequested(triggeredByUser = false)
17701787
}
17711788

1789+
override fun onNewPasswordsImported() {
1790+
webView?.let {
1791+
browserAutofill.onNewAutofillDataAvailable()
1792+
}
1793+
}
1794+
17721795
override fun onRejectGeneratedPassword(originalUrl: String) {
17731796
rejectGeneratedPassword(originalUrl)
17741797
}
@@ -3985,6 +4008,7 @@ class BrowserTabFragment :
39854008
}
39864009

39874010
companion object {
4011+
private const val AUTOFILL_DIALOG_TAB = "AUTOFILL_DIALOG_TAB"
39884012
private const val CUSTOM_TAB_TOOLBAR_COLOR_ARG = "CUSTOM_TAB_TOOLBAR_COLOR_ARG"
39894013
private const val TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG = "TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG"
39904014
private const val TAB_ID_ARG = "TAB_ID_ARG"

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,11 @@ interface CredentialAutofillDialogFactory {
249249
* Creates a dialog which prompts the user to sign up for Email Protection
250250
*/
251251
fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment
252+
253+
/**
254+
* Creates a dialog which prompts the user to import passwords from Google Passwords
255+
*/
256+
fun autofillImportPasswordsDialog(importSource: AutofillImportLaunchSource, tabId: String): DialogFragment
252257
}
253258

254259
private fun prefix(
@@ -257,3 +262,13 @@ private fun prefix(
257262
): String {
258263
return "$tabId/$tag"
259264
}
265+
266+
@Parcelize
267+
enum class AutofillImportLaunchSource(val value: String) : Parcelable {
268+
PasswordManagementPromo("password_management_promo"),
269+
PasswordManagementEmptyState("password_management_empty_state"),
270+
PasswordManagementOverflow("password_management_overflow"),
271+
AutofillSettings("autofill_settings_button"),
272+
InBrowserPromo("in_browser_promo"),
273+
Unknown("unknown"),
274+
}

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillEventListener.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,10 @@ interface AutofillEventListener {
9090
* Called when a change was detected in the autofill state, such that reloading the page may be necessary.
9191
*/
9292
fun onAutofillStateChange()
93+
94+
/**
95+
* Called when new passwords are imported.
96+
* This can be used to trigger the autofill js flow to re-fetch credentials.
97+
*/
98+
fun onNewPasswordsImported()
9399
}

autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ interface BrowserAutofill {
8181
* Informs the JS layer that the in-context Email Protection flow has finished
8282
*/
8383
fun inContextEmailProtectionFlowFinished()
84+
85+
/**
86+
* Informs the JS layer that new autofill data is available, and the JS flow to ask for credentials can start again.
87+
*/
88+
fun onNewAutofillDataAvailable()
8489
}
8590

8691
/**
@@ -153,4 +158,10 @@ interface Callback {
153158
* Called when credentials have been saved, and we want to show the user some visual confirmation.
154159
*/
155160
fun onCredentialsSaved(savedCredentials: LoginCredentials)
161+
162+
suspend fun promptUserTo(event: AutofillPrompt)
163+
}
164+
165+
sealed interface AutofillPrompt {
166+
data class ImportPasswords(val currentUrl: String) : AutofillPrompt
156167
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.webkit.JavascriptInterface
2020
import android.webkit.WebView
2121
import com.duckduckgo.app.di.AppCoroutineScope
2222
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
23+
import com.duckduckgo.autofill.api.AutofillPrompt.ImportPasswords
2324
import com.duckduckgo.autofill.api.Callback
2425
import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType
2526
import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener
@@ -33,6 +34,7 @@ import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator
3334
import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials
3435
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker
3536
import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore
37+
import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
3638
import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster
3739
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest
3840
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser
@@ -72,6 +74,7 @@ import javax.inject.Inject
7274
import kotlinx.coroutines.CoroutineScope
7375
import kotlinx.coroutines.launch
7476
import kotlinx.coroutines.withContext
77+
import logcat.LogPriority.DEBUG
7578
import logcat.LogPriority.ERROR
7679
import logcat.LogPriority.INFO
7780
import logcat.LogPriority.VERBOSE
@@ -110,6 +113,7 @@ interface AutofillJavascriptInterface {
110113

111114
@JavascriptInterface
112115
fun closeEmailProtectionTab(data: String)
116+
fun onNewAutofillDataAvailable()
113117
}
114118

115119
@ContributesBinding(AppScope::class)
@@ -133,6 +137,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
133137
private val partialCredentialSaveStore: PartialCredentialSaveStore,
134138
private val usernameBackFiller: UsernameBackFiller,
135139
private val existingCredentialMatchDetector: ExistingCredentialMatchDetector,
140+
private val inBrowserImportPromo: InBrowserImportPromo,
136141
) : AutofillJavascriptInterface {
137142

138143
override var callback: Callback? = null
@@ -147,10 +152,11 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
147152
private val storeFormDataJob = ConflatedJob()
148153
private val injectCredentialsJob = ConflatedJob()
149154
private val emailProtectionInContextSignupJob = ConflatedJob()
155+
private val autofillReloadJob = ConflatedJob()
150156

151157
@JavascriptInterface
152158
override fun getAutofillData(requestString: String) {
153-
logcat(VERBOSE) { "BrowserAutofill: getAutofillData called:\n$requestString" }
159+
logcat(DEBUG) { "BrowserAutofill: getAutofillData called:\n$requestString" }
154160
getAutofillDataJob += coroutineScope.launch(dispatcherProvider.io()) {
155161
val url = currentUrlProvider.currentUrl(webView)
156162
if (url == null) {
@@ -186,6 +192,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
186192
}
187193
}
188194

195+
private fun handlePromoteImport(url: String) {
196+
coroutineScope.launch {
197+
callback?.promptUserTo(ImportPasswords(url))
198+
}
199+
}
200+
189201
@JavascriptInterface
190202
override fun getIncontextSignupDismissedAt(data: String) {
191203
emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) {
@@ -201,6 +213,14 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
201213
emailProtectionInContextSignupFlowCallback?.closeInContextSignup()
202214
}
203215

216+
override fun onNewAutofillDataAvailable() {
217+
autofillReloadJob += coroutineScope.launch(dispatcherProvider.io()) {
218+
val json = autofillResponseWriter.generateResponseNewAutofillDataAvailable()
219+
logcat { "import completed; refresh request: $json" }
220+
autofillMessagePoster.postMessage(webView, json)
221+
}
222+
}
223+
204224
@Suppress("UNUSED_PARAMETER")
205225
@JavascriptInterface
206226
fun showInContextEmailProtectionSignupPrompt(data: String) {
@@ -246,7 +266,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
246266
val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials)
247267

248268
if (finalCredentialList.isEmpty()) {
249-
callback?.noCredentialsAvailable(url)
269+
val canShowImport = inBrowserImportPromo.canShowPromo()
270+
if (canShowImport) {
271+
handlePromoteImport(url)
272+
} else {
273+
callback?.noCredentialsAvailable(url)
274+
}
250275
} else {
251276
callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
252277
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,8 @@ class InlineBrowserAutofill @Inject constructor(
7979
override fun inContextEmailProtectionFlowFinished() {
8080
autofillInterface.inContextEmailProtectionFlowFinished()
8181
}
82+
83+
override fun onNewAutofillDataAvailable() {
84+
autofillInterface.onNewAutofillDataAvailable()
85+
}
8286
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.duckduckgo.autofill.api.AutofillFeature
2121
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
2222
import com.duckduckgo.autofill.api.email.EmailManager
2323
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules
24+
import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
2425
import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials
2526
import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials
2627
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
@@ -49,6 +50,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
4950
private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules,
5051
private val neverSavedSiteRepository: NeverSavedSiteRepository,
5152
private val siteSpecificFixesStore: AutofillSiteSpecificFixesStore,
53+
private val inBrowserPromo: InBrowserImportPromo,
5254
) : AutofillRuntimeConfigProvider {
5355
override suspend fun getRuntimeConfiguration(
5456
rawJs: String,
@@ -90,8 +92,9 @@ class RealAutofillRuntimeConfigProvider @Inject constructor(
9092
private suspend fun generateAvailableInputTypes(url: String?): String {
9193
val credentialsAvailable = determineIfCredentialsAvailable(url)
9294
val emailAvailable = determineIfEmailAvailable()
95+
val canPromoImport = inBrowserPromo.canShowPromo()
9396

94-
val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(credentialsAvailable, emailAvailable).also {
97+
val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(credentialsAvailable, emailAvailable, canPromoImport).also {
9598
logcat(VERBOSE) { "autofill-config: availableInputTypes for $url: \n$it" }
9699
}
97100
return "availableInputTypes = $json"

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface RuntimeConfigurationWriter {
2828
fun generateResponseGetAvailableInputTypes(
2929
credentialsAvailable: AvailableInputTypeCredentials,
3030
emailAvailable: Boolean,
31+
credentialsImport: Boolean,
3132
): String
3233

3334
fun generateContentScope(settingsJson: AutofillSiteSpecificFixesSettings): String
@@ -52,8 +53,9 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run
5253
override fun generateResponseGetAvailableInputTypes(
5354
credentialsAvailable: AvailableInputTypeCredentials,
5455
emailAvailable: Boolean,
56+
credentialsImport: Boolean,
5557
): String {
56-
val availableInputTypes = AvailableInputSuccessResponse(credentialsAvailable, emailAvailable)
58+
val availableInputTypes = AvailableInputSuccessResponse(credentialsAvailable, emailAvailable, credentialsImport)
5759
return availableInputTypesAdapter.toJson(availableInputTypes)
5860
}
5961

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

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.autofill.impl.importing
18+
19+
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
20+
import com.duckduckgo.di.scopes.AppScope
21+
import com.squareup.anvil.annotations.ContributesBinding
22+
import javax.inject.Inject
23+
24+
interface InBrowserImportPromo {
25+
suspend fun canShowPromo(): Boolean
26+
}
27+
28+
@ContributesBinding(AppScope::class)
29+
class RealInBrowserImportPromo @Inject constructor(
30+
private val autofillStore: InternalAutofillStore,
31+
) : InBrowserImportPromo {
32+
33+
override suspend fun canShowPromo(): Boolean {
34+
// TODO: stopShowing if:
35+
// user has imported
36+
// has more than 25 login
37+
// more than 5 times shown
38+
// user dismissed
39+
// once per form
40+
return autofillStore.hasEverImportedPasswords.not()
41+
}
42+
}

0 commit comments

Comments
 (0)