Skip to content

Commit 43b93f4

Browse files
committed
Add in-browser prompt to import Google Passwords
1 parent dd71734 commit 43b93f4

File tree

23 files changed

+690
-798
lines changed

23 files changed

+690
-798
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ 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.AutofillPrompt
213+
import com.duckduckgo.autofill.api.AutofillPrompt.ImportPasswords
212214
import com.duckduckgo.autofill.api.AutofillScreenLaunchSource
213215
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreenWithSuggestions
214216
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementViewCredential
@@ -755,6 +757,20 @@ class BrowserTabFragment :
755757
viewModel.onShowUserCredentialsSaved(savedCredentials)
756758
}
757759

760+
override suspend fun promptUserTo(event: AutofillPrompt) {
761+
withContext(dispatchers.main()) {
762+
when (event) {
763+
is ImportPasswords -> {
764+
showDialogHidingPrevious(
765+
credentialAutofillDialogFactory.autofillImportPasswordsDialog(tabId),
766+
AUTOFILL_DIALOG_TAB,
767+
event.currentUrl,
768+
)
769+
}
770+
}
771+
}
772+
}
773+
758774
override suspend fun onCredentialsAvailableToSave(
759775
currentUrl: String,
760776
credentials: LoginCredentials,
@@ -1769,6 +1785,12 @@ class BrowserTabFragment :
17691785
viewModel.onRefreshRequested(triggeredByUser = false)
17701786
}
17711787

1788+
override fun onNewPasswordsImported() {
1789+
webView?.let {
1790+
browserAutofill.onNewAutofillDataAvailable()
1791+
}
1792+
}
1793+
17721794
override fun onRejectGeneratedPassword(originalUrl: String) {
17731795
rejectGeneratedPassword(originalUrl)
17741796
}
@@ -3985,6 +4007,7 @@ class BrowserTabFragment :
39854007
}
39864008

39874009
companion object {
4010+
private const val AUTOFILL_DIALOG_TAB = "AUTOFILL_DIALOG_TAB"
39884011
private const val CUSTOM_TAB_TOOLBAR_COLOR_ARG = "CUSTOM_TAB_TOOLBAR_COLOR_ARG"
39894012
private const val TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG = "TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG"
39904013
private const val TAB_ID_ARG = "TAB_ID_ARG"

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

Lines changed: 5 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(tabId: String): DialogFragment
252257
}
253258

254259
private fun prefix(

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

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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 android.content.Context
20+
import android.os.Bundle
21+
import androidx.fragment.app.Fragment
22+
import com.duckduckgo.autofill.api.AutofillEventListener
23+
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
24+
import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog.ImportPasswordsDialog
25+
import com.duckduckgo.di.scopes.AppScope
26+
import com.squareup.anvil.annotations.ContributesMultibinding
27+
import javax.inject.Inject
28+
import logcat.logcat
29+
30+
@ContributesMultibinding(AppScope::class)
31+
class ResultHandlerImportPasswords @Inject constructor() : AutofillFragmentResultsPlugin {
32+
33+
override fun processResult(
34+
result: Bundle,
35+
context: Context,
36+
tabId: String,
37+
fragment: Fragment,
38+
autofillCallback: AutofillEventListener,
39+
) {
40+
if (result.getBoolean(ImportPasswordsDialog.KEY_IMPORT_SUCCESS)) {
41+
logcat { "Autofill: refresh after import passwords success" }
42+
autofillCallback.onNewPasswordsImported()
43+
}
44+
}
45+
46+
override fun resultKey(tabId: String): String {
47+
return ImportPasswordsDialog.resultKey(tabId)
48+
}
49+
}

0 commit comments

Comments
 (0)