From f278e6e1b5ecaad7ba7afcbc5094ad0c6656af6a Mon Sep 17 00:00:00 2001 From: Michael Shafrir Date: Thu, 27 Feb 2020 16:22:34 -0500 Subject: [PATCH] Change CardMultilineWidget icon to represent validity of input Summary After validating `CardMultilineWidget` input (i.e. when a params object is attempted to be created), if validation fails, show an error icon for the brand icon. Testing Add unit tests Manually test --- .../android/view/CardMultilineWidget.kt | 61 +++++++++++++++++-- .../android/view/CardMultilineWidgetTest.kt | 21 +++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/stripe/src/main/java/com/stripe/android/view/CardMultilineWidget.kt b/stripe/src/main/java/com/stripe/android/view/CardMultilineWidget.kt index 72ebf8548b2..735e88514b2 100644 --- a/stripe/src/main/java/com/stripe/android/view/CardMultilineWidget.kt +++ b/stripe/src/main/java/com/stripe/android/view/CardMultilineWidget.kt @@ -16,6 +16,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.IntRange import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import com.google.android.material.textfield.TextInputLayout @@ -164,9 +165,12 @@ class CardMultilineWidget @JvmOverloads constructor( override val cardBuilder: Card.Builder? get() { if (!validateAllFields()) { + shouldShowErrorIcon = true return null } + shouldShowErrorIcon = false + val cardNumber = cardNumber val cardDate = requireNotNull(expiryDateEditText.validDateFields) val cvcValue = cvcEditText.text?.toString() @@ -216,6 +220,17 @@ class CardMultilineWidget @JvmOverloads constructor( .setScale(0, RoundingMode.HALF_DOWN) .toInt() + @VisibleForTesting + internal var shouldShowErrorIcon = false + private set(value) { + val isValueChange = field != value + field = value + + if (isValueChange) { + updateBrandUi() + } + } + init { orientation = VERTICAL View.inflate(getContext(), R.layout.card_multiline_widget, this) @@ -294,6 +309,15 @@ class CardMultilineWidget @JvmOverloads constructor( cardBrand = CardBrand.Unknown updateBrandUi() + allFields.forEach { + it.addTextChangedListener(object : StripeTextWatcher() { + override fun afterTextChanged(s: Editable?) { + super.afterTextChanged(s) + shouldShowErrorIcon = false + } + }) + } + isEnabled = true } @@ -518,11 +542,21 @@ class CardMultilineWidget @JvmOverloads constructor( } private fun flipToCvcIconIfNotFinished() { - if (cardBrand.isMaxCvc(cvcEditText.text?.toString())) { + if (cardBrand.isMaxCvc(cvcEditText.fieldText)) { return } - updateDrawable(cardBrand.cvcIcon, true) + if (shouldShowErrorIcon) { + updateDrawable( + iconResourceId = cardBrand.errorIcon, + shouldTint = false + ) + } else { + updateDrawable( + iconResourceId = cardBrand.cvcIcon, + shouldTint = true + ) + } } private fun initDeleteEmptyListeners() { @@ -595,26 +629,41 @@ class CardMultilineWidget @JvmOverloads constructor( private fun updateBrandUi() { updateCvc() - updateDrawable(cardBrand.icon, CardBrand.Unknown == cardBrand) + if (shouldShowErrorIcon) { + updateDrawable( + iconResourceId = cardBrand.errorIcon, + shouldTint = false + ) + } else { + updateDrawable( + iconResourceId = cardBrand.icon, + shouldTint = CardBrand.Unknown == cardBrand + ) + } } private fun updateCvc() { cvcEditText.updateBrand(cardBrand, customCvcLabel, cvcTextInputLayout) } - private fun updateDrawable(@DrawableRes iconResourceId: Int, needsTint: Boolean) { + private fun updateDrawable(@DrawableRes iconResourceId: Int, shouldTint: Boolean) { val icon = ContextCompat.getDrawable(context, iconResourceId) ?: return val original = cardNumberEditText.compoundDrawablesRelative[0] ?: return val iconPadding = cardNumberEditText.compoundDrawablePadding icon.bounds = createDrawableBounds(original) val compatIcon = DrawableCompat.wrap(icon) - if (needsTint) { + if (shouldTint) { DrawableCompat.setTint(compatIcon.mutate(), tintColorInt) } cardNumberEditText.compoundDrawablePadding = iconPadding - cardNumberEditText.setCompoundDrawablesRelative(compatIcon, null, null, null) + cardNumberEditText.setCompoundDrawablesRelative( + compatIcon, + null, + null, + null + ) } private fun createDrawableBounds(drawable: Drawable): Rect { diff --git a/stripe/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt b/stripe/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt index 299c666c020..b4e91cae6a2 100644 --- a/stripe/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt +++ b/stripe/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt @@ -753,6 +753,27 @@ internal class CardMultilineWidgetTest { ) } + @Test + fun shouldShowErrorIcon_shouldBeUpdatedCorrectly() { + cardMultilineWidget.setExpiryDate(12, 2030) + cardMultilineWidget.setCvcCode("123") + + // show error icon when validating fields with invalid card number + cardMultilineWidget.setCardNumber(VISA_NO_SPACES.take(6)) + assertNull(cardMultilineWidget.paymentMethodCreateParams) + assertTrue(cardMultilineWidget.shouldShowErrorIcon) + + // don't show error icon after changing input + cardMultilineWidget.setCardNumber(VISA_NO_SPACES.take(7)) + assertFalse(cardMultilineWidget.shouldShowErrorIcon) + + // don't show error icon when validating fields with invalid card number + assertNull(cardMultilineWidget.paymentMethodCreateParams) + cardMultilineWidget.setCardNumber(VISA_NO_SPACES) + assertNotNull(cardMultilineWidget.paymentMethodCreateParams) + assertFalse(cardMultilineWidget.shouldShowErrorIcon) + } + internal class WidgetControlGroup(widget: CardMultilineWidget) { val cardNumberEditText: CardNumberEditText = widget.findViewById(R.id.et_card_number) val cardInputLayout: TextInputLayout = widget.findViewById(R.id.tl_card_number)