diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index fe065d6389c..1462d81eb02 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -64,6 +64,43 @@ typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) : PromptDelegate { + override fun onCreditCardSave( + session: GeckoSession, + request: AutocompleteRequest + ): GeckoResult { + val geckoResult = GeckoResult() + + val onConfirm: (CreditCardEntry) -> Unit = { creditCard -> + if (!request.isComplete) { + geckoResult.complete( + request.confirm( + Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard()) + ) + ) + } + } + + val onDismiss: () -> Unit = { + request.dismissSafely(geckoResult) + } + + geckoEngineSession.notifyObservers { + onPromptRequest( + PromptRequest.SaveCreditCard( + creditCard = request.options[0].value.toCreditCardEntry(), + onDismiss = onDismiss, + onConfirm = onConfirm + ).also { + request.delegate = PromptInstanceDismissDelegate( + geckoEngineSession, it + ) + } + ) + } + + return geckoResult + } + /** * Handle a credit card selection prompt request. This is triggered by the user * focusing on a credit card input field. @@ -721,7 +758,6 @@ internal fun Date.toString(format: String): String { * Only dismiss if the prompt is not already dismissed. */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun PromptDelegate.BasePrompt.dismissSafely(geckoResult: GeckoResult) { if (!this.isComplete) { geckoResult.complete(dismiss()) diff --git a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt index 760a17bfa38..1e10ef7acd8 100644 --- a/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt +++ b/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt @@ -834,6 +834,95 @@ class GeckoPromptDelegateTest { passwordField = passwordField ) + @Test + fun `Calling onCreditCardSave must provide an SaveCreditCard PromptRequest`() { + val mockSession = GeckoEngineSession(runtime) + var onCreditCardSaved = false + var onDismissWasCalled = false + + var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock() + + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + mockSession.register(object : EngineSession.Observer { + override fun onPromptRequest(promptRequest: PromptRequest) { + saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard + } + }) + + val creditCard = CreditCardEntry( + guid = "1", + name = "Banana Apple", + number = "4111111111111110", + expiryMonth = "5", + expiryYear = "2030", + cardType = "amex" + ) + val creditCardSaveOption = + Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard()) + + var geckoResult = promptDelegate.onCreditCardSave( + mock(), + geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption)) + ) + + geckoResult.accept { + onDismissWasCalled = true + } + + saveCreditCardPrompt.onDismiss() + shadowOf(getMainLooper()).idle() + assertTrue(onDismissWasCalled) + + val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption)) + geckoResult = promptDelegate.onCreditCardSave(mock(), geckoPrompt) + + geckoResult.accept { + onCreditCardSaved = true + } + + saveCreditCardPrompt.onConfirm(creditCard) + shadowOf(getMainLooper()).idle() + + assertTrue(onCreditCardSaved) + + whenever(geckoPrompt.isComplete).thenReturn(true) + onCreditCardSaved = false + saveCreditCardPrompt.onConfirm(creditCard) + + assertFalse(onCreditCardSaved) + } + + @Test + fun `Calling onCreditSave must set a PromptInstanceDismissDelegate`() { + val mockSession = GeckoEngineSession(runtime) + var saveCreditCardPrompt: PromptRequest.SaveCreditCard = mock() + val promptDelegate = spy(GeckoPromptDelegate(mockSession)) + + mockSession.register(object : EngineSession.Observer { + override fun onPromptRequest(promptRequest: PromptRequest) { + saveCreditCardPrompt = promptRequest as PromptRequest.SaveCreditCard + } + }) + + val creditCard = CreditCardEntry( + guid = "1", + name = "Banana Apple", + number = "4111111111111110", + expiryMonth = "5", + expiryYear = "2030", + cardType = "amex" + ) + val creditCardSaveOption = + Autocomplete.CreditCardSaveOption(creditCard.toAutocompleteCreditCard()) + val geckoPrompt = geckoCreditCardSavePrompt(arrayOf(creditCardSaveOption)) + + promptDelegate.onCreditCardSave(mock(), geckoPrompt) + + assertNotNull(saveCreditCardPrompt) + assertNotNull(geckoPrompt.delegate) + } + @Test fun `Calling onCreditCardSelect must provide as CreditCardSelectOption PromptRequest`() { val mockSession = GeckoEngineSession(runtime) @@ -1595,4 +1684,17 @@ class GeckoPromptDelegateTest { ReflectionUtils.setField(prompt, "options", creditCards) return prompt } + + @Suppress("UNCHECKED_CAST") + private fun geckoCreditCardSavePrompt( + creditCard: Array + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt = Mockito.mock( + GeckoSession.PromptDelegate.AutocompleteRequest::class.java, + Mockito.RETURNS_DEEP_STUBS // for testing prompt.delegate + ) as GeckoSession.PromptDelegate.AutocompleteRequest + + ReflectionUtils.setField(prompt, "options", creditCard) + return prompt + } } diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt index 68d85c512a8..347ddaac620 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt @@ -92,6 +92,18 @@ sealed class PromptRequest( val onStay: () -> Unit ) : PromptRequest() + /** + * Value type that represents a request for a save credit card prompt. + * @property creditCard the [CreditCardEntry] to save or update. + * @property onDismiss callback to let the page know the user dismissed the dialog. + * @property onConfirm callback that is called when the user confirms the credit card selection. + */ + data class SaveCreditCard( + val creditCard: CreditCardEntry, + override val onDismiss: () -> Unit, + val onConfirm: (CreditCardEntry) -> Unit + ) : PromptRequest(shouldDismissOnLoad = false), Dismissible + /** * Value type that represents a request for a select credit card prompt. * @property creditCards a list of [CreditCardEntry]s to select from. diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt index 0f1a58449bc..63133b03fc4 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt @@ -33,6 +33,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.Popup import mozilla.components.concept.engine.prompt.PromptRequest.Repost +import mozilla.components.concept.engine.prompt.PromptRequest.SaveCreditCard import mozilla.components.concept.engine.prompt.PromptRequest.SaveLoginPrompt import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt @@ -790,6 +791,7 @@ class PromptFeature private constructor( is SaveLoginPrompt, is SelectLoginPrompt, is SelectCreditCard, + is SaveCreditCard, is Share -> true is Alert, is TextPrompt, is Confirm, is Repost, is Popup -> promptAbuserDetector.shouldShowMoreDialogs } diff --git a/docs/changelog.md b/docs/changelog.md index 8678f08dde7..23cd366fa27 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -26,6 +26,9 @@ permalink: /changelog/ * **concept-storage**: * Added `CreditCardValidationDelegate` which is a delegate that will check against the `CreditCardsAddressesStorage` to determine if a `CreditCard` can be persisted in storage. [#9838](https://github.com/mozilla-mobile/android-components/issues/9838) * Refactors `CreditCard` from `concept-engine` to `CreditCardEntry` in `concept-storage` so that it can validated with the `CreditCardValidationDelegate`. [#9838](https://github.com/mozilla-mobile/android-components/issues/9838) + +* **concept-engine** + * Adds a new `SaveCreditCard` in `PromptRequest` to display a prompt for saving a credit card on autofill. [#11249](https://github.com/mozilla-mobile/android-components/issues/11249) # 101.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v100.0.0...v101.0.0)