Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Issue #10205: Add support for select credit card prompt #10213

Merged
merged 1 commit into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.engine.gecko.ext

import mozilla.components.concept.engine.prompt.CreditCard
import org.mozilla.geckoview.Autocomplete

// Placeholder for the card type. This will be replaced when we can identify the card type.
// This is dependent on https://github.com/mozilla-mobile/android-components/issues/9813.
private const val CARD_TYPE_PLACEHOLDER = ""

/**
* Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCard].
*/
fun Autocomplete.CreditCard.toCreditCard() = CreditCard(
guid = guid,
name = name,
number = number,
expiryMonth = expirationMonth,
expiryYear = expirationYear,
cardType = CARD_TYPE_PLACEHOLDER
)

/**
* Converts an Android Components [CreditCard] to a GeckoView [Autocomplete.CreditCard].
*/
fun CreditCard.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder()
.guid(guid)
.name(name)
.number(number)
.expirationMonth(expiryMonth)
.expirationYear(expiryYear)
.build()
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import android.content.Context
import android.net.Uri
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
import mozilla.components.browser.engine.gecko.ext.toCreditCard
import mozilla.components.browser.engine.gecko.ext.toLogin
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.engine.prompt.Choice
import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.MenuChoice
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
Expand All @@ -25,6 +28,7 @@ import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.PromptDelegate
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
import org.mozilla.geckoview.GeckoSession.PromptDelegate.BeforeUnloadPrompt
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATE
import org.mozilla.geckoview.GeckoSession.PromptDelegate.DateTimePrompt.Type.DATETIME_LOCAL
Expand Down Expand Up @@ -59,9 +63,49 @@ typealias AC_FILE_FACING_MODE = PromptRequest.File.FacingMode
internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSession) :
PromptDelegate {

/**
* Handle a credit card selection prompt request. This is triggered by the user
* focusing on a credit card input field.
*
* @param session The [GeckoSession] that triggered the request.
* @param request The [AutocompleteRequest] containing the credit card selection request.
*/
override fun onCreditCardSelect(
session: GeckoSession,
request: AutocompleteRequest<Autocomplete.CreditCardSelectOption>
): GeckoResult<PromptResponse>? {
val geckoResult = GeckoResult<PromptResponse>()

val onConfirm: (CreditCard) -> Unit = { creditCard ->
if (!request.isComplete) {
geckoResult.complete(
request.confirm(
Autocomplete.CreditCardSelectOption(creditCard.toAutocompleteCreditCard())
)
)
}
}

val onDismiss: () -> Unit = {
request.dismissSafely(geckoResult)
}

geckoEngineSession.notifyObservers {
onPromptRequest(
PromptRequest.SelectCreditCard(
creditCards = request.options.map { it.value.toCreditCard() },
onDismiss = onDismiss,
onConfirm = onConfirm
)
)
}

return geckoResult
}

override fun onLoginSave(
session: GeckoSession,
prompt: PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption>
prompt: AutocompleteRequest<Autocomplete.LoginSaveOption>
): GeckoResult<PromptResponse>? {
val geckoResult = GeckoResult<PromptResponse>()
val onConfirmSave: (Login) -> Unit = { login ->
Expand All @@ -88,7 +132,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe

override fun onLoginSelect(
session: GeckoSession,
prompt: PromptDelegate.AutocompleteRequest<Autocomplete.LoginSelectOption>
prompt: AutocompleteRequest<Autocomplete.LoginSelectOption>
): GeckoResult<PromptResponse>? {
val geckoResult = GeckoResult<PromptResponse>()
val onConfirmSelect: (Login) -> Unit = { login ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ package mozilla.components.browser.engine.gecko.prompt
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.browser.engine.gecko.GeckoEngineSession
import mozilla.components.browser.engine.gecko.ext.toAutocompleteCreditCard
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.prompt.Choice
import mozilla.components.concept.engine.prompt.CreditCard
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
Expand All @@ -30,10 +32,10 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mozilla.geckoview.AllowOrDeny
import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.GeckoResult
Expand Down Expand Up @@ -755,6 +757,75 @@ class GeckoPromptDelegateTest {
passwordField = passwordField
)

@Test
fun `Calling onCreditCardSelect must provide as CreditCardSelectOption PromptRequest`() {
val mockSession = GeckoEngineSession(runtime)
var onConfirmWasCalled = false
var onDismissWasCalled = false

var selectCreditCardPrompt: PromptRequest.SelectCreditCard = mock()

val promptDelegate = spy(GeckoPromptDelegate(mockSession))

mockSession.register(object : EngineSession.Observer {
override fun onPromptRequest(promptRequest: PromptRequest) {
selectCreditCardPrompt = promptRequest as PromptRequest.SelectCreditCard
}
})

val creditCard1 = CreditCard(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
expiryMonth = "5",
expiryYear = "2030",
cardType = "amex"
)
val creditCardSelectOption1 =
Autocomplete.CreditCardSelectOption(creditCard1.toAutocompleteCreditCard())

val creditCard2 = CreditCard(
guid = "2",
name = "Orange Pineapple",
number = "4111111111115555",
expiryMonth = "1",
expiryYear = "2040",
cardType = "amex"
)
val creditCardSelectOption2 =
Autocomplete.CreditCardSelectOption(creditCard2.toAutocompleteCreditCard())

var geckoResult = promptDelegate.onCreditCardSelect(
mock(),
geckoSelectCreditCardPrompt(arrayOf(creditCardSelectOption1, creditCardSelectOption2))
)

geckoResult!!.accept {
onDismissWasCalled = true
}

selectCreditCardPrompt.onDismiss()
assertTrue(onDismissWasCalled)

val geckoPrompt =
geckoSelectCreditCardPrompt(arrayOf(creditCardSelectOption1, creditCardSelectOption2))
geckoResult = promptDelegate.onCreditCardSelect(mock(), geckoPrompt)

geckoResult!!.accept {
onConfirmWasCalled = true
}

selectCreditCardPrompt.onConfirm(creditCard1)

assertTrue(onConfirmWasCalled)

whenever(geckoPrompt.isComplete).thenReturn(true)
onConfirmWasCalled = false
selectCreditCardPrompt.onConfirm(creditCard1)

assertFalse(onConfirmWasCalled)
}

@Test
fun `Calling onAuthPrompt must provide an Authentication PromptRequest`() {
val mockSession = GeckoEngineSession(runtime)
Expand Down Expand Up @@ -1410,4 +1481,13 @@ class GeckoPromptDelegateTest {
private fun geckoRepostPrompt(): GeckoSession.PromptDelegate.RepostConfirmPrompt {
return mock()
}

private fun geckoSelectCreditCardPrompt(
creditCards: Array<Autocomplete.CreditCardSelectOption>
): GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption> {
val prompt: GeckoSession.PromptDelegate.AutocompleteRequest<Autocomplete.CreditCardSelectOption> =
mock()
ReflectionUtils.setField(prompt, "options", creditCards)
return prompt
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.concept.engine.prompt

import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

/**
* Value type that represents a credit card.
*
* @property guid The unique identifier for this credit card.
* @property name The credit card billing name.
* @property number The credit card number.
* @property expiryMonth The credit card expiry month.
* @property expiryYear The credit card expiry year.
* @property cardType The credit card network ID.
*/
@Parcelize
data class CreditCard(
val guid: String?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit strange to have an identifier that can be null? Would you mind helping me to clarify this part?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems odd. Maybe there is a mistake in the API? The other fields are non-null, so a GUID should also be non-null.

val name: String,
val number: String,
val expiryMonth: String,
val expiryYear: String,
val cardType: String
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type

/**
* Value type that represents a request for showing a native dialog for prompt web content.
*
*/
sealed class PromptRequest {
/**
Expand Down Expand Up @@ -67,6 +66,18 @@ sealed class PromptRequest {
val onStay: () -> Unit
) : PromptRequest()

/**
* Value type that represents a request for a select credit card prompt.
* @property creditCards a list of [CreditCard]s to select from.
* @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 SelectCreditCard(
val creditCards: List<CreditCard>,
override val onDismiss: () -> Unit,
val onConfirm: (CreditCard) -> Unit
) : PromptRequest(), Dismissible

/**
* Value type that represents a request for a save login prompt.
* @property hint a value that helps to determine the appropriate prompting behavior.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.concept.engine.prompt

import org.junit.Assert.assertEquals
import org.junit.Test

class CreditCardTest {

@Test
fun `Create a CreditCard`() {
val guid = "1"
val name = "Banana Apple"
val number = "4111111111111110"
val expiryMonth = "5"
val expiryYear = "2030"
val cardType = "amex"
val creditCard = CreditCard(
guid = guid,
name = name,
number = number,
expiryMonth = expiryMonth,
expiryYear = expiryYear,
cardType = cardType
)

assertEquals(guid, creditCard.guid)
assertEquals(name, creditCard.name)
assertEquals(number, creditCard.number)
assertEquals(expiryMonth, creditCard.expiryMonth)
assertEquals(expiryYear, creditCard.expiryYear)
assertEquals(cardType, creditCard.cardType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.SaveLoginPrompt
import mozilla.components.concept.engine.prompt.PromptRequest.SelectCreditCard
import mozilla.components.concept.engine.prompt.PromptRequest.SelectLoginPrompt
import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice
import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt
Expand All @@ -24,6 +25,7 @@ import mozilla.components.support.test.mock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.Date
Expand Down Expand Up @@ -256,4 +258,47 @@ class PromptRequestTest {
assertTrue(onAcceptWasCalled)
assertTrue(onDismissWasCalled)
}

@Test
fun `GIVEN a list of credit cards WHEN SelectCreditCard is confirmed or dismissed THEN their respective callback is invoked`() {
val creditCard = CreditCard(
guid = "id",
name = "Banana Apple",
number = "4111111111111110",
expiryMonth = "5",
expiryYear = "2030",
cardType = "amex"
)
var onDismissCalled = false
var onConfirmCalled = false
var confirmedCreditCard: CreditCard? = null

val selectCreditCardPrompt = SelectCreditCard(
creditCards = listOf(creditCard),
onDismiss = {
onDismissCalled = true
},
onConfirm = {
confirmedCreditCard = it
onConfirmCalled = true
}
)

assertEquals(selectCreditCardPrompt.creditCards, listOf(creditCard))

selectCreditCardPrompt.onConfirm(creditCard)

assertTrue(onConfirmCalled)
assertFalse(onDismissCalled)
assertEquals(creditCard, confirmedCreditCard)

onConfirmCalled = false
confirmedCreditCard = null

selectCreditCardPrompt.onDismiss()

assertTrue(onDismissCalled)
assertFalse(onConfirmCalled)
assertNull(confirmedCreditCard)
}
}
Loading