Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CardInputWidget.CardValidCallback will consider the postal code in validation. #4202

Merged
merged 3 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.stripe.example.activity

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
Expand Down Expand Up @@ -50,11 +51,21 @@ class CreateCardSourceActivity : AppCompatActivity() {
setContentView(viewBinding.root)

viewBinding.createButton.setOnClickListener {
// If a field is invalid this will shift focus to that field
viewBinding.cardWidget.cardParams?.let {
createCardSource(it)
} ?: showSnackbar("Enter a valid card.")
}

viewBinding.cardWidget.setCardValidCallback { isValid, invalidFields -> // We will not call cardParams unless it is valid because
// this will cause the inFocus field to change.
if (isValid) {
Log.e("STRIPE", "Validity: $isValid, ${viewBinding.cardWidget.cardParams}")
} else {
Log.e("STRIPE", "Validity: $isValid, invalidField: $invalidFields")
}
}

viewBinding.recyclerView.setHasFixedSize(true)
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this)
viewBinding.recyclerView.adapter = sourcesAdapter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class CardInputWidget @JvmOverloads constructor(
},
CardValidCallback.Fields.Cvc.takeIf {
this.cvc == null
},
CardValidCallback.Fields.Postal.takeIf {
isPostalRequired() && postalCodeEditText.postalCode.isNullOrBlank()
}
).toSet()
}
Expand Down Expand Up @@ -152,8 +155,8 @@ class CardInputWidget @JvmOverloads constructor(

@VisibleForTesting
@JvmSynthetic
internal val requiredFields: List<StripeEditText>
private val allFields: List<StripeEditText>
internal val requiredFields: MutableSet<StripeEditText>
private val allFields: Set<StripeEditText>

/**
* The [StripeEditText] fields that are currently enabled and active in the UI.
Expand All @@ -169,7 +172,7 @@ class CardInputWidget @JvmOverloads constructor(

/**
* A [PaymentMethodCreateParams.Card] representing the card details if all fields are valid;
* otherwise `null`
* otherwise `null`. If a field is invalid focus will shift to the invalid field.
*/
override val paymentMethodCard: PaymentMethodCreateParams.Card?
get() {
Expand Down Expand Up @@ -197,7 +200,7 @@ class CardInputWidget @JvmOverloads constructor(

/**
* A [PaymentMethodCreateParams] representing the card details and postal code if all fields
* are valid; otherwise `null`
* are valid; otherwise `null`. If a field is invalid focus will shift to the invalid field
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: missing a . here

*/
override val paymentMethodCreateParams: PaymentMethodCreateParams?
get() {
Expand All @@ -208,7 +211,7 @@ class CardInputWidget @JvmOverloads constructor(

/**
* A [CardParams] representing the card details and postal code if all fields are valid;
* otherwise `null`
* otherwise `null`. If a field is invalid focus will shift to the invalid field.
*/
override val cardParams: CardParams?
get() {
Expand Down Expand Up @@ -275,7 +278,7 @@ class CardInputWidget @JvmOverloads constructor(
/**
* The postal code field is enabled by default. Disabling the postal code field may impact
* auth success rates, so it is discouraged to disable it unless you are collecting the postal
* code outside of this form.
* code outside of this form. If the postal code is disabled it will not be shown in the view.
*/
var postalCodeEnabled: Boolean by Delegates.observable(
CardWidget.DEFAULT_POSTAL_CODE_ENABLED
Expand All @@ -291,6 +294,7 @@ class CardInputWidget @JvmOverloads constructor(

cvcEditText.imeOptions = EditorInfo.IME_ACTION_DONE
}
updatePostalRequired()
}

/**
Expand All @@ -302,7 +306,11 @@ class CardInputWidget @JvmOverloads constructor(
* Note that some countries do not have postal codes, so requiring postal code will prevent
* those users from submitting this form successfully.
*/
var postalCodeRequired: Boolean = CardWidget.DEFAULT_POSTAL_CODE_REQUIRED
var postalCodeRequired: Boolean by Delegates.observable(
CardWidget.DEFAULT_POSTAL_CODE_REQUIRED
) { _, _, _ ->
updatePostalRequired()
}

/**
* If [postalCodeEnabled] is true and [usZipCodeRequired] is true, then postal code is a
Expand All @@ -318,8 +326,20 @@ class CardInputWidget @JvmOverloads constructor(
} else {
postalCodeEditText.config = PostalCodeEditText.Config.Global
}

updatePostalRequired()
}

private fun updatePostalRequired() {
if (isPostalRequired()) {
requiredFields.add(postalCodeEditText)
} else {
requiredFields.remove(postalCodeEditText)
}
}

private fun isPostalRequired() = (postalCodeRequired || usZipCodeRequired) && postalCodeEnabled

private val frameStart: Int
get() {
val isLtr = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
Expand All @@ -342,7 +362,7 @@ class CardInputWidget @JvmOverloads constructor(

frameWidthSupplier = { containerLayout.width }

requiredFields = listOf(
requiredFields = mutableSetOf(
cardNumberEditText,
cvcEditText,
expiryDateEditText
Expand Down Expand Up @@ -481,8 +501,8 @@ class CardInputWidget @JvmOverloads constructor(
* Override of [View.isEnabled] that returns `true` only
* if all three sub-controls are enabled.
*
* @return `true` if the card number field, expiry field, and cvc field are enabled,
* `false` otherwise
* @return `true` if the card number field, expiry field, cvc field, and postal (if required)
* are enabled, `false` otherwise
*/
override fun isEnabled(): Boolean {
return requiredFields.all { it.isEnabled }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1297,18 +1297,18 @@ internal class CardInputWidgetTest {
idleLooper()

assertThat(cardInputWidget.requiredFields)
.isEqualTo(cardInputWidget.currentFields)
.isEqualTo(cardInputWidget.currentFields.toHashSet())
}

@Test
fun currentFields_notEquals_requiredFields_withPostalCodeEnabled() {
cardInputWidget.postalCodeEnabled = true
assertThat(cardInputWidget.requiredFields)
.isNotEqualTo(cardInputWidget.currentFields)
.isNotEqualTo(cardInputWidget.currentFields.toHashSet())
}

@Test
fun testCardValidCallback() {
fun testCardValidCallback_withPostalCodeDefaultDisabled() {
var currentIsValid = false
var currentInvalidFields = emptySet<CardValidCallback.Fields>()
cardInputWidget.setCardValidCallback { isValid, invalidFields ->
Expand Down Expand Up @@ -1362,6 +1362,60 @@ internal class CardInputWidgetTest {
.containsExactly(CardValidCallback.Fields.Cvc)
}

@Test
fun testCardValidCallback_withPostalCodeEnabledNotRequired() {
var currentIsValid = false
var currentInvalidFields = emptySet<CardValidCallback.Fields>()
cardInputWidget.postalCodeEnabled = true
cardInputWidget.setCardValidCallback { isValid, invalidFields ->
currentIsValid = isValid
currentInvalidFields = invalidFields
}
assertThat(currentIsValid)
.isFalse()
assertThat(currentInvalidFields)
.containsExactly(
CardValidCallback.Fields.Number,
CardValidCallback.Fields.Expiry,
CardValidCallback.Fields.Cvc
)
}

@Test
fun testCardValidCallback_withPostalCodeEnabledAndRequired() {
var currentIsValid = false
var currentInvalidFields = emptySet<CardValidCallback.Fields>()
cardInputWidget.postalCodeEnabled = true
cardInputWidget.usZipCodeRequired = true
cardInputWidget.setCardValidCallback { isValid, invalidFields ->
currentIsValid = isValid
currentInvalidFields = invalidFields
}
assertThat(currentIsValid)
.isFalse()
assertThat(currentInvalidFields)
.containsExactly(
CardValidCallback.Fields.Number,
CardValidCallback.Fields.Expiry,
CardValidCallback.Fields.Cvc,
CardValidCallback.Fields.Postal
)

cardInputWidget.setCardNumber(VISA_NO_SPACES)
expiryEditText.setText("1250")
cvcEditText.setText("123")
assertThat(currentIsValid).isFalse()
assertThat(currentInvalidFields).containsExactly(CardValidCallback.Fields.Postal)

postalCodeEditText.setText("12345")
assertThat(currentIsValid).isTrue()
assertThat(currentInvalidFields).isEmpty()

postalCodeEditText.setText("123")
assertThat(currentIsValid).isFalse()
assertThat(currentInvalidFields).containsExactly(CardValidCallback.Fields.Postal)
}

@Test
fun shouldShowErrorIcon_shouldBeUpdatedCorrectly() {
cardInputWidget.setExpiryDate(12, 2030)
Expand Down Expand Up @@ -1480,31 +1534,35 @@ internal class CardInputWidgetTest {

@Test
fun `Verify on postal code focus change listeners trigger the callback`() {
cardInputWidget.postalCodeEditText.getParentOnFocusChangeListener().onFocusChange(cardInputWidget.postalCodeEditText, true)
cardInputWidget.postalCodeEditText.getParentOnFocusChangeListener()
.onFocusChange(cardInputWidget.postalCodeEditText, true)

assertThat(cardInputListener.focusedFields)
.contains(CardInputListener.FocusField.PostalCode)
}

@Test
fun `Verify on cvc focus change listeners trigger the callback`() {
cardInputWidget.cvcEditText.getParentOnFocusChangeListener().onFocusChange(cardInputWidget.cvcEditText, true)
cardInputWidget.cvcEditText.getParentOnFocusChangeListener()
.onFocusChange(cardInputWidget.cvcEditText, true)

assertThat(cardInputListener.focusedFields)
.contains(CardInputListener.FocusField.Cvc)
}

@Test
fun `Verify on expiration date focus change listeners trigger the callback`() {
cardInputWidget.expiryDateEditText.getParentOnFocusChangeListener().onFocusChange(cardInputWidget.expiryDateEditText, true)
cardInputWidget.expiryDateEditText.getParentOnFocusChangeListener()
.onFocusChange(cardInputWidget.expiryDateEditText, true)

assertThat(cardInputListener.focusedFields)
.contains(CardInputListener.FocusField.ExpiryDate)
}

@Test
fun `Verify on card number focus change listeners trigger the callback`() {
cardInputWidget.cardNumberEditText.getParentOnFocusChangeListener().onFocusChange(cardInputWidget.cardNumberEditText, true)
cardInputWidget.cardNumberEditText.getParentOnFocusChangeListener()
.onFocusChange(cardInputWidget.cardNumberEditText, true)

assertThat(cardInputListener.focusedFields)
.contains(CardInputListener.FocusField.CardNumber)
Expand Down