From 0bfa52032594c77a3eeecd2e0e9c19a921923966 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Thu, 11 Mar 2021 11:46:16 -0500 Subject: [PATCH] Issue #9838: Introduce CreditCardValidationDelegate and implement onCreditCardSave in GeckoCreditCardsAddressesStorageDelegate - Introduces `CreditCardValidationDelegate` and a default implementation in `DefaultCreditCardValidationDelegate` - Implements `onCreditCardSave` in `GeckoCreditCardsAddressesStorageDelegate` - Refactors `CreditCard` from concept-engine to `CreditCardEntry` in concept-storage so that it can validated with the `CreditCardValidationDelegate` --- .../GeckoAutocompleteStorageDelegate.kt | 11 +- .../browser/engine/gecko/ext/CreditCard.kt | 10 +- .../gecko/prompt/GeckoPromptDelegate.kt | 8 +- .../gecko/prompt/GeckoPromptDelegateTest.kt | 6 +- .../concept/engine/prompt/CreditCard.kt | 50 ------- .../concept/engine/prompt/PromptRequest.kt | 7 +- .../concept/engine/prompt/CreditCardTest.kt | 44 ------ .../engine/prompt/PromptRequestTest.kt | 5 +- components/concept/storage/build.gradle | 2 + .../storage/CreditCardsAddressesStorage.kt | 104 ++++++++++++-- .../feature/prompts/PromptFeature.kt | 8 +- .../creditcard/CreditCardItemViewHolder.kt | 12 +- .../prompts/creditcard/CreditCardPicker.kt | 10 +- .../prompts/creditcard/CreditCardSelectBar.kt | 8 +- .../prompts/creditcard/CreditCardsAdapter.kt | 12 +- .../feature/prompts/PromptFeatureTest.kt | 28 ++-- .../CreditCardItemViewHolderTest.kt | 10 +- .../creditcard/CreditCardPickerTest.kt | 6 +- .../creditcard/CreditCardSelectBarTest.kt | 8 +- .../creditcard/CreditCardsAdapterTest.kt | 6 +- components/service/sync-autofill/build.gradle | 1 + .../DefaultCreditCardValidationDelegate.kt | 58 ++++++++ ...eckoCreditCardsAddressesStorageDelegate.kt | 75 +++++++--- ...DefaultCreditCardValidationDelegateTest.kt | 134 ++++++++++++++++++ ...CreditCardsAddressesStorageDelegateTest.kt | 118 ++++++++++++--- .../components/support/ktx/kotlin/String.kt | 10 ++ .../support/ktx/kotlin/StringTest.kt | 10 ++ docs/changelog.md | 4 + 28 files changed, 550 insertions(+), 215 deletions(-) delete mode 100644 components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt delete mode 100644 components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt create mode 100644 components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt create mode 100644 components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt index 66770616917..6f766d00fc8 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt @@ -31,15 +31,17 @@ class GeckoAutocompleteStorageDelegate( private val loginStorageDelegate: LoginStorageDelegate ) : Autocomplete.StorageDelegate { - override fun onCreditCardFetch(): GeckoResult>? { + override fun onCreditCardFetch(): GeckoResult> { val result = GeckoResult>() @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(IO) { - val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch().await() + val key = creditCardsAddressesStorageDelegate.getOrGenerateKey() + + val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch() .mapNotNull { val plaintextCardNumber = - creditCardsAddressesStorageDelegate.decrypt(it.encryptedCardNumber)?.number + creditCardsAddressesStorageDelegate.decrypt(key, it.encryptedCardNumber)?.number if (plaintextCardNumber == null) { null @@ -54,6 +56,7 @@ class GeckoAutocompleteStorageDelegate( } } .toTypedArray() + result.complete(creditCards) } @@ -64,7 +67,7 @@ class GeckoAutocompleteStorageDelegate( loginStorageDelegate.onLoginSave(login.toLoginEntry()) } - override fun onLoginFetch(domain: String): GeckoResult>? { + override fun onLoginFetch(domain: String): GeckoResult> { val result = GeckoResult>() @OptIn(DelicateCoroutinesApi::class) diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt index b3b55760b8f..3706281d2dc 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt @@ -4,14 +4,14 @@ package mozilla.components.browser.engine.gecko.ext -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.support.utils.creditCardIIN import org.mozilla.geckoview.Autocomplete /** - * Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCard]. + * Converts a GeckoView [Autocomplete.CreditCard] to an Android Components [CreditCardEntry]. */ -fun Autocomplete.CreditCard.toCreditCard() = CreditCard( +fun Autocomplete.CreditCard.toCreditCardEntry() = CreditCardEntry( guid = guid, name = name, number = number, @@ -21,9 +21,9 @@ fun Autocomplete.CreditCard.toCreditCard() = CreditCard( ) /** - * Converts an Android Components [CreditCard] to a GeckoView [Autocomplete.CreditCard]. + * Converts an Android Components [CreditCardEntry] to a GeckoView [Autocomplete.CreditCard]. */ -fun CreditCard.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder() +fun CreditCardEntry.toAutocompleteCreditCard() = Autocomplete.CreditCard.Builder() .guid(guid) .name(name) .number(number) 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 877723a49b3..fe065d6389c 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 @@ -9,15 +9,15 @@ 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.toCreditCardEntry 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 import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.support.base.log.logger.Logger @@ -77,7 +77,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe ): GeckoResult? { val geckoResult = GeckoResult() - val onConfirm: (CreditCard) -> Unit = { creditCard -> + val onConfirm: (CreditCardEntry) -> Unit = { creditCard -> if (!request.isComplete) { geckoResult.complete( request.confirm( @@ -94,7 +94,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe geckoEngineSession.notifyObservers { onPromptRequest( PromptRequest.SelectCreditCard( - creditCards = request.options.map { it.value.toCreditCard() }, + creditCards = request.options.map { it.value.toCreditCardEntry() }, onDismiss = onDismiss, onConfirm = onConfirm ) 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 2aef43ede5d..506dbd0cc5f 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 @@ -11,10 +11,10 @@ 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 +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.support.ktx.kotlin.toDate @@ -813,7 +813,7 @@ class GeckoPromptDelegateTest { } }) - val creditCard1 = CreditCard( + val creditCard1 = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111110", @@ -824,7 +824,7 @@ class GeckoPromptDelegateTest { val creditCardSelectOption1 = Autocomplete.CreditCardSelectOption(creditCard1.toAutocompleteCreditCard()) - val creditCard2 = CreditCard( + val creditCard2 = CreditCardEntry( guid = "2", name = "Orange Pineapple", number = "4111111111115555", diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt deleted file mode 100644 index 1b3b650e219..00000000000 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/CreditCard.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* 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.annotation.SuppressLint -import android.os.Parcelable -import kotlinx.parcelize.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. - */ -@SuppressLint("ParcelCreator") -@Parcelize -data class CreditCard( - val guid: String?, - val name: String, - val number: String, - val expiryMonth: String, - val expiryYear: String, - val cardType: String -) : Parcelable { - val obfuscatedCardNumber: String - get() = ellipsesStart + - ellipsis + ellipsis + ellipsis + ellipsis + - number.substring(number.length - digitsToShow) + - ellipsesEnd - - companion object { - // Left-To-Right Embedding (LTE) mark - const val ellipsesStart = "\u202A" - - // One dot ellipsis - const val ellipsis = "\u2022\u2060\u2006\u2060" - - // Pop Directional Formatting (PDF) mark - const val ellipsesEnd = "\u202C" - - // Number of digits to be displayed after ellipses on an obfuscated credit card number. - const val digitsToShow = 4 - } -} 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 9e9635c1574..68d85c512a8 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 @@ -9,6 +9,7 @@ import android.net.Uri import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Level import mozilla.components.concept.engine.prompt.PromptRequest.Authentication.Method import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import java.util.UUID @@ -93,13 +94,13 @@ sealed class PromptRequest( /** * Value type that represents a request for a select credit card prompt. - * @property creditCards a list of [CreditCard]s to select from. + * @property creditCards a list of [CreditCardEntry]s to select from. * @property onConfirm callback that is called when the user confirms the credit card selection. * @property onDismiss callback to let the page know the user dismissed the dialog. */ data class SelectCreditCard( - val creditCards: List, - val onConfirm: (CreditCard) -> Unit, + val creditCards: List, + val onConfirm: (CreditCardEntry) -> Unit, override val onDismiss: () -> Unit ) : PromptRequest(), Dismissible diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt deleted file mode 100644 index 2d2ec2ac4fb..00000000000 --- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/CreditCardTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* 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 last4Digits = "1110" - 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) - assertEquals( - CreditCard.ellipsesStart + - CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis + CreditCard.ellipsis + - last4Digits + - CreditCard.ellipsesEnd, - creditCard.obfuscatedCardNumber - ) - } -} diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt index 72f700878bf..ef0e3a41540 100644 --- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt +++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/prompt/PromptRequestTest.kt @@ -20,6 +20,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection.Type +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.support.test.mock @@ -271,7 +272,7 @@ class PromptRequestTest { @Test fun `GIVEN a list of credit cards WHEN SelectCreditCard is confirmed or dismissed THEN their respective callback is invoked`() { - val creditCard = CreditCard( + val creditCard = CreditCardEntry( guid = "id", name = "Banana Apple", number = "4111111111111110", @@ -281,7 +282,7 @@ class PromptRequestTest { ) var onDismissCalled = false var onConfirmCalled = false - var confirmedCreditCard: CreditCard? = null + var confirmedCreditCard: CreditCardEntry? = null val selectCreditCardRequest = SelectCreditCard( creditCards = listOf(creditCard), diff --git a/components/concept/storage/build.gradle b/components/concept/storage/build.gradle index ecc355a17e4..587ba4a5d1c 100644 --- a/components/concept/storage/build.gradle +++ b/components/concept/storage/build.gradle @@ -27,6 +27,8 @@ dependencies { // dependency, but it will crash at runtime. // Included via 'api' because this module is unusable without coroutines. api Dependencies.kotlin_coroutines + + implementation project(':support-ktx') } apply from: '../../../publish.gradle' diff --git a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt index dd88ed50528..2ea8ad2facd 100644 --- a/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt +++ b/components/concept/storage/src/main/java/mozilla/components/concept/storage/CreditCardsAddressesStorage.kt @@ -6,8 +6,8 @@ package mozilla.components.concept.storage import android.annotation.SuppressLint import android.os.Parcelable -import kotlinx.coroutines.Deferred import kotlinx.parcelize.Parcelize +import mozilla.components.support.ktx.kotlin.last4Digits /** * An interface which defines read/write methods for credit card and address data. @@ -49,6 +49,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 @@ -196,10 +197,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 + @@ -219,6 +220,44 @@ data class CreditCard( } } +/** + * Credit card autofill entry. + * + * This contains the data needed to handle autofill but not the data related to the DB record. + * + * @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. + */ +data class CreditCardEntry( + val guid: String? = null, + val name: String, + val number: String, + val expiryMonth: String, + val expiryYear: String, + val cardType: String +) { + val obfuscatedCardNumber: String + get() = ellipsesStart + + ellipsis + ellipsis + ellipsis + ellipsis + + number.last4Digits() + + ellipsesEnd + + companion object { + // Left-To-Right Embedding (LTE) mark + const val ellipsesStart = "\u202A" + + // One dot ellipsis + const val ellipsis = "\u2022\u2060\u2006\u2060" + + // Pop Directional Formatting (PDF) mark + const val ellipsesEnd = "\u202C" + } +} + /** * Information about a new credit card. * Use this when creating a credit card via [CreditCardsAddressesStorage.addCreditCard]. @@ -332,40 +371,85 @@ 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 or updated. + */ + 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() + } + + /** + * Determines whether a [CreditCardEntry] can be added or updated in the credit card storage. + * + * @param creditCard [CreditCardEntry] to be added or updated in the credit card storage. + * @return [Result] that indicates whether or not the [CreditCardEntry] should be saved or + * updated. + */ + suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): 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. */ -interface CreditCardsAddressesStorageDelegate { +interface CreditCardsAddressesStorageDelegate : KeyProvider { /** * Decrypt a [CreditCardNumber.Encrypted] into its plaintext equivalent or `null` if * it fails to decrypt. * + * @param key The encryption key to decrypt the decrypt credit card number. * @param encryptedCardNumber An encrypted credit card number to be decrypted. * @return A plaintext, non-encrypted credit card number. */ - suspend fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? + suspend fun decrypt( + key: ManagedKey, + encryptedCardNumber: CreditCardNumber.Encrypted + ): CreditCardNumber.Plaintext? /** * 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> + suspend fun onAddressesFetch(): List
/** * 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> + suspend fun onCreditCardsFetch(): List /** * Saves the given credit card to storage. + * + * @param creditCard [CreditCardEntry] to be saved or updated in the credit card storage. */ - fun onCreditCardSave(creditCard: CreditCard) + fun onCreditCardSave(creditCard: CreditCardEntry) } 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 56c0d2e7083..dc9ded4cf06 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 @@ -21,7 +21,6 @@ import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore 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.Alert import mozilla.components.concept.engine.prompt.PromptRequest.Authentication @@ -41,6 +40,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.Share import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt import mozilla.components.concept.engine.prompt.PromptRequest.TimeSelection +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.concept.storage.LoginValidationDelegate @@ -143,7 +143,7 @@ class PromptFeature private constructor( override val loginExceptionStorage: LoginExceptions? = null, private val loginPickerView: SelectablePromptView? = null, private val onManageLogins: () -> Unit = {}, - private val creditCardPickerView: SelectablePromptView? = null, + private val creditCardPickerView: SelectablePromptView? = null, private val onManageCreditCards: () -> Unit = {}, private val onSelectCreditCard: () -> Unit = {}, onNeedToRequestPermissions: OnNeedToRequestPermissions @@ -181,7 +181,7 @@ class PromptFeature private constructor( loginExceptionStorage: LoginExceptions? = null, loginPickerView: SelectablePromptView? = null, onManageLogins: () -> Unit = {}, - creditCardPickerView: SelectablePromptView? = null, + creditCardPickerView: SelectablePromptView? = null, onManageCreditCards: () -> Unit = {}, onSelectCreditCard: () -> Unit = {}, onNeedToRequestPermissions: OnNeedToRequestPermissions @@ -215,7 +215,7 @@ class PromptFeature private constructor( loginExceptionStorage: LoginExceptions? = null, loginPickerView: SelectablePromptView? = null, onManageLogins: () -> Unit = {}, - creditCardPickerView: SelectablePromptView? = null, + creditCardPickerView: SelectablePromptView? = null, onManageCreditCards: () -> Unit = {}, onSelectCreditCard: () -> Unit = {}, onNeedToRequestPermissions: OnNeedToRequestPermissions diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt index 4f2749cd404..e4ffe121c80 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolder.kt @@ -8,7 +8,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.feature.prompts.R import mozilla.components.support.utils.creditCardIssuerNetwork import java.text.SimpleDateFormat @@ -22,15 +22,15 @@ import java.util.Locale */ class CreditCardItemViewHolder( view: View, - private val onCreditCardSelected: (CreditCard) -> Unit + private val onCreditCardSelected: (CreditCardEntry) -> Unit ) : RecyclerView.ViewHolder(view) { /** - * Binds the view with the provided [CreditCard]. + * Binds the view with the provided [CreditCardEntry]. * - * @param creditCard The [CreditCard] to display. + * @param creditCard The [CreditCardEntry] to display. */ - fun bind(creditCard: CreditCard) { + fun bind(creditCard: CreditCardEntry) { itemView.findViewById(R.id.credit_card_logo) .setImageResource(creditCard.cardType.creditCardIssuerNetwork().icon) @@ -47,7 +47,7 @@ class CreditCardItemViewHolder( /** * Set the credit card expiry date formatted according to the locale. */ - private fun bindCreditCardExpiryDate(creditCard: CreditCard) { + private fun bindCreditCardExpiryDate(creditCard: CreditCardEntry) { val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault()) val calendar = Calendar.getInstance() diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt index eace0537467..cc789af4a2f 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardPicker.kt @@ -6,8 +6,8 @@ package mozilla.components.feature.prompts.creditcard import androidx.annotation.VisibleForTesting import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.prompt.CreditCard import mozilla.components.concept.engine.prompt.PromptRequest +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.feature.prompts.concept.SelectablePromptView import mozilla.components.feature.prompts.consumePromptFrom import mozilla.components.feature.prompts.facts.emitCreditCardAutofillDismissedFact @@ -29,11 +29,11 @@ import mozilla.components.support.base.log.logger.Logger */ class CreditCardPicker( private val store: BrowserStore, - private val creditCardSelectBar: SelectablePromptView, + private val creditCardSelectBar: SelectablePromptView, private val manageCreditCardsCallback: () -> Unit = {}, private val selectCreditCardCallback: () -> Unit = {}, private var sessionId: String? = null -) : SelectablePromptView.Listener { +) : SelectablePromptView.Listener { init { creditCardSelectBar.listener = this @@ -41,14 +41,14 @@ class CreditCardPicker( // The selected credit card option to confirm. @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var selectedCreditCard: CreditCard? = null + internal var selectedCreditCard: CreditCardEntry? = null override fun onManageOptions() { manageCreditCardsCallback.invoke() dismissSelectCreditCardRequest() } - override fun onOptionSelect(option: CreditCard) { + override fun onOptionSelect(option: CreditCardEntry) { selectedCreditCard = option creditCardSelectBar.hidePrompt() selectCreditCardCallback.invoke() diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt index 4bb38070835..6cf9a5bffb1 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBar.kt @@ -17,7 +17,7 @@ import androidx.core.widget.ImageViewCompat import androidx.core.widget.TextViewCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.feature.prompts.R import mozilla.components.feature.prompts.concept.SelectablePromptView import mozilla.components.feature.prompts.facts.emitCreditCardAutofillExpandedFact @@ -31,7 +31,7 @@ class CreditCardSelectBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView { +) : ConstraintLayout(context, attrs, defStyleAttr), SelectablePromptView { private var view: View? = null private var recyclerView: RecyclerView? = null @@ -47,7 +47,7 @@ class CreditCardSelectBar @JvmOverloads constructor( } } - override var listener: SelectablePromptView.Listener? = null + override var listener: SelectablePromptView.Listener? = null init { context.withStyledAttributes( @@ -78,7 +78,7 @@ class CreditCardSelectBar @JvmOverloads constructor( toggleSelectCreditCardHeader(shouldExpand = false) } - override fun showPrompt(options: List) { + override fun showPrompt(options: List) { if (view == null) { view = View.inflate(context, LAYOUT_ID, this) bindViews() diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt index 4ea8571e1bf..14f8e06276d 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapter.kt @@ -8,7 +8,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry /** * Adapter for a list of credit cards to be displayed. @@ -16,8 +16,8 @@ import mozilla.components.concept.engine.prompt.CreditCard * @param onCreditCardSelected Callback invoked when a credit card item is selected. */ class CreditCardsAdapter( - private val onCreditCardSelected: (CreditCard) -> Unit -) : ListAdapter(DiffCallback) { + private val onCreditCardSelected: (CreditCardEntry) -> Unit +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CreditCardItemViewHolder { val view = LayoutInflater.from(parent.context) @@ -29,11 +29,11 @@ class CreditCardsAdapter( holder.bind(getItem(position)) } - internal object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: CreditCard, newItem: CreditCard) = + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CreditCardEntry, newItem: CreditCardEntry) = oldItem.guid == newItem.guid - override fun areContentsTheSame(oldItem: CreditCard, newItem: CreditCard) = + override fun areContentsTheSame(oldItem: CreditCardEntry, newItem: CreditCardEntry) = oldItem == newItem } } diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt index 40b7c9f90a0..2e6feeed1c8 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt @@ -31,7 +31,6 @@ import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore 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.Alert import mozilla.components.concept.engine.prompt.PromptRequest.Authentication @@ -43,6 +42,7 @@ import mozilla.components.concept.engine.prompt.PromptRequest.MultipleChoice import mozilla.components.concept.engine.prompt.PromptRequest.SingleChoice import mozilla.components.concept.engine.prompt.PromptRequest.TextPrompt import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.Login import mozilla.components.concept.storage.LoginEntry import mozilla.components.feature.prompts.concept.SelectablePromptView @@ -410,7 +410,7 @@ class PromptFeatureTest { @Test fun `GIVEN creditCardPickerView is visible WHEN dismissSelectPrompts is called THEN dismissSelectCreditCardRequest returns true`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = spy( PromptFeature( mock(), @@ -434,7 +434,7 @@ class PromptFeatureTest { @Test fun `GIVEN creditCardPickerView is not visible WHEN dismissSelectPrompts is called THEN dismissSelectPrompt returns false`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = spy( PromptFeature( mock(), @@ -457,7 +457,7 @@ class PromptFeatureTest { @Test fun `GIVEN an active select credit card request WHEN onBackPressed is called THEN dismissSelectPrompts is called`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = spy( PromptFeature( mock(), @@ -481,7 +481,7 @@ class PromptFeatureTest { @Test fun `WHEN dismissSelectPrompts is called THEN the active credit card picker should be dismissed`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = spy( PromptFeature( mock(), @@ -817,7 +817,7 @@ class PromptFeatureTest { @Test fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_OK THEN onAuthSuccess) is called`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = PromptFeature( activity = mock(), @@ -836,7 +836,7 @@ class PromptFeatureTest { @Test fun `WHEN onActivityResult is called with PIN_REQUEST and RESULT_CANCELED THEN onAuthFailure is called`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = PromptFeature( activity = mock(), @@ -855,7 +855,7 @@ class PromptFeatureTest { @Test fun `GIVEN user successfully authenticates by biometric prompt WHEN onBiometricResult is called THEN onAuthSuccess is called`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = PromptFeature( activity = mock(), @@ -873,7 +873,7 @@ class PromptFeatureTest { @Test fun `GIVEN user fails to authenticate by biometric prompt WHEN onBiometricResult is called THEN onAuthFailure) is called`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = PromptFeature( activity = mock(), @@ -925,7 +925,7 @@ class PromptFeatureTest { @Test fun `WHEN a credit card is selected THEN confirm the prompt request with the selected credit card`() { - val creditCard = CreditCard( + val creditCard = CreditCardEntry( guid = "id", name = "Banana Apple", number = "4111111111111110", @@ -935,7 +935,7 @@ class PromptFeatureTest { ) var onDismissCalled = false var onConfirmCalled = false - var confirmedCreditCard: CreditCard? = null + var confirmedCreditCard: CreditCardEntry? = null val selectCreditCardRequest = PromptRequest.SelectCreditCard( creditCards = listOf(creditCard), @@ -1393,7 +1393,7 @@ class PromptFeatureTest { @Test fun `WHEN page is refreshed THEN credit card prompt is dismissed`() { - val creditCardPickerView: SelectablePromptView = mock() + val creditCardPickerView: SelectablePromptView = mock() val feature = PromptFeature( activity = mock(), @@ -1404,8 +1404,8 @@ class PromptFeatureTest { ) { } feature.creditCardPicker = creditCardPicker val onDismiss: () -> Unit = {} - val onConfirm: (CreditCard) -> Unit = {} - val creditCard = CreditCard( + val onConfirm: (CreditCardEntry) -> Unit = {} + val creditCard = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111110", diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt index 0cb08d848e0..aecf8fe4775 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardItemViewHolderTest.kt @@ -9,7 +9,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.feature.prompts.R import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext @@ -27,9 +27,9 @@ class CreditCardItemViewHolderTest { private lateinit var cardLogoView: ImageView private lateinit var cardNumberView: TextView private lateinit var expirationDateView: TextView - private lateinit var onCreditCardSelected: (CreditCard) -> Unit + private lateinit var onCreditCardSelected: (CreditCardEntry) -> Unit - private val creditCard = CreditCard( + private val creditCard = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111111", @@ -58,8 +58,8 @@ class CreditCardItemViewHolderTest { @Test fun `GIVEN a credit card item WHEN a credit item is clicked THEN onCreditCardSelected is called with the given credit card item`() { - var onCreditCardSelectedCalled: CreditCard? = null - val onCreditCardSelected = { creditCard: CreditCard -> + var onCreditCardSelectedCalled: CreditCardEntry? = null + val onCreditCardSelected = { creditCard: CreditCardEntry -> onCreditCardSelectedCalled = creditCard } CreditCardItemViewHolder(view, onCreditCardSelected).bind(creditCard) diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt index 3f7d07e4741..242661c0bbb 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardPickerTest.kt @@ -10,8 +10,8 @@ import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.prompt.CreditCard import mozilla.components.concept.engine.prompt.PromptRequest +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.support.test.mock import mozilla.components.support.test.whenever import org.junit.Assert.assertEquals @@ -30,7 +30,7 @@ class CreditCardPickerTest { private lateinit var creditCardPicker: CreditCardPicker private lateinit var creditCardSelectBar: CreditCardSelectBar - private val creditCard = CreditCard( + private val creditCard = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111110", @@ -39,7 +39,7 @@ class CreditCardPickerTest { cardType = "" ) var onDismissCalled = false - var confirmedCreditCard: CreditCard? = null + var confirmedCreditCard: CreditCardEntry? = null private val promptRequest = PromptRequest.SelectCreditCard( creditCards = listOf(creditCard), onDismiss = { onDismissCalled = true }, diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt index deb09d182cc..ed695b2285a 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardSelectBarTest.kt @@ -10,7 +10,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.runBlocking -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.feature.prompts.R import mozilla.components.feature.prompts.concept.SelectablePromptView import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts @@ -36,7 +36,7 @@ class CreditCardSelectBarTest { private lateinit var creditCardSelectBar: CreditCardSelectBar - private val creditCard = CreditCard( + private val creditCard = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111110", @@ -68,7 +68,7 @@ class CreditCardSelectBarTest { @Test fun `GIVEN a listener WHEN manage credit cards button is clicked THEN onManageOptions is called`() { - val listener: SelectablePromptView.Listener = mock() + val listener: SelectablePromptView.Listener = mock() assertNull(creditCardSelectBar.listener) @@ -82,7 +82,7 @@ class CreditCardSelectBarTest { @Test fun `GIVEN a listener WHEN a credit card is selected THEN onOptionSelect is called`() = runBlocking { - val listener: SelectablePromptView.Listener = mock() + val listener: SelectablePromptView.Listener = mock() creditCardSelectBar.listener = listener val facts = mutableListOf() diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt index 414a0ec3fa1..6fbfc06a23e 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/creditcard/CreditCardsAdapterTest.kt @@ -1,7 +1,7 @@ package mozilla.components.feature.prompts.creditcard -import mozilla.components.concept.engine.prompt.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -10,7 +10,7 @@ class CreditCardsAdapterTest { @Test fun testDiffCallback() { - val creditCard1 = CreditCard( + val creditCard1 = CreditCardEntry( guid = "1", name = "Banana Apple", number = "4111111111111110", @@ -27,7 +27,7 @@ class CreditCardsAdapterTest { CreditCardsAdapter.DiffCallback.areContentsTheSame(creditCard1, creditCard2) ) - val creditCard3 = CreditCard( + val creditCard3 = CreditCardEntry( guid = "2", name = "Pineapple Orange", number = "4111111111115555", diff --git a/components/service/sync-autofill/build.gradle b/components/service/sync-autofill/build.gradle index 87bb3bbf66a..db95a8a066a 100644 --- a/components/service/sync-autofill/build.gradle +++ b/components/service/sync-autofill/build.gradle @@ -32,6 +32,7 @@ dependencies { api project(':lib-dataprotect') implementation project(':support-utils') + implementation project(':support-ktx') implementation Dependencies.kotlin_stdlib diff --git a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt new file mode 100644 index 00000000000..92a18758f03 --- /dev/null +++ b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegate.kt @@ -0,0 +1,58 @@ +/* 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.CreditCardEntry +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 +) : CreditCardValidationDelegate { + + private val coroutineContext by lazy { Dispatchers.IO } + + override suspend fun shouldCreateOrUpdate(creditCard: CreditCardEntry): 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 + null + } else { + val crypto = storage.value.getCreditCardCrypto() + val key = crypto.getOrGenerateKey() + + // Found a matching guid and credit card number -> update + creditCards.find { + it.guid == creditCard.guid && + crypto.decrypt(key, it.encryptedCardNumber)?.number == creditCard.number + } + // Found a matching guid -> update + ?: creditCards.find { it.guid == creditCard.guid } + // Found a matching credit card number -> update + ?: creditCards.find { crypto.decrypt(key, it.encryptedCardNumber)?.number == creditCard.number } + // Found a non-matching guid and blank credit card number -> update + ?: creditCards.find { crypto.decrypt(key, it.encryptedCardNumber)?.number?.isEmpty() ?: false } + // Else create a new credit card + } + + if (foundCreditCard == null) { + Result.CanBeCreated + } else { + Result.CanBeUpdated(foundCreditCard) + } + } +} diff --git a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt index 1f6f52b5dd0..c8b60b4db93 100644 --- a/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt +++ b/components/service/sync-autofill/src/main/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegate.kt @@ -4,16 +4,21 @@ 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.CreditCardEntry import mozilla.components.concept.storage.CreditCardNumber +import mozilla.components.concept.storage.CreditCardValidationDelegate import mozilla.components.concept.storage.CreditCardsAddressesStorage import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate +import mozilla.components.concept.storage.ManagedKey +import mozilla.components.concept.storage.NewCreditCardFields +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.support.ktx.kotlin.last4Digits /** * [CreditCardsAddressesStorageDelegate] implementation. @@ -28,33 +33,67 @@ class GeckoCreditCardsAddressesStorageDelegate( private val isCreditCardAutofillEnabled: () -> Boolean = { false } ) : CreditCardsAddressesStorageDelegate { - override suspend fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? { + override suspend fun getOrGenerateKey(): ManagedKey { + val crypto = storage.value.getCreditCardCrypto() + return crypto.getOrGenerateKey() + } + + override suspend fun decrypt( + key: ManagedKey, + encryptedCardNumber: CreditCardNumber.Encrypted + ): CreditCardNumber.Plaintext? { val crypto = storage.value.getCreditCardCrypto() - val key = crypto.getOrGenerateKey() return crypto.decrypt(key, encryptedCardNumber) } - override fun onAddressesFetch(): Deferred> { - return scope.async { - storage.value.getAllAddresses() - } + override suspend fun onAddressesFetch(): List
= withContext(scope.coroutineContext) { + storage.value.getAllAddresses() } override fun onAddressSave(address: Address) { TODO("Not yet implemented") } - override fun onCreditCardsFetch(): Deferred> { - if (isCreditCardAutofillEnabled().not()) { - return CompletableDeferred(listOf()) + override suspend fun onCreditCardsFetch(): List = + withContext(scope.coroutineContext) { + if (!isCreditCardAutofillEnabled()) { + emptyList() + } else { + storage.value.getAllCreditCards() + } } - return scope.async { - storage.value.getAllCreditCards() - } - } + override fun onCreditCardSave(creditCard: CreditCardEntry) { + val validationDelegate = DefaultCreditCardValidationDelegate(storage) - override fun onCreditCardSave(creditCard: CreditCard) { - TODO("Not yet implemented") + scope.launch { + when (val result = validationDelegate.shouldCreateOrUpdate(creditCard)) { + is CreditCardValidationDelegate.Result.CanBeCreated -> { + storage.value.addCreditCard( + NewCreditCardFields( + billingName = creditCard.name, + plaintextCardNumber = CreditCardNumber.Plaintext(creditCard.number), + cardNumberLast4 = creditCard.number.last4Digits(), + expiryMonth = creditCard.expiryMonth.toLong(), + expiryYear = creditCard.expiryYear.toLong(), + cardType = creditCard.cardType + ) + ) + } + is CreditCardValidationDelegate.Result.CanBeUpdated -> { + storage.value.updateCreditCard( + guid = result.foundCreditCard.guid, + creditCardFields = UpdatableCreditCardFields( + billingName = creditCard.name, + cardNumber = CreditCardNumber.Plaintext(creditCard.number), + cardNumberLast4 = creditCard.number.last4Digits(), + expiryMonth = creditCard.expiryMonth.toLong(), + expiryYear = creditCard.expiryYear.toLong(), + cardType = creditCard.cardType + ) + ) + } + } + } } } diff --git a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt new file mode 100644 index 00000000000..40a4311e4e8 --- /dev/null +++ b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/DefaultCreditCardValidationDelegateTest.kt @@ -0,0 +1,134 @@ +/* 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 mozilla.components.concept.storage.CreditCardEntry +import mozilla.components.concept.storage.CreditCardNumber +import mozilla.components.concept.storage.CreditCardValidationDelegate.Result +import mozilla.components.concept.storage.NewCreditCardFields +import mozilla.components.lib.dataprotect.SecureAbove22Preferences +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 storage: AutofillCreditCardsAddressesStorage + private lateinit var securePrefs: SecureAbove22Preferences + private lateinit var validationDelegate: DefaultCreditCardValidationDelegate + + @Before + fun before() = runBlocking { + // forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment. + securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true) + storage = AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs }) + validationDelegate = DefaultCreditCardValidationDelegate(storage = lazy { storage }) + } + + @Test + fun `WHEN no credit cards exist in the storage, THEN add the new credit card to storage`() = + runBlocking { + val newCreditCard = createCreditCardEntry(guid = "1") + val result = validationDelegate.shouldCreateOrUpdate(newCreditCard) + + 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 = NewCreditCardFields( + billingName = "Pineapple Orange", + plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"), + cardNumberLast4 = "1111", + expiryMonth = 12, + expiryYear = 2028, + cardType = "visa" + ) + val creditCard = storage.addCreditCard(creditCardFields) + val newCreditCard = createCreditCardEntry(guid = creditCard.guid) + val result = validationDelegate.shouldCreateOrUpdate(newCreditCard) + + 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 = NewCreditCardFields( + billingName = "Pineapple Orange", + plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"), + cardNumberLast4 = "1111", + expiryMonth = 12, + expiryYear = 2028, + cardType = "visa" + ) + val creditCard = storage.addCreditCard(creditCardFields) + val newCreditCard = createCreditCardEntry(guid = creditCard.guid) + val result = validationDelegate.shouldCreateOrUpdate(newCreditCard) + + 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 = NewCreditCardFields( + billingName = "Pineapple Orange", + plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"), + cardNumberLast4 = "1111", + expiryMonth = 12, + expiryYear = 2028, + cardType = "visa" + ) + val creditCard = storage.addCreditCard(creditCardFields) + val newCreditCard = createCreditCardEntry(cardNumber = "4111111111111111") + val result = validationDelegate.shouldCreateOrUpdate(newCreditCard) + + 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 = createCreditCardEntry(guid = "2") + val creditCardFields = NewCreditCardFields( + billingName = "Pineapple Orange", + plaintextCardNumber = CreditCardNumber.Plaintext("4111111111111111"), + cardNumberLast4 = "1111", + expiryMonth = 12, + expiryYear = 2028, + cardType = "visa" + ) + storage.addCreditCard(creditCardFields) + + val result = validationDelegate.shouldCreateOrUpdate(newCreditCard) + + assertEquals(Result.CanBeCreated, result) + } +} + +fun createCreditCardEntry( + guid: String = "id", + billingName: String = "Banana Apple", + cardNumber: String = "4111111111111110", + expiryMonth: String = "1", + expiryYear: String = "2030", + cardType: String = "amex" +) = CreditCardEntry( + guid = guid, + name = billingName, + number = cardNumber, + expiryMonth = expiryMonth, + expiryYear = expiryYear, + cardType = cardType +) diff --git a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt index 4dd998ce0eb..4ebba7c9185 100644 --- a/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt +++ b/components/service/sync-autofill/src/test/java/mozilla/components/service/sync/autofill/GeckoCreditCardsAddressesStorageDelegateTest.kt @@ -10,9 +10,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import mozilla.components.concept.storage.CreditCard +import mozilla.components.concept.storage.CreditCardEntry import mozilla.components.concept.storage.CreditCardNumber import mozilla.components.concept.storage.NewCreditCardFields +import mozilla.components.concept.storage.UpdatableCreditCardFields import mozilla.components.lib.dataprotect.SecureAbove22Preferences +import mozilla.components.support.ktx.kotlin.last4Digits import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals @@ -46,26 +49,28 @@ class GeckoCreditCardsAddressesStorageDelegateTest { } @Test - fun `decrypt`() = runBlocking { - val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111") - val creditCardFields = NewCreditCardFields( - billingName = "Jon Doe", - plaintextCardNumber = plaintextNumber, - cardNumberLast4 = "1111", - expiryMonth = 12, - expiryYear = 2028, - cardType = "amex" - ) - val creditCard = storage.addCreditCard(creditCardFields) - - assertEquals( - plaintextNumber, - delegate.decrypt(creditCard.encryptedCardNumber) - ) - } + fun `GIVEN a newly added credit card WHEN decrypt is called THEN it returns the plain credit card number`() = + runBlocking { + val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111") + val creditCardFields = NewCreditCardFields( + billingName = "Jon Doe", + plaintextCardNumber = plaintextNumber, + cardNumberLast4 = "1111", + expiryMonth = 12, + expiryYear = 2028, + cardType = "amex" + ) + val creditCard = storage.addCreditCard(creditCardFields) + val key = delegate.getOrGenerateKey() + + assertEquals( + plaintextNumber, + delegate.decrypt(key, creditCard.encryptedCardNumber) + ) + } @Test - fun `onAddressFetch`() { + fun `WHEN onAddressFetch is called THEN the storage is called to gett all addresses`() { scope.launch { delegate.onAddressesFetch() verify(storage, times(1)).getAllAddresses() @@ -101,4 +106,81 @@ class GeckoCreditCardsAddressesStorageDelegateTest { assertEquals(emptyList(), result) } } + + @Test + fun `GIVEN a new credit card WHEN onCreditCardSave is called THEN it adds a new credit card in storage`() { + scope.launch { + val billingName = "Jon Doe" + val cardNumber = "4111111111111111" + val expiryMonth = 12L + val expiryYear = 2028L + val cardType = "amex" + + delegate.onCreditCardSave( + CreditCardEntry( + name = billingName, + number = cardNumber, + expiryMonth = expiryMonth.toString(), + expiryYear = expiryYear.toString(), + cardType = cardType + ) + ) + + verify(storage, times(1)).addCreditCard( + creditCardFields = NewCreditCardFields( + billingName = billingName, + plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber), + cardNumberLast4 = cardNumber.last4Digits(), + expiryMonth = expiryMonth, + expiryYear = expiryYear, + cardType = cardType + ) + ) + } + } + + @Test + fun `GIVEN an existing credit card WHEN onCreditCardSave is called THEN it updates the existing credit card in storage`() { + scope.launch { + val billingName = "Jon Doe" + val cardNumber = "4111111111111111" + val expiryMonth = 12L + val expiryYear = 2028L + val cardType = "amex" + + val creditCard = storage.addCreditCard( + NewCreditCardFields( + billingName = "Jon Doe", + plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber), + cardNumberLast4 = "1111", + expiryMonth = expiryMonth, + expiryYear = expiryYear, + cardType = cardType + ) + ) + + delegate.onCreditCardSave( + CreditCardEntry( + guid = creditCard.guid, + name = billingName, + number = "4111111111111112", + expiryMonth = expiryMonth.toString(), + expiryYear = expiryYear.toString(), + cardType = cardType + ) + ) + + verify(storage, times(1)).updateCreditCard( + guid = creditCard.guid, + creditCardFields = UpdatableCreditCardFields( + billingName = billingName, + cardNumber = CreditCardNumber.Plaintext("4111111111111112"), + cardNumberLast4 = "4111111111111112".last4Digits(), + expiryMonth = expiryMonth, + expiryYear = expiryYear, + cardType = cardType + ) + ) + } + } } diff --git a/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt b/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt index 0e9a5058248..ff3ccdad43a 100644 --- a/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt +++ b/components/support/ktx/src/main/java/mozilla/components/support/ktx/kotlin/String.kt @@ -29,6 +29,9 @@ private val re = object { private const val MAILTO = "mailto:" +// Number of last digits to be shown when credit card number is obfuscated. +private const val LAST_VISIBLE_DIGITS_COUNT = 4 + /** * Checks if this String is a URL. */ @@ -280,3 +283,10 @@ fun String.getRepresentativeCharacter(): String { return "?" } + +/** + * Returns the last 4 digits from a formatted credit card number string. + */ +fun String.last4Digits(): String { + return this.takeLast(LAST_VISIBLE_DIGITS_COUNT) +} diff --git a/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt b/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt index 41decc904a8..1e6b59fec89 100644 --- a/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt +++ b/components/support/ktx/src/test/java/mozilla/components/support/ktx/kotlin/StringTest.kt @@ -278,4 +278,14 @@ class StringTest { // IP assertEquals("1", "https://192.168.0.1".getRepresentativeCharacter()) } + + @Test + fun `last4Digits returns a string with only last 4 digits `() { + assertEquals("8431", "371449635398431".last4Digits()) + assertEquals("2345", "12345".last4Digits()) + assertEquals("1234", "1234".last4Digits()) + assertEquals("123", "123".last4Digits()) + assertEquals("1", "1".last4Digits()) + assertEquals("", "".last4Digits()) + } } diff --git a/docs/changelog.md b/docs/changelog.md index f677e4557ca..9938833cf3c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -20,6 +20,10 @@ permalink: /changelog/ * **feature-session** * 🆕 New `ScreenOrientationFeature` to enable support for setting a requested screen orientation as part of supporting the ScreenOrientation web APIs. +* **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-sync** * 🌟️️ Add `onReady` method to `AccountObserver`, allowing consumers to know when they can start querying account state.