Skip to content

Commit

Permalink
Issue mozilla-mobile#10205: Add support for select credit card prompt
Browse files Browse the repository at this point in the history
- Implements an override function of `onCreditCardSelect` in `GeckoPromptDelegate`
- Adds a new `CreditCard` data class in `concept-engine`. This is a parallel of GV's
`Autocomplete.CreditCard`. We can't using the existing `CreditCard` from `concept-storage`
since that has encryption dependencies whereas the card number is already decrypted
when it reaches GV.
- Adds a new `SelectCreditCard` in `PromptRequest`
  • Loading branch information
gabrielluong committed May 11, 2021
1 parent 471da2b commit bf725a2
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 5 deletions.
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?,
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

0 comments on commit bf725a2

Please sign in to comment.