Skip to content

Commit

Permalink
Issue mozilla-mobile#9838: Introduce CreditCardValidationDelegate and…
Browse files Browse the repository at this point in the history
… implement onCreditCardSave in GeckoCreditCardsAddressesStorageDelegate
  • Loading branch information
gabrielluong committed Mar 11, 2021
1 parent c5e9046 commit 7713049
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ data class CreditCard(
val expiryMonth: Long,
val expiryYear: Long,
val cardType: String,
val timeCreated: Long,
val timeLastUsed: Long?,
val timeLastModified: Long,
val timesUsed: Long
val timeCreated: Long = 0L,
val timeLastUsed: Long? = 0L,
val timeLastModified: Long = 0L,
val timesUsed: Long = 0L
)

/**
Expand Down Expand Up @@ -221,6 +221,46 @@ data class UpdatableAddressFields(
val email: String
)

/**
* Provides a method for checking whether or not a given credit card can be stored.
*/
interface CreditCardValidationDelegate {

/**
* The result from validating a given [CreditCard] against the credit card storage. This will
* include whether or not it can be created, updated, or neither, along with an explanation
* of any errors.
*/
sealed class Result {
/**
* Indicates that the [CreditCard] does not currently exist in the storage, and a new
* credit card entry can be created.
*/
object CanBeCreated : Result()

/**
* Indicates that a matching [CreditCard] was found in the storage, and the [CreditCard]
* can be used to update its information.
*/
data class CanBeUpdated(val foundCreditCard: CreditCard) : Result()

/**
* The [CreditCard] cannot be saved.
*/
object Error : Result()
}

/**
* Determines whether a [CreditCard] can be added or updated in the credit card storage.
*
* Note that this method is not thread safe.
*
* @param newCreditCard [CreditCard] to be added or updated in the credit card storage.
* @return [Result] that indicates whether or not the [CreditCard] should be saved or updated.
*/
fun validate(newCreditCard: CreditCard): Deferred<Result>
}

/**
* Used to handle [Address] and [CreditCard] storage so that the underlying engine doesn't have to.
* An instance of this should be attached to the Gecko runtime in order to be used.
Expand All @@ -230,22 +270,30 @@ interface CreditCardsAddressesStorageDelegate {
/**
* Returns all stored addresses. This is called when the engine believes an address field
* should be autofilled.
*
* @return A [Deferred] that will resolve to the list of all stored addresses.
*/
fun onAddressesFetch(): Deferred<List<Address>>

/**
* Saves the given address to storage.
*
* @param address [Address] to be saved or updated in the address storage.
*/
fun onAddressSave(address: Address)

/**
* Returns all stored credit cards. This is called when the engine believes a credit card
* field should be autofilled.
*
* @return A [Deferred] that will resolve to the list of all stored credit cards.
*/
fun onCreditCardsFetch(): Deferred<List<CreditCard>>

/**
* Saves the given credit card to storage.
*
* @param creditCard [CreditCard] to be saved or updated in the credit card storage.
*/
fun onCreditCardSave(creditCard: CreditCard)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* 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.service.sync.autofill

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
import mozilla.components.concept.storage.CreditCardsAddressesStorage

/**
* A delegate that will check against the [CreditCardsAddressesStorage] to determine if a given
* [CreditCard] can be persisted and returns information about why it can or cannot.
*/
class DefaultCreditCardValidationDelegate(
private val storage: Lazy<CreditCardsAddressesStorage>,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) : CreditCardValidationDelegate {

override fun validate(newCreditCard: CreditCard): Deferred<Result> {
return scope.async {
val creditCards = storage.value.getAllCreditCards()

val foundCreditCard = if (creditCards.isEmpty()) {
// No credit cards exist in the storage, create a new credit card to the storage.
null
} else {
// Attempt to find a credit card with a matching guid or card number with the
// new credit card. If an existing credit card is found, update it with the new
// credit card entry. Otherwise, create a new credit card entry in the storage.
creditCards.find { it.guid == newCreditCard.guid || it.cardNumber == newCreditCard.cardNumber }
}

if (foundCreditCard == null) Result.CanBeCreated else Result.CanBeUpdated(
foundCreditCard
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.CreditCardsAddressesStorage
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate

Expand Down Expand Up @@ -38,6 +40,22 @@ class GeckoCreditCardsAddressesStorageDelegate(
}

override fun onCreditCardSave(creditCard: CreditCard) {
TODO("Not yet implemented")
val validationDelegate = DefaultCreditCardValidationDelegate(storage)
scope.launch {
when (val result = validationDelegate.validate(creditCard).await()) {
is CreditCardValidationDelegate.Result.CanBeCreated -> {
storage.value.addCreditCard(creditCard.intoUpdatableCreditCardFields())
}
is CreditCardValidationDelegate.Result.CanBeUpdated -> {
storage.value.updateCreditCard(
guid = result.foundCreditCard.guid,
creditCardFields = creditCard.intoUpdatableCreditCardFields()
)
}
is CreditCardValidationDelegate.Result.Error -> {
// Do nothing since an error occurred and the credit card cannot be saved.
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,30 @@ internal fun mozilla.appservices.autofill.CreditCard.into(): CreditCard {
timesUsed = this.timesUsed
)
}

internal fun Address.intoUpdatableAddressFields(): mozilla.components.concept.storage.UpdatableAddressFields {
return mozilla.components.concept.storage.UpdatableAddressFields(
givenName = this.givenName,
additionalName = this.additionalName,
familyName = this.familyName,
organization = this.organization,
streetAddress = this.streetAddress,
addressLevel3 = this.addressLevel3,
addressLevel2 = this.addressLevel2,
addressLevel1 = this.addressLevel1,
postalCode = this.postalCode,
country = this.country,
tel = this.tel,
email = this.email
)
}

internal fun CreditCard.intoUpdatableCreditCardFields(): mozilla.components.concept.storage.UpdatableCreditCardFields {
return mozilla.components.concept.storage.UpdatableCreditCardFields(
billingName = this.billingName,
cardNumber = this.cardNumber,
expiryMonth = this.expiryMonth,
expiryYear = this.expiryYear,
cardType = this.cardType
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* 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.service.sync.autofill

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardValidationDelegate.Result
import mozilla.components.concept.storage.UpdatableCreditCardFields
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class DefaultCreditCardValidationDelegateTest {

private lateinit var validationDelegate: DefaultCreditCardValidationDelegate
private lateinit var scope: TestCoroutineScope

private val storage = AutofillCreditCardsAddressesStorage(testContext)

init {
testContext.getDatabasePath(AUTOFILL_DB_NAME)!!.parentFile!!.mkdirs()
}

@Before
fun before() = runBlocking {
scope = TestCoroutineScope()
validationDelegate =
DefaultCreditCardValidationDelegate(storage = lazy { storage }, scope = scope)
}

@Test
fun `WHEN no credit cards exist in the storage, THEN add the new credit card to storage`() =
runBlocking {
val newCreditCard = createCreditCard(guid = "1")
val result = validationDelegate.validate(newCreditCard).await()

assertEquals(Result.CanBeCreated, result)
}

@Test
fun `WHEN existing credit card matches by guid and card number, THEN update the credit card in storage`() =
runBlocking {
val creditCardFields = UpdatableCreditCardFields(
billingName = "Pineapple Orange",
cardNumber = "4111111111111111",
expiryMonth = 2,
expiryYear = 2028,
cardType = "visa"
)
val creditCard = storage.addCreditCard(creditCardFields)
val result = validationDelegate.validate(creditCard).await()

assertEquals(Result.CanBeUpdated(creditCard), result)
}

@Test
fun `WHEN existing credit card matches by guid only, THEN update the credit card in storage`() =
runBlocking {
val creditCardFields = UpdatableCreditCardFields(
billingName = "Pineapple Orange",
cardNumber = "4111111111111113",
expiryMonth = 2,
expiryYear = 2028,
cardType = "visa"
)
val creditCard = storage.addCreditCard(creditCardFields)
val newCreditCard = createCreditCard(guid = creditCard.guid)
val result = validationDelegate.validate(newCreditCard).await()

assertEquals(Result.CanBeUpdated(creditCard), result)
}

@Test
fun `WHEN existing credit card matches by card number only, THEN update the credit card in storage`() =
runBlocking {
val creditCardFields = UpdatableCreditCardFields(
billingName = "Pineapple Orange",
cardNumber = "4111111111111114",
expiryMonth = 2,
expiryYear = 2028,
cardType = "visa"
)
val creditCard = storage.addCreditCard(creditCardFields)
val newCreditCard = createCreditCard(
guid = creditCard.guid + 1,
billingName = creditCard.billingName,
cardNumber = creditCard.cardNumber,
expiryMonth = creditCard.expiryMonth,
expiryYear = creditCard.expiryYear,
cardType = creditCard.cardType
)
val result = validationDelegate.validate(newCreditCard).await()

assertEquals(Result.CanBeUpdated(creditCard), result)
}

@Test
fun `WHEN existing credit card does not match by guid and card number, THEN add the new credit card to storage`() =
runBlocking {
val newCreditCard = createCreditCard(guid = "2")
val creditCardFields = UpdatableCreditCardFields(
billingName = "Pineapple Orange",
cardNumber = "4111111111111116",
expiryMonth = 2,
expiryYear = 2028,
cardType = "visa"
)
storage.addCreditCard(creditCardFields)

val result = validationDelegate.validate(newCreditCard).await()

assertEquals(Result.CanBeCreated, result)
}
}

fun createCreditCard(
guid: String = "id",
billingName: String = "Banana Apple",
cardNumber: String = "4111111111111110",
expiryMonth: Long = 1,
expiryYear: Long = 2030,
cardType: String = "amex"
) = CreditCard(
guid = guid,
billingName = billingName,
cardNumber = cardNumber,
expiryMonth = expiryMonth,
expiryYear = expiryYear,
cardType = cardType
)
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ permalink: /changelog/

* **service-sync-autofill**
* Refactors `AutofillCreditCardsAddressesStorage` from **browser-storage-sync** into its own component. [#9801](https://github.com/mozilla-mobile/android-components/issues/9801)
* 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)

* **service-firefox-accounts**,**browser-storage-sync**,**service-nimbus**,**service-sync-logins**
* Due to a temporary build issue in the Application Services project, it is not currently
Expand Down

0 comments on commit 7713049

Please sign in to comment.