Skip to content

Commit f5689bd

Browse files
committed
Add in-browser prompt to import Google Passwords
1 parent ca959e1 commit f5689bd

File tree

16 files changed

+221
-20
lines changed

16 files changed

+221
-20
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ import com.duckduckgo.autoconsent.api.AutoconsentCallback
208208
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
209209
import com.duckduckgo.autofill.api.AutofillEventListener
210210
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
211+
import com.duckduckgo.autofill.api.AutofillPrompt
212+
import com.duckduckgo.autofill.api.AutofillPrompt.ImportPasswords
211213
import com.duckduckgo.autofill.api.AutofillScreenLaunchSource
212214
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreenWithSuggestions
213215
import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementViewCredential
@@ -754,6 +756,20 @@ class BrowserTabFragment :
754756
viewModel.onShowUserCredentialsSaved(savedCredentials)
755757
}
756758

759+
override suspend fun promptUserTo(event: AutofillPrompt) {
760+
withContext(dispatchers.main()) {
761+
when (event) {
762+
is ImportPasswords -> {
763+
showDialogHidingPrevious(
764+
credentialAutofillDialogFactory.autofillImportPasswordsDialog(tabId),
765+
AUTOFILL_DIALOG_TAB,
766+
event.currentUrl,
767+
)
768+
}
769+
}
770+
}
771+
}
772+
757773
override suspend fun onCredentialsAvailableToSave(
758774
currentUrl: String,
759775
credentials: LoginCredentials,
@@ -3984,6 +4000,7 @@ class BrowserTabFragment :
39844000
}
39854001

39864002
companion object {
4003+
private const val AUTOFILL_DIALOG_TAB = "AUTOFILL_DIALOG_TAB"
39874004
private const val CUSTOM_TAB_TOOLBAR_COLOR_ARG = "CUSTOM_TAB_TOOLBAR_COLOR_ARG"
39884005
private const val TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG = "TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG"
39894006
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/BrowserAutofill.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,10 @@ interface Callback {
153153
* Called when credentials have been saved, and we want to show the user some visual confirmation.
154154
*/
155155
fun onCredentialsSaved(savedCredentials: LoginCredentials)
156+
157+
suspend fun promptUserTo(event: AutofillPrompt)
158+
}
159+
160+
sealed interface AutofillPrompt {
161+
data class ImportPasswords(val currentUrl: String) : AutofillPrompt
156162
}

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

Lines changed: 15 additions & 1 deletion
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
@@ -133,6 +135,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
133135
private val partialCredentialSaveStore: PartialCredentialSaveStore,
134136
private val usernameBackFiller: UsernameBackFiller,
135137
private val existingCredentialMatchDetector: ExistingCredentialMatchDetector,
138+
private val inBrowserImportPromo: InBrowserImportPromo,
136139
) : AutofillJavascriptInterface {
137140

138141
override var callback: Callback? = null
@@ -186,6 +189,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
186189
}
187190
}
188191

192+
private fun handlePromoteImport(url: String) {
193+
coroutineScope.launch {
194+
callback?.promptUserTo(ImportPasswords(url))
195+
}
196+
}
197+
189198
@JavascriptInterface
190199
override fun getIncontextSignupDismissedAt(data: String) {
191200
emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) {
@@ -246,7 +255,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor(
246255
val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials)
247256

248257
if (finalCredentialList.isEmpty()) {
249-
callback?.noCredentialsAvailable(url)
258+
val canShowImport = inBrowserImportPromo.canShowPromo()
259+
if (canShowImport) {
260+
handlePromoteImport(url)
261+
} else {
262+
callback?.noCredentialsAvailable(url)
263+
}
250264
} else {
251265
callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
252266
}

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,51 @@
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+
override fun processResult(
33+
result: Bundle,
34+
context: Context,
35+
tabId: String,
36+
fragment: Fragment,
37+
autofillCallback: AutofillEventListener,
38+
) {
39+
if (result.getBoolean(ImportPasswordsDialog.KEY_IMPORT_SUCCESS)) {
40+
logcat { "Autofill: refresh after import passwords success" }
41+
42+
// TODO change this for another API that triggers a refresh without reloading the page
43+
autofillCallback.onAutofillStateChange()
44+
return
45+
}
46+
}
47+
48+
override fun resultKey(tabId: String): String {
49+
return ImportPasswordsDialog.resultKey(tabId)
50+
}
51+
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.duckduckgo.anvil.annotations.InjectWith
3636
import com.duckduckgo.app.statistics.pixels.Pixel
3737
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
3838
import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin
39+
import com.duckduckgo.autofill.api.AutofillPrompt
3940
import com.duckduckgo.autofill.api.BrowserAutofill
4041
import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory
4142
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
@@ -324,6 +325,10 @@ class ImportGooglePasswordsWebFlowFragment :
324325
}
325326
}
326327

328+
override suspend fun promptUserTo(event: AutofillPrompt) {
329+
// no-op, we don't prompt the user for anything in this flow
330+
}
331+
327332
override suspend fun onCsvAvailable(csv: String) {
328333
viewModel.onCsvAvailable(csv)
329334
}

autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ data class EmptyResponse(
5757
data class AvailableInputSuccessResponse(
5858
val credentials: AvailableInputTypeCredentials,
5959
val email: Boolean,
60+
val credentialsImport: Boolean,
6061
)
6162

6263
data class AvailableInputTypeCredentials(

0 commit comments

Comments
 (0)