For #18266: Validate credit card entry info.

upstream-sync
mcarare 3 years ago committed by Mihai Adrian Carare
parent 3428dd67ed
commit 516d52997d

@ -76,7 +76,7 @@ class CreditCardEditorFragment : Fragment(R.layout.fragment_credit_card_editor)
true
}
R.id.save_credit_card_button -> {
creditCardEditorView.saveCreditCardInfo(creditCardEditorState)
creditCardEditorView.saveCreditCard(creditCardEditorState)
true
}
else -> false

@ -8,6 +8,8 @@ import androidx.annotation.VisibleForTesting
private const val MAX_CREDIT_CARD_NUMBER_LENGTH = 19
private const val MIN_CREDIT_CARD_NUMBER_LENGTH = 12
// Number of last digits to be shown when credit card number is obfuscated.
private const val LAST_VISIBLE_DIGITS_COUNT = 4
/**
* Strips characters other than digits from a string.
@ -17,6 +19,13 @@ fun String.toCreditCardNumber(): String {
return this.filter { it.isDigit() }
}
/**
* Returns the last 4 digits from a formatted credit card number string.
*/
fun String.last4Digits(): String {
return this.takeLast(LAST_VISIBLE_DIGITS_COUNT)
}
/**
* Uses string size and Luhn Algorithm validation to validate a credit card number.
*/

@ -4,19 +4,24 @@
package org.mozilla.fenix.settings.creditcards.view
import android.R
import android.view.View
import android.widget.ArrayAdapter
import androidx.annotation.VisibleForTesting
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_credit_card_editor.*
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.NewCreditCardFields
import mozilla.components.concept.storage.UpdatableCreditCardFields
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.toEditable
import org.mozilla.fenix.settings.creditcards.CreditCardEditorFragment.Companion.CARD_TYPE_PLACEHOLDER
import org.mozilla.fenix.settings.creditcards.CreditCardEditorState
import org.mozilla.fenix.settings.creditcards.interactor.CreditCardEditorInteractor
import org.mozilla.fenix.settings.creditcards.last4Digits
import org.mozilla.fenix.settings.creditcards.toCreditCardNumber
import org.mozilla.fenix.settings.creditcards.validateCreditCardNumber
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -48,7 +53,7 @@ class CreditCardEditorView(
}
save_button.setOnClickListener {
saveCreditCardInfo(state)
saveCreditCard(state)
}
card_number_input.text = state.cardNumber.toEditable()
@ -58,33 +63,59 @@ class CreditCardEditorView(
bindExpiryYearDropDown(state.expiryYears)
}
@Suppress("MagicNumber")
internal fun saveCreditCardInfo(state: CreditCardEditorState) {
/**
* Saves a new credit card or updates an existing one with data from user input.
* @param state The state for the CreditCardEditorFragment containing all credit card data.
*/
internal fun saveCreditCard(state: CreditCardEditorState) {
containerView.hideKeyboard()
// TODO need to know if we're updating a number, or just round-tripping it
val cardNumber = card_number_input.text.toString()
if (state.isEditing) {
val fields = UpdatableCreditCardFields(
billingName = name_on_card_input.text.toString(),
cardNumber = CreditCardNumber.Encrypted(cardNumber),
cardNumberLast4 = cardNumber.substring(cardNumber.length - 4),
expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(),
expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(),
cardType = CARD_TYPE_PLACEHOLDER
)
interactor.onUpdateCreditCard(state.guid, fields)
if (validateCreditCard()) {
val cardNumber = card_number_input.text.toString().toCreditCardNumber()
if (state.isEditing) {
val fields = UpdatableCreditCardFields(
billingName = name_on_card_input.text.toString(),
cardNumber = CreditCardNumber.Encrypted(cardNumber),
cardNumberLast4 = cardNumber.last4Digits(),
expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(),
expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(),
cardType = CARD_TYPE_PLACEHOLDER
)
interactor.onUpdateCreditCard(state.guid, fields)
} else {
val fields = NewCreditCardFields(
billingName = name_on_card_input.text.toString(),
plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber),
cardNumberLast4 = cardNumber.last4Digits(),
expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(),
expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(),
cardType = CARD_TYPE_PLACEHOLDER
)
interactor.onSaveCreditCard(fields)
}
}
}
/**
* Validates the credit card information entered by the user.
* @return true if the credit card is valid, false otherwise.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun validateCreditCard(): Boolean {
var isValid = true
if (card_number_input.text.toString().validateCreditCardNumber()) {
card_number_layout.error = null
card_number_title.setTextColor(containerView.context.getColorFromAttr(R.attr.primaryText))
} else {
val fields = NewCreditCardFields(
billingName = name_on_card_input.text.toString(),
plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber),
cardNumberLast4 = cardNumber.substring(cardNumber.length - 4),
expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(),
expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(),
cardType = CARD_TYPE_PLACEHOLDER
)
interactor.onSaveCreditCard(fields)
card_number_layout.error =
containerView.context.getString(R.string.credit_cards_number_validation_error_message)
card_number_title.setTextColor(containerView.context.getColorFromAttr(R.attr.destructive))
isValid = false
}
return isValid
}
/**
@ -95,7 +126,10 @@ class CreditCardEditorView(
*/
private fun bindExpiryMonthDropDown(expiryMonth: Int) {
val adapter =
ArrayAdapter<String>(containerView.context, R.layout.simple_spinner_dropdown_item)
ArrayAdapter<String>(
containerView.context,
android.R.layout.simple_spinner_dropdown_item
)
val dateFormat = SimpleDateFormat("MMMM (MM)", Locale.getDefault())
val calendar = Calendar.getInstance()
@ -118,7 +152,10 @@ class CreditCardEditorView(
*/
private fun bindExpiryYearDropDown(expiryYears: Pair<Int, Int>) {
val adapter =
ArrayAdapter<String>(containerView.context, R.layout.simple_spinner_dropdown_item)
ArrayAdapter<String>(
containerView.context,
android.R.layout.simple_spinner_dropdown_item
)
val (startYear, endYear) = expiryYears
for (year in startYear until endYear) {

@ -1549,6 +1549,8 @@
<string name="credit_cards_cancel_button">Cancel</string>
<!-- Title of the "Saved cards" screen -->
<string name="credit_cards_saved_cards">Saved cards</string>
<!-- Error message for credit card number validation -->
<string name="credit_cards_number_validation_error_message">Please enter a valid credit card number</string>
<!-- Title of the Add search engine screen -->
<string name="search_engine_add_custom_search_engine_title">Add search engine</string>

@ -7,6 +7,7 @@ package org.mozilla.fenix.settings.creditcards
import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.android.synthetic.main.fragment_credit_card_editor.view.*
import mozilla.components.concept.storage.CreditCard
@ -15,6 +16,8 @@ import mozilla.components.concept.storage.NewCreditCardFields
import mozilla.components.concept.storage.UpdatableCreditCardFields
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@ -37,8 +40,8 @@ class CreditCardEditorViewTest {
private val creditCard = CreditCard(
guid = "id",
billingName = "Banana Apple",
encryptedCardNumber = CreditCardNumber.Encrypted("4111111111111110"),
cardNumberLast4 = "1110",
encryptedCardNumber = CreditCardNumber.Encrypted("371449635398431"),
cardNumberLast4 = "8431",
expiryMonth = 5,
expiryYear = 2030,
cardType = "amex",
@ -53,7 +56,7 @@ class CreditCardEditorViewTest {
view = LayoutInflater.from(testContext).inflate(R.layout.fragment_credit_card_editor, null)
interactor = mockk(relaxed = true)
creditCardEditorView = CreditCardEditorView(view, interactor)
creditCardEditorView = spyk(CreditCardEditorView(view, interactor))
}
@Test
@ -124,7 +127,7 @@ class CreditCardEditorViewTest {
}
@Test
fun `GIVEN the credit input are filled WHEN the save button is clicked THEN interactor is called`() {
fun `GIVEN invalid credit card number WHEN the save button is clicked THEN interactor is not called`() {
creditCardEditorView.bind(getInitialCreditCardEditorState())
val calendar = Calendar.getInstance()
@ -141,6 +144,12 @@ class CreditCardEditorViewTest {
view.save_button.performClick()
verify {
creditCardEditorView.validateCreditCard()
}
assertFalse(creditCardEditorView.validateCreditCard())
verify(exactly = 0) {
interactor.onSaveCreditCard(
NewCreditCardFields(
billingName = billingName,
@ -155,7 +164,44 @@ class CreditCardEditorViewTest {
}
@Test
fun `GIVEN a credit card WHEN the save button is clicked THEN interactor is called`() {
fun `GIVEN valid credit card number WHEN the save button is clicked THEN interactor is called`() {
creditCardEditorView.bind(getInitialCreditCardEditorState())
val calendar = Calendar.getInstance()
val billingName = "Banana Apple"
val cardNumber = "371449635398431"
val expiryMonth = 5
val expiryYear = calendar.get(Calendar.YEAR)
view.card_number_input.text = cardNumber.toEditable()
view.name_on_card_input.text = billingName.toEditable()
view.expiry_month_drop_down.setSelection(expiryMonth - 1)
view.save_button.performClick()
verify {
creditCardEditorView.validateCreditCard()
}
assertTrue(creditCardEditorView.validateCreditCard())
verify {
interactor.onSaveCreditCard(
NewCreditCardFields(
billingName = billingName,
plaintextCardNumber = CreditCardNumber.Plaintext(cardNumber),
cardNumberLast4 = "8431",
expiryMonth = expiryMonth.toLong(),
expiryYear = expiryYear.toLong(),
cardType = CARD_TYPE_PLACEHOLDER
)
)
}
}
@Test
fun `GIVEN a valid credit card WHEN the save button is clicked THEN interactor is called`() {
creditCardEditorView.bind(creditCard.toCreditCardEditorState())
view.save_button.performClick()

@ -24,6 +24,16 @@ class StringTest {
assertEquals("123456789", "1-234-5678-9".toCreditCardNumber())
}
@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())
}
@Test
fun `validateCreditCardNumber returns true for valid credit card numbers `() {
val americanExpressCard = "371449635398431"

Loading…
Cancel
Save