mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-03 23:15:31 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/18266: Validate credit card entry info.
This commit is contained in:
parent
8155ca4c64
commit
3681f15501
@ -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,35 +63,61 @@ 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)
|
||||
} 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)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the expiry month dropdown by formatting and populating it with the months in a calendar
|
||||
* year, and set the selection to the provided expiry month.
|
||||
@ -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…
Reference in New Issue
Block a user