Skip to content

Commit

Permalink
Issue mozilla-mobile#11338: Add a SaveCreditCard dialog to handle sav…
Browse files Browse the repository at this point in the history
…ing and updating a credit card
  • Loading branch information
gabrielluong committed May 9, 2022
1 parent 498b53c commit a2cbcac
Show file tree
Hide file tree
Showing 13 changed files with 817 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.browser.engine.gecko.ext.toCreditCardEntry
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
Expand Down Expand Up @@ -63,6 +64,13 @@ class GeckoAutocompleteStorageDelegate(
return result
}

override fun onCreditCardSave(creditCard: Autocomplete.CreditCard) {
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(IO) {
creditCardsAddressesStorageDelegate.onCreditCardSave(creditCard.toCreditCardEntry())
}
}

override fun onLoginSave(login: Autocomplete.LoginEntry) {
loginStorageDelegate.onLoginSave(login.toLoginEntry())
}
Expand Down
4 changes: 4 additions & 0 deletions components/concept/storage/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ dependencies {
api Dependencies.kotlin_coroutines

implementation project(':support-ktx')

testImplementation project(':support-test')
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_mockito
}

apply from: '../../../publish.gradle'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
import mozilla.components.support.ktx.kotlin.last4Digits
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale

/**
* An interface which defines read/write methods for credit card and address data.
Expand Down Expand Up @@ -235,19 +238,41 @@ data class CreditCard(
* @property expiryYear The credit card expiry year.
* @property cardType The credit card network ID.
*/
@Parcelize
data class CreditCardEntry(
val guid: String? = null,
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.last4Digits() +
ellipsesEnd

/**
* Credit card expiry date formatted according to the locale.
*/
val expiryDate: String
get() {
val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())

val calendar = Calendar.getInstance()
calendar.set(Calendar.DAY_OF_MONTH, 1)
// Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed.
calendar.set(Calendar.MONTH, expiryMonth.toInt() - 1)
calendar.set(Calendar.YEAR, expiryYear.toInt())

return dateFormat.format(calendar.time)
}

companion object {
// Date format pattern for the credit card expiry date.
private const val DATE_PATTERN = "MM/yyyy"
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* 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.storage

import mozilla.components.concept.storage.CreditCard.Companion.ellipsesEnd
import mozilla.components.concept.storage.CreditCard.Companion.ellipsesStart
import mozilla.components.concept.storage.CreditCard.Companion.ellipsis
import mozilla.components.support.ktx.kotlin.last4Digits
import org.junit.Assert.assertEquals
import org.junit.Test

class CreditCardEntryTest {

private val creditCard = CreditCardEntry(
guid = "1",
name = "Banana Apple",
number = "4111111111111110",
expiryMonth = "5",
expiryYear = "2030",
cardType = "amex"
)

@Test
fun `WHEN obfuscatedCardNumber getter is called THEN the expected obfuscated card number is returned`() {
assertEquals(
ellipsesStart +
ellipsis + ellipsis + ellipsis + ellipsis +
creditCard.number.last4Digits() +
ellipsesEnd,
creditCard.obfuscatedCardNumber
)
}

@Test
fun `WHEN expiryDdate getter is called THEN the expected expiry date string is returned`() {
assertEquals("0${creditCard.expiryMonth}/${creditCard.expiryYear}", creditCard.expiryDate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ 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.CreditCardValidationDelegate
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.concept.storage.LoginValidationDelegate
import mozilla.components.feature.prompts.concept.SelectablePromptView
import mozilla.components.feature.prompts.creditcard.CreditCardPicker
import mozilla.components.feature.prompts.creditcard.CreditCardSaveDialogFragment
import mozilla.components.feature.prompts.dialog.AlertDialogFragment
import mozilla.components.feature.prompts.dialog.AuthenticationDialogFragment
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment
Expand Down Expand Up @@ -138,6 +140,7 @@ class PromptFeature private constructor(
private var customTabId: String?,
private val fragmentManager: FragmentManager,
private val shareDelegate: ShareDelegate,
override val creditCardValidationDelegate: CreditCardValidationDelegate? = null,
override val loginValidationDelegate: LoginValidationDelegate? = null,
private val isSaveLoginEnabled: () -> Boolean = { false },
private val isCreditCardAutofillEnabled: () -> Boolean = { false },
Expand Down Expand Up @@ -176,6 +179,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
isCreditCardAutofillEnabled: () -> Boolean = { false },
Expand All @@ -192,6 +196,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
Expand All @@ -210,6 +215,7 @@ class PromptFeature private constructor(
customTabId: String? = null,
fragmentManager: FragmentManager,
shareDelegate: ShareDelegate = DefaultShareDelegate(),
creditCardValidationDelegate: CreditCardValidationDelegate? = null,
loginValidationDelegate: LoginValidationDelegate? = null,
isSaveLoginEnabled: () -> Boolean = { false },
isCreditCardAutofillEnabled: () -> Boolean = { false },
Expand All @@ -226,6 +232,7 @@ class PromptFeature private constructor(
customTabId = customTabId,
fragmentManager = fragmentManager,
shareDelegate = shareDelegate,
creditCardValidationDelegate = creditCardValidationDelegate,
loginValidationDelegate = loginValidationDelegate,
isSaveLoginEnabled = isSaveLoginEnabled,
isCreditCardAutofillEnabled = isCreditCardAutofillEnabled,
Expand Down Expand Up @@ -281,6 +288,8 @@ class PromptFeature private constructor(
loginPicker?.dismissCurrentLoginSelect(activePromptRequest as SelectLoginPrompt)
} else if (activePromptRequest is SaveLoginPrompt) {
(activePrompt?.get() as? SaveLoginDialogFragment)?.dismissAllowingStateLoss()
} else if (activePromptRequest is SaveCreditCard) {
(activePrompt?.get() as? CreditCardSaveDialogFragment)?.dismissAllowingStateLoss()
} else if (activePromptRequest is SelectCreditCard) {
creditCardPicker?.dismissSelectCreditCardRequest(
activePromptRequest as SelectCreditCard
Expand Down Expand Up @@ -489,6 +498,7 @@ class PromptFeature private constructor(

is Share -> it.onSuccess()

is SaveCreditCard -> it.onConfirm(value as CreditCardEntry)
is SaveLoginPrompt -> it.onConfirm(value as LoginEntry)

is Confirm -> {
Expand Down Expand Up @@ -556,6 +566,9 @@ class PromptFeature private constructor(
)
}

/**
* Called from on [onPromptRequested] to handle requests for showing native dialogs.
*/
@Suppress("ComplexMethod", "LongMethod")
@VisibleForTesting(otherwise = PRIVATE)
internal fun handleDialogsRequest(
Expand All @@ -564,6 +577,16 @@ class PromptFeature private constructor(
) {
// Requests that are handled with dialogs
val dialog = when (promptRequest) {
is SaveCreditCard -> {
if (!isCreditCardAutofillEnabled.invoke()) return

CreditCardSaveDialogFragment.newInstance(
sessionId = session.id,
promptRequestUID = promptRequest.uid,
shouldDismissOnLoad = false,
creditCard = promptRequest.creditCard
)
}

is SaveLoginPrompt -> {
if (!isSaveLoginEnabled.invoke()) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.storage.CreditCardEntry
import mozilla.components.feature.prompts.R
import mozilla.components.support.utils.creditCardIssuerNetwork
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale

/**
* View holder for displaying a credit card item.
Expand All @@ -37,33 +34,15 @@ class CreditCardItemViewHolder(
itemView.findViewById<TextView>(R.id.credit_card_number).text =
creditCard.obfuscatedCardNumber

bindCreditCardExpiryDate(creditCard)
itemView.findViewById<TextView>(R.id.credit_card_expiration_date).text =
creditCard.expiryDate

itemView.setOnClickListener {
onCreditCardSelected(creditCard)
}
}

/**
* Set the credit card expiry date formatted according to the locale.
*/
private fun bindCreditCardExpiryDate(creditCard: CreditCardEntry) {
val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault())

val calendar = Calendar.getInstance()
calendar.set(Calendar.DAY_OF_MONTH, 1)
// Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed.
calendar.set(Calendar.MONTH, creditCard.expiryMonth.toInt() - 1)
calendar.set(Calendar.YEAR, creditCard.expiryYear.toInt())

itemView.findViewById<TextView>(R.id.credit_card_expiration_date).text =
dateFormat.format(calendar.time)
}

companion object {
val LAYOUT_ID = R.layout.mozac_feature_prompts_credit_card_list_item

// Date format pattern for the credit card expiry date.
private const val DATE_PATTERN = "MM/yyyy"
}
}
Loading

0 comments on commit a2cbcac

Please sign in to comment.