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

Change CardInputWidget icon to represent validity of input #2224

Merged
merged 1 commit into from
Feb 26, 2020
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
2 changes: 2 additions & 0 deletions stripe/src/main/java/com/stripe/android/model/CardBrand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum class CardBrand(
val displayName: String,
@DrawableRes val icon: Int,
@DrawableRes val cvcIcon: Int = R.drawable.stripe_ic_cvc,
@DrawableRes val errorIcon: Int = R.drawable.stripe_ic_error,

/**
* Accepted CVC lengths
Expand Down Expand Up @@ -47,6 +48,7 @@ enum class CardBrand(
"American Express",
R.drawable.stripe_ic_amex,
cvcIcon = R.drawable.stripe_ic_cvc_amex,
errorIcon = R.drawable.stripe_ic_error_amex,
cvcLength = setOf(3, 4),
defaultMaxLength = 15,
prefixes = listOf("34", "37"),
Expand Down
81 changes: 60 additions & 21 deletions stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ class CardInputWidget @JvmOverloads constructor(
cardValidCallback?.onInputChanged(invalidFields.isEmpty(), invalidFields)
}
}
private val inputChangeTextWatcher = object : StripeTextWatcher() {
override fun afterTextChanged(s: Editable?) {
super.afterTextChanged(s)
shouldShowErrorIcon = false
}
}

private val invalidFields: Set<CardValidCallback.Fields>
get() {
return listOfNotNull(
Expand All @@ -85,6 +92,17 @@ class CardInputWidget @JvmOverloads constructor(
).toSet()
}

@VisibleForTesting
internal var shouldShowErrorIcon = false
private set(value) {
val isValueChange = field != value
field = value

if (isValueChange) {
updateIcon()
}
}

@JvmSynthetic
internal var cardNumberIsViewed = true

Expand Down Expand Up @@ -119,13 +137,17 @@ class CardInputWidget @JvmOverloads constructor(

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

/**
* The [StripeEditText] fields that are currently enabled and active in the UI.
*/
@VisibleForTesting
internal val allFields: List<StripeEditText>
internal val currentFields: List<StripeEditText>
@JvmSynthetic
get() {
return standardFields
return requiredFields
.plus(postalCodeEditText.takeIf { postalCodeEnabled })
.filterNotNull()
}
Expand Down Expand Up @@ -193,7 +215,7 @@ class CardInputWidget @JvmOverloads constructor(
postalCodeRequired && postalCodeEditText.fieldText.isBlank()

// Announce error messages for accessibility
allFields
currentFields
.filter { it.shouldShowError }
.forEach { editText ->
editText.errorMessage?.let { errorMessage ->
Expand All @@ -215,12 +237,15 @@ class CardInputWidget @JvmOverloads constructor(
postalCodeEditText.requestFocus()
}
else -> {
shouldShowErrorIcon = false
return Card.Builder(cardNumber, cardDate.first, cardDate.second, cvcValue)
.addressZip(postalCodeValue)
.loggingTokens(setOf(LOGGING_TOKEN))
}
}

shouldShowErrorIcon = true

return null
}

Expand All @@ -237,7 +262,7 @@ class CardInputWidget @JvmOverloads constructor(
*/
var postalCodeEnabled: Boolean = CardWidget.DEFAULT_POSTAL_CODE_ENABLED
set(value) {
updatePostalCodeEditText(value)
onPostalCodeEnabledChanged(value)
field = value
}

Expand Down Expand Up @@ -275,26 +300,28 @@ class CardInputWidget @JvmOverloads constructor(
expiryDateEditText = expiryDateTextInputLayout.findViewById(R.id.et_expiry_date)
cvcNumberEditText = cvcNumberTextInputLayout.findViewById(R.id.et_cvc)
postalCodeEditText = postalCodeTextInputLayout.findViewById(R.id.et_postal_code)

postalCodeEditText.configureForGlobal()

frameWidthSupplier = { frameLayout.width }

cardIconImageView = findViewById(R.id.iv_card_icon)

standardFields = listOf(
requiredFields = listOf(
cardNumberEditText, cvcNumberEditText, expiryDateEditText
)
allFields = requiredFields.plus(postalCodeEditText)

initView(attrs)
}

override fun setCardValidCallback(callback: CardValidCallback?) {
this.cardValidCallback = callback
standardFields.forEach { it.removeTextChangedListener(cardValidTextWatcher) }
requiredFields.forEach { it.removeTextChangedListener(cardValidTextWatcher) }

// only add the TextWatcher if it will be used
if (callback != null) {
standardFields.forEach { it.addTextChangedListener(cardValidTextWatcher) }
requiredFields.forEach { it.addTextChangedListener(cardValidTextWatcher) }
}

// call immediately after setting
Expand Down Expand Up @@ -362,11 +389,11 @@ class CardInputWidget @JvmOverloads constructor(
* Clear all text fields in the CardInputWidget.
*/
override fun clear() {
if (allFields.any { it.hasFocus() } || this.hasFocus()) {
if (currentFields.any { it.hasFocus() } || this.hasFocus()) {
cardNumberEditText.requestFocus()
}

allFields.forEach { it.setText("") }
currentFields.forEach { it.setText("") }
}

/**
Expand All @@ -375,7 +402,7 @@ class CardInputWidget @JvmOverloads constructor(
* @param isEnabled boolean indicating whether fields should be enabled
*/
override fun setEnabled(isEnabled: Boolean) {
allFields.forEach { it.isEnabled = isEnabled }
currentFields.forEach { it.isEnabled = isEnabled }
}

/**
Expand Down Expand Up @@ -414,7 +441,7 @@ class CardInputWidget @JvmOverloads constructor(
* `false` otherwise
*/
override fun isEnabled(): Boolean {
return standardFields.all { it.isEnabled }
return requiredFields.all { it.isEnabled }
}

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
Expand Down Expand Up @@ -489,7 +516,7 @@ class CardInputWidget @JvmOverloads constructor(
}
}

private fun updatePostalCodeEditText(isEnabled: Boolean) {
private fun onPostalCodeEnabledChanged(isEnabled: Boolean) {
if (isEnabled) {
postalCodeEditText.isEnabled = true
postalCodeTextInputLayout.visibility = View.VISIBLE
Expand Down Expand Up @@ -678,7 +705,7 @@ class CardInputWidget @JvmOverloads constructor(
cardNumberEditText.hint = it
}

allFields.forEach { it.setErrorColor(errorColorInt) }
currentFields.forEach { it.setErrorColor(errorColorInt) }

cardNumberEditText.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
Expand Down Expand Up @@ -738,6 +765,8 @@ class CardInputWidget @JvmOverloads constructor(
}
}

allFields.forEach { it.addTextChangedListener(inputChangeTextWatcher) }

cardNumberEditText.requestFocus()
}

Expand Down Expand Up @@ -957,20 +986,30 @@ class CardInputWidget @JvmOverloads constructor(
}

private fun updateIcon() {
cardIconImageView.setImageResource(brand.icon)
if (brand == CardBrand.Unknown) {
applyTint(false)
if (shouldShowErrorIcon) {
cardIconImageView.setImageResource(brand.errorIcon)
} else {
cardIconImageView.setImageResource(brand.icon)
if (brand == CardBrand.Unknown) {
applyTint(false)
}
}
}

private fun updateIconCvc(
hasFocus: Boolean,
cvcText: String?
) {
if (shouldIconShowBrand(brand, hasFocus, cvcText)) {
updateIcon()
} else {
updateIconForCvcEntry()
when {
shouldShowErrorIcon -> {
updateIcon()
}
shouldIconShowBrand(brand, hasFocus, cvcText) -> {
updateIcon()
}
else -> {
updateIconForCvcEntry()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,34 @@ import com.stripe.android.R
*/
internal class CardInputTestActivity : AppCompatActivity() {

lateinit var cardInputWidget: CardInputWidget
lateinit var cardMultilineWidget: CardMultilineWidget
lateinit var noZipCardMulitlineWidget: CardMultilineWidget
lateinit var maskedCardView: MaskedCardView

val cardNumberEditText: CardNumberEditText
get() = cardInputWidget.findViewById(R.id.et_card_number)
val cardInputWidget: CardInputWidget by lazy {
CardInputWidget(this)
}
val cardMultilineWidget: CardMultilineWidget by lazy {
CardMultilineWidget(this, shouldShowPostalCode = true)
}
val noZipCardMulitlineWidget: CardMultilineWidget by lazy {
CardMultilineWidget(this, shouldShowPostalCode = false)
}
val maskedCardView: MaskedCardView by lazy {
MaskedCardView(this)
}
val cardNumberEditText: CardNumberEditText by lazy {
cardInputWidget.findViewById<CardNumberEditText>(R.id.et_card_number)
}

val expiryDateEditText: ExpiryDateEditText
get() = cardInputWidget.findViewById(R.id.et_expiry_date)
val expiryDateEditText: ExpiryDateEditText by lazy {
cardInputWidget.findViewById<ExpiryDateEditText>(R.id.et_expiry_date)
}

val cvcEditText: StripeEditText
get() = cardInputWidget.findViewById(R.id.et_cvc)
val cvcEditText: StripeEditText by lazy {
cardInputWidget.findViewById<StripeEditText>(R.id.et_cvc)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setTheme(R.style.StripeDefaultTheme)
cardInputWidget = CardInputWidget(this)
cardMultilineWidget = CardMultilineWidget(this, shouldShowPostalCode = true)
noZipCardMulitlineWidget = CardMultilineWidget(this, shouldShowPostalCode = false)
maskedCardView = MaskedCardView(this)

val linearLayout = LinearLayout(this)
linearLayout.addView(cardInputWidget)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ internal class CardInputWidgetTest : BaseViewTest<CardInputTestActivity>(
cardInputWidget.findViewById<PostalCodeEditText>(R.id.et_postal_code)
}
private val cardInputListener: CardInputListener = mock()
private val cardIcon: ImageView by lazy {
cardInputWidget.findViewById<ImageView>(R.id.iv_card_icon)
}

@BeforeTest
fun setup() {
Expand Down Expand Up @@ -1204,13 +1207,13 @@ internal class CardInputWidgetTest : BaseViewTest<CardInputTestActivity>(
@Test
fun allFields_equals_standardFields_withPostalCodeDisabled() {
cardInputWidget.postalCodeEnabled = false
assertEquals(cardInputWidget.standardFields, cardInputWidget.allFields)
assertEquals(cardInputWidget.requiredFields, cardInputWidget.currentFields)
}

@Test
fun allFields_notEquals_standardFields_withPostalCodeEnabled() {
cardInputWidget.postalCodeEnabled = true
assertNotEquals(cardInputWidget.standardFields, cardInputWidget.allFields)
assertNotEquals(cardInputWidget.requiredFields, cardInputWidget.currentFields)
}

@Test
Expand Down Expand Up @@ -1277,6 +1280,27 @@ internal class CardInputWidgetTest : BaseViewTest<CardInputTestActivity>(
)
}

@Test
fun shouldShowErrorIcon_shouldBeUpdatedCorrectly() {
cardInputWidget.setExpiryDate(12, 2030)
cardInputWidget.setCvcCode(CVC_VALUE_COMMON)

// show error icon when validating fields with invalid card number
cardInputWidget.setCardNumber(VISA_NO_SPACES.take(6))
assertNull(cardInputWidget.paymentMethodCreateParams)
assertTrue(cardInputWidget.shouldShowErrorIcon)

// don't show error icon after changing input
cardInputWidget.setCardNumber(VISA_NO_SPACES.take(7))
assertFalse(cardInputWidget.shouldShowErrorIcon)

// don't show error icon when validating fields with invalid card number
assertNull(cardInputWidget.paymentMethodCreateParams)
cardInputWidget.setCardNumber(VISA_NO_SPACES)
assertNotNull(cardInputWidget.paymentMethodCreateParams)
assertFalse(cardInputWidget.shouldShowErrorIcon)
}

private companion object {
// Every Card made by the CardInputView should have the card widget token.
private val ATTRIBUTION = setOf(LOGGING_TOKEN)
Expand Down