Skip to content

Commit d352fce

Browse files
authored
Restrict promo from showing on multi-page login scenarios (#6405)
Task/Issue URL: https://app.asana.com/1/137249556945/task/1210810000390746?focus=true ### Description Restricts the in-browser password promo prompt from showing multiple times on the same site, especially added to ensure it won't show multiples times during a multi-step login form scenario. If you visit another site and are offered it there, you can see it again on the original site too since we only check the last site that was offered (and only held in memory). ### Steps to test this PR QA optional. - [ ] Fresh install - [ ] Visit amazon login page and tap on the email address field; verify you are prompted to import - [ ] Tap off of the dialog to soft-dismiss it. Enter your email address and get to the password entry page. - [ ] Tap on the password field; verify you do not see the promo prompt - [ ] Now visit https://fill.dev/form/login-simple. tap on the username field and verify you are shown the prompt --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210729428247645
1 parent 5ee7574 commit d352fce

File tree

4 files changed

+115
-8
lines changed

4 files changed

+115
-8
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class RealInBrowserImportPromo @Inject constructor(
4343
private val neverSavedSiteRepository: NeverSavedSiteRepository,
4444
private val autofillFeature: AutofillFeature,
4545
private val webViewCapabilityChecker: WebViewCapabilityChecker,
46+
private val inBrowserPromoPreviousPromptsStore: InBrowserPromoPreviousPromptsStore,
4647
) : InBrowserImportPromo {
4748

4849
override suspend fun canShowPromo(
@@ -54,6 +55,14 @@ class RealInBrowserImportPromo @Inject constructor(
5455
return@withContext false
5556
}
5657

58+
if (url == null) {
59+
return@withContext false
60+
}
61+
62+
if (inBrowserPromoPreviousPromptsStore.hasPromoBeenDisplayed(url)) {
63+
return@withContext false
64+
}
65+
5766
if (featureEnabled().not()) {
5867
return@withContext false
5968
}
@@ -74,7 +83,7 @@ class RealInBrowserImportPromo @Inject constructor(
7483
return@withContext false
7584
}
7685

77-
if (url != null && neverSavedSiteRepository.isInNeverSaveList(url)) {
86+
if (neverSavedSiteRepository.isInNeverSaveList(url)) {
7887
return@withContext false
7988
}
8089

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.urlmatcher.AutofillUrlMatcher
20+
import com.duckduckgo.common.utils.DispatcherProvider
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import dagger.SingleInstanceIn
24+
import javax.inject.Inject
25+
import kotlinx.coroutines.withContext
26+
27+
@SingleInstanceIn(AppScope::class)
28+
@ContributesBinding(AppScope::class)
29+
class InMemoryInBrowserPromoPreviousPromptsStore @Inject constructor(
30+
private val urlMatcher: AutofillUrlMatcher,
31+
private val dispatchers: DispatcherProvider,
32+
) : InBrowserPromoPreviousPromptsStore {
33+
34+
private var previousETldPlusOneOffered: String? = null
35+
36+
override suspend fun recordPromoDisplayed(originalUrl: String) {
37+
withContext(dispatchers.io()) {
38+
val eTldPlusOne = urlMatcher.extractUrlPartsForAutofill(originalUrl).eTldPlus1 ?: return@withContext
39+
previousETldPlusOneOffered = eTldPlusOne
40+
}
41+
}
42+
43+
override suspend fun hasPromoBeenDisplayed(originalUrl: String): Boolean {
44+
if (previousETldPlusOneOffered == null) return false
45+
46+
return withContext(dispatchers.io()) {
47+
val eTldPlusOne = urlMatcher.extractUrlPartsForAutofill(originalUrl).eTldPlus1 ?: return@withContext false
48+
eTldPlusOne.equals(previousETldPlusOneOffered, ignoreCase = true)
49+
}
50+
}
51+
}
52+
53+
interface InBrowserPromoPreviousPromptsStore {
54+
55+
suspend fun recordPromoDisplayed(originalUrl: String)
56+
suspend fun hasPromoBeenDisplayed(originalUrl: String): Boolean
57+
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class ResultHandlerImportPasswords @Inject constructor(
3939
private val autofillAvailableInputTypesProvider: AutofillAvailableInputTypesProvider,
4040
private val dispatchers: DispatcherProvider,
4141
private val autofillMessagePoster: AutofillMessagePoster,
42+
private val previousPromoPrompts: InBrowserPromoPreviousPromptsStore,
4243
) : AutofillFragmentResultsPlugin {
4344

4445
override suspend fun processResult(
@@ -49,22 +50,27 @@ class ResultHandlerImportPasswords @Inject constructor(
4950
autofillCallback: AutofillEventListener,
5051
webView: WebView?,
5152
) {
52-
logcat { "Autofill: processing import passwords result for tab $tabId" }
53+
val originalUrl = result.getString(ImportPasswordsDialog.KEY_URL) ?: return
54+
storeUrlForPromptToAvoidAskingAgain(originalUrl)
55+
56+
logcat { "Autofill: processing import passwords result for url=$originalUrl, tab=$tabId" }
5357
if (result.getBoolean(ImportPasswordsDialog.KEY_IMPORT_SUCCESS)) {
5458
logcat { "Autofill: refresh after import passwords success" }
55-
val originalUrl = result.getString(ImportPasswordsDialog.KEY_URL)
56-
if (originalUrl != null && webView != null) {
59+
if (webView != null) {
5760
refreshAvailableInputTypes(webView, originalUrl)
5861
} else {
59-
logcat { "Autofill: cannot refresh available input types for url=$originalUrl (webView is null: ${webView == null})" }
62+
logcat { "Autofill: cannot refresh available input types for url=$originalUrl, webView is null" }
6063
}
6164
} else {
6265
logcat { "Autofill: import didn't succeed; returning a 'no credential' response" }
63-
val originalUrl = result.getString(ImportPasswordsDialog.KEY_URL) ?: return
6466
autofillCallback.onNoCredentialsChosenForAutofill(originalUrl)
6567
}
6668
}
6769

70+
private suspend fun storeUrlForPromptToAvoidAskingAgain(originalUrl: String) {
71+
previousPromoPrompts.recordPromoDisplayed(originalUrl)
72+
}
73+
6874
private suspend fun refreshAvailableInputTypes(
6975
webView: WebView,
7076
originalUrl: String,

autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/RealInBrowserImportPromoTest.kt

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.junit.Test
2020
import org.junit.runner.RunWith
2121
import org.junit.runners.Parameterized
2222
import org.mockito.Mockito.mock
23+
import org.mockito.kotlin.anyOrNull
2324
import org.mockito.kotlin.whenever
2425

2526
@SuppressLint("DenyListedApi")
@@ -35,13 +36,15 @@ class RealInBrowserImportPromoParameterizedTest(
3536
private val autofillStore: InternalAutofillStore = mock()
3637
private val neverSavedSiteRepository: NeverSavedSiteRepository = mock()
3738
private val webViewCapabilityChecker: WebViewCapabilityChecker = mock()
39+
private val inBrowserPromoPreviousPromptsStore: InBrowserPromoPreviousPromptsStore = mock()
3840

3941
private val testee = RealInBrowserImportPromo(
4042
autofillStore = autofillStore,
4143
dispatchers = coroutineTestRule.testDispatcherProvider,
4244
neverSavedSiteRepository = neverSavedSiteRepository,
4345
autofillFeature = autofillFeature,
4446
webViewCapabilityChecker = webViewCapabilityChecker,
47+
inBrowserPromoPreviousPromptsStore = inBrowserPromoPreviousPromptsStore,
4548
)
4649

4750
@Before
@@ -56,6 +59,7 @@ class RealInBrowserImportPromoParameterizedTest(
5659
autofillFeature.self().setRawStoredState(State(enable = testCase.autofillFeatureEnabled))
5760
whenever(webViewCapabilityChecker.isSupported(WebMessageListener)).thenReturn(testCase.webViewWebMessageSupport)
5861
whenever(webViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(testCase.webViewDocumentStartJavascript)
62+
whenever(inBrowserPromoPreviousPromptsStore.hasPromoBeenDisplayed(anyOrNull())).thenReturn(testCase.promoPreviouslyShownForUrl)
5963
}
6064

6165
@Test
@@ -235,8 +239,8 @@ class RealInBrowserImportPromoParameterizedTest(
235239
hasDeclinedPromo = false,
236240
credentialCount = 0,
237241
promoShownCount = 0,
238-
expected = true,
239-
description = "eligible: url is null, all other conditions met",
242+
expected = false,
243+
description = "ineligible: url is null",
240244
),
241245
CanShowPromoTestCase(
242246
credentialsAvailableForCurrentPage = true,
@@ -278,6 +282,36 @@ class RealInBrowserImportPromoParameterizedTest(
278282
expected = false,
279283
description = "ineligible: webview does not support DocumentStartJavaScript",
280284
),
285+
CanShowPromoTestCase(
286+
credentialsAvailableForCurrentPage = false,
287+
url = EXAMPLE_URL,
288+
inBrowserPromoFeatureEnabled = true,
289+
autofillFeatureEnabled = true,
290+
hasEverImportedPasswords = false,
291+
hasDeclinedPromo = false,
292+
credentialCount = 0,
293+
promoShownCount = 0,
294+
webViewWebMessageSupport = true,
295+
webViewDocumentStartJavascript = true,
296+
promoPreviouslyShownForUrl = true,
297+
expected = false,
298+
description = "ineligible: promo previously shown for url",
299+
),
300+
CanShowPromoTestCase(
301+
credentialsAvailableForCurrentPage = false,
302+
url = EXAMPLE_URL,
303+
inBrowserPromoFeatureEnabled = true,
304+
autofillFeatureEnabled = true,
305+
hasEverImportedPasswords = false,
306+
hasDeclinedPromo = false,
307+
credentialCount = 0,
308+
promoShownCount = 0,
309+
webViewWebMessageSupport = true,
310+
webViewDocumentStartJavascript = true,
311+
promoPreviouslyShownForUrl = false,
312+
expected = true,
313+
description = "eligible: promo not previously shown for url",
314+
),
281315
)
282316
}
283317

@@ -301,6 +335,7 @@ data class CanShowPromoTestCase(
301335
val promoShownCount: Int,
302336
val webViewWebMessageSupport: Boolean = true,
303337
val webViewDocumentStartJavascript: Boolean = true,
338+
val promoPreviouslyShownForUrl: Boolean = false,
304339
val expected: Boolean,
305340
val description: String,
306341
) {

0 commit comments

Comments
 (0)