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

Commit

Permalink
Issue #9838: Introduce CreditCardValidationDelegate and implement onC…
Browse files Browse the repository at this point in the history
…reditCardSave in GeckoCreditCardsAddressesStorageDelegate

- Introduces `CreditCardValidationDelegate` and a default implementation in `DefaultCreditCardValidationDelegate`
- Implements `onCreditCardSave` in `GeckoCreditCardsAddressesStorageDelegate`
  • Loading branch information
gabrielluong committed Nov 22, 2021
1 parent 713ee45 commit 415b0f4
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package mozilla.components.concept.storage

import android.annotation.SuppressLint
import android.os.Parcelable
import kotlinx.coroutines.Deferred
import kotlinx.parcelize.Parcelize

/**
Expand Down Expand Up @@ -49,6 +48,7 @@ interface CreditCardsAddressesStorage {
/**
* Deletes the credit card with the given [guid].
*
* @param guid Unique identifier for the desired credit card.
* @return True if the deletion did anything, false otherwise.
*/
suspend fun deleteCreditCard(guid: String): Boolean
Expand Down Expand Up @@ -191,10 +191,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
) : Parcelable {
val obfuscatedCardNumber: String
get() = ellipsesStart +
Expand Down Expand Up @@ -327,6 +327,44 @@ 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 or updated.
*/
object Error : Result()
}

/**
* Determines whether a [CreditCard] can be added or updated in the credit card storage.
*
* @param creditCard [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.
*/
suspend fun validate(creditCard: CreditCard): 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 @@ -345,22 +383,30 @@ interface CreditCardsAddressesStorageDelegate {
/**
* Returns all stored addresses. This is called when the engine believes an address field
* should be autofilled.
*
* @return A list of all stored addresses.
*/
fun onAddressesFetch(): Deferred<List<Address>>
suspend fun onAddressesFetch(): 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 list of all stored credit cards.
*/
fun onCreditCardsFetch(): Deferred<List<CreditCard>>
suspend fun onCreditCardsFetch(): 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,68 @@
/* 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.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
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.
*
* @param storage An instance of [CreditCardsAddressesStorage].
*/
class DefaultCreditCardValidationDelegate(
private val storage: Lazy<CreditCardsAddressesStorage>
) : CreditCardValidationDelegate {

private val coroutineContext by lazy { Dispatchers.IO }

override suspend fun validate(creditCard: CreditCard): Result =
withContext(coroutineContext) {
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 {
// Found a matching guid and credit card number -> update
creditCards.find {
it.guid == creditCard.guid &&
decrypt(it.encryptedCardNumber) == decrypt(creditCard.encryptedCardNumber)
}
// Found a matching guid -> update
?: creditCards.find { it.guid == creditCard.guid }
// Found a matching credit card number -> update
?: creditCards.find { decrypt(it.encryptedCardNumber) == decrypt(creditCard.encryptedCardNumber) }
// Found a non-matching guid and blank credit card number -> update
?: creditCards.find { decrypt(it.encryptedCardNumber)!!.number.isEmpty() }
// Else create a new credit card
}

if (foundCreditCard == null) {
Result.CanBeCreated
} else {
Result.CanBeUpdated(foundCreditCard)
}
}

/**
* Helper function to decrypt a [CreditCardNumber.Encrypted] into its plaintext equivalent or
* `null` if it fails to decrypt.
*
* @param encryptedCardNumber An encrypted credit card number to be decrypted.
* @return A plaintext, non-encrypted credit card number.
*/
private fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? {
val crypto = storage.value.getCreditCardCrypto()
val key = crypto.key()
return crypto.decrypt(key, encryptedCardNumber)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

package mozilla.components.service.sync.autofill

import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.CreditCardValidationDelegate
import mozilla.components.concept.storage.CreditCardsAddressesStorage
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate

Expand All @@ -34,27 +34,43 @@ class GeckoCreditCardsAddressesStorageDelegate(
return crypto.decrypt(key, encryptedCardNumber)
}

override fun onAddressesFetch(): Deferred<List<Address>> {
return scope.async {
storage.value.getAllAddresses()
}
override suspend fun onAddressesFetch(): List<Address> = withContext(scope.coroutineContext) {
storage.value.getAllAddresses()
}

override fun onAddressSave(address: Address) {
TODO("Not yet implemented")
}

override fun onCreditCardsFetch(): Deferred<List<CreditCard>> {
if (isCreditCardAutofillEnabled().not()) {
return CompletableDeferred(listOf())
}

return scope.async {
override suspend fun onCreditCardsFetch(): List<CreditCard> =
withContext(scope.coroutineContext) {
storage.value.getAllCreditCards()
}
}

override fun onCreditCardSave(creditCard: CreditCard) {
TODO("Not yet implemented")
val validationDelegate = DefaultCreditCardValidationDelegate(storage)

scope.launch {
when (val result = validationDelegate.validate(creditCard)) {
is CreditCardValidationDelegate.Result.CanBeCreated -> {
decrypt(creditCard.encryptedCardNumber)?.let { plaintextCardNumber ->
storage.value.addCreditCard(
creditCard.intoNewCreditCardFields(
plaintextCardNumber
)
)
}
}
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 @@ -7,6 +7,7 @@ package mozilla.components.service.sync.autofill
import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.NewCreditCardFields
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.concept.storage.UpdatableCreditCardFields

Expand Down Expand Up @@ -94,3 +95,33 @@ internal fun mozilla.appservices.autofill.CreditCard.into(): CreditCard {
timesUsed = this.timesUsed
)
}

/**
* Conversion from [CreditCard] to [NewCreditCardFields].
*
* @param plaintextCardNumber A plaintext credit card number.
*/
internal fun CreditCard.intoNewCreditCardFields(plaintextCardNumber: CreditCardNumber.Plaintext): NewCreditCardFields {
return NewCreditCardFields(
billingName = this.billingName,
plaintextCardNumber = plaintextCardNumber,
cardNumberLast4 = this.cardNumberLast4,
expiryMonth = this.expiryMonth,
expiryYear = this.expiryYear,
cardType = this.cardType
)
}

/**
* Conversion from [CreditCard] to [UpdatableCreditCardFields].
*/
internal fun CreditCard.intoUpdatableCreditCardFields(): UpdatableCreditCardFields {
return UpdatableCreditCardFields(
billingName = this.billingName,
cardNumber = this.encryptedCardNumber,
cardNumberLast4 = this.cardNumberLast4,
expiryMonth = this.expiryMonth,
expiryYear = this.expiryYear,
cardType = this.cardType
)
}
Loading

1 comment on commit 415b0f4

@firefoxci-taskcluster
Copy link

Choose a reason for hiding this comment

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

Uh oh! Looks like an error! Details

Failed to fetch task artifact public/github/customCheckRunText.md for GitHub integration.
Make sure the artifact exists on the worker or other location.

Please sign in to comment.