From c75cef9532edd81705cd56a2601c299d090ec486 Mon Sep 17 00:00:00 2001 From: Chen Cen <79880926+ccen-stripe@users.noreply.github.com> Date: Thu, 9 Dec 2021 09:02:00 -0800 Subject: [PATCH] callback for postal code complete (#4424) * callback for us zip code complete * resolve comments * have valid callback for all non-empty global postals * lint and dump --- payments-core/api/payments-core.api | 1 + .../stripe/android/view/CardInputListener.kt | 7 +++ .../stripe/android/view/CardInputWidget.kt | 6 +++ .../android/view/CardMultilineWidget.kt | 9 ++++ .../stripe/android/view/PostalCodeEditText.kt | 7 +++ .../android/view/CardInputWidgetTest.kt | 47 +++++++++++++++++++ .../android/view/CardMultilineWidgetTest.kt | 37 ++++++++++++++- .../CardDataCollectionFragment.kt | 2 + 8 files changed, 115 insertions(+), 1 deletion(-) diff --git a/payments-core/api/payments-core.api b/payments-core/api/payments-core.api index b7cfb8d531c..55c85eb6a7c 100644 --- a/payments-core/api/payments-core.api +++ b/payments-core/api/payments-core.api @@ -5671,6 +5671,7 @@ public abstract interface class com/stripe/android/view/CardInputListener { public abstract fun onCvcComplete ()V public abstract fun onExpirationComplete ()V public abstract fun onFocusChange (Lcom/stripe/android/view/CardInputListener$FocusField;)V + public abstract fun onPostalCodeComplete ()V } public final class com/stripe/android/view/CardInputListener$FocusField : java/lang/Enum { diff --git a/payments-core/src/main/java/com/stripe/android/view/CardInputListener.kt b/payments-core/src/main/java/com/stripe/android/view/CardInputListener.kt index 2ce8e50717a..035d7bbb8f0 100644 --- a/payments-core/src/main/java/com/stripe/android/view/CardInputListener.kt +++ b/payments-core/src/main/java/com/stripe/android/view/CardInputListener.kt @@ -39,4 +39,11 @@ interface CardInputListener { * the user edits the CVC. */ fun onCvcComplete() + + /** + * Called when a valid postal code has been entered. + * May be called multiple times, if the user edits the field. + * If the [CardWidget] is not collecting US card, any non-empty postal is considered valid. + */ + fun onPostalCodeComplete() } diff --git a/payments-core/src/main/java/com/stripe/android/view/CardInputWidget.kt b/payments-core/src/main/java/com/stripe/android/view/CardInputWidget.kt index 617c216dda7..6b76fec335d 100644 --- a/payments-core/src/main/java/com/stripe/android/view/CardInputWidget.kt +++ b/payments-core/src/main/java/com/stripe/android/view/CardInputWidget.kt @@ -747,6 +747,12 @@ class CardInputWidget @JvmOverloads constructor( } } + postalCodeEditText.setAfterTextChangedListener { + if (isPostalRequired() && postalCodeEditText.hasValidPostal()) { + cardInputListener?.onPostalCodeComplete() + } + } + cardNumberEditText.completionCallback = { scrollEnd() cardInputListener?.onCardComplete() diff --git a/payments-core/src/main/java/com/stripe/android/view/CardMultilineWidget.kt b/payments-core/src/main/java/com/stripe/android/view/CardMultilineWidget.kt index c59716ee86e..651e92375a7 100644 --- a/payments-core/src/main/java/com/stripe/android/view/CardMultilineWidget.kt +++ b/payments-core/src/main/java/com/stripe/android/view/CardMultilineWidget.kt @@ -146,6 +146,9 @@ class CardMultilineWidget @JvmOverloads constructor( } } + private fun isPostalRequired() = + (postalCodeRequired || usZipCodeRequired) && shouldShowPostalCode + /** * A [PaymentMethodCreateParams.Card] representing the card details if all fields are valid; * otherwise `null` @@ -367,6 +370,12 @@ class CardMultilineWidget @JvmOverloads constructor( cvcEditText.shouldShowError = false } + postalCodeEditText.setAfterTextChangedListener { + if (isPostalRequired() && postalCodeEditText.hasValidPostal()) { + cardInputListener?.onPostalCodeComplete() + } + } + adjustViewForPostalCodeAttribute(shouldShowPostalCode) cardNumberEditText.updateLengthFilter() diff --git a/payments-core/src/main/java/com/stripe/android/view/PostalCodeEditText.kt b/payments-core/src/main/java/com/stripe/android/view/PostalCodeEditText.kt index 6ebfb888b3b..069840ba117 100644 --- a/payments-core/src/main/java/com/stripe/android/view/PostalCodeEditText.kt +++ b/payments-core/src/main/java/com/stripe/android/view/PostalCodeEditText.kt @@ -112,6 +112,13 @@ class PostalCodeEditText @JvmOverloads constructor( US } + /** + * Returns if the postal is valid. If config is not US, any non-empty postal is valid. + */ + internal fun hasValidPostal() = + config == Config.US && ZIP_CODE_PATTERN.matcher(fieldText) + .matches() || config == Config.Global && fieldText.isNotEmpty() + private companion object { private const val MAX_LENGTH_US = 5 diff --git a/payments-core/src/test/java/com/stripe/android/view/CardInputWidgetTest.kt b/payments-core/src/test/java/com/stripe/android/view/CardInputWidgetTest.kt index c6a4d913aaf..1733d22d448 100644 --- a/payments-core/src/test/java/com/stripe/android/view/CardInputWidgetTest.kt +++ b/payments-core/src/test/java/com/stripe/android/view/CardInputWidgetTest.kt @@ -1525,6 +1525,48 @@ internal class CardInputWidgetTest { ) } + @Test + fun usZipCodeRequired_whenFalse_shouldNotCallOnPostalCodeComplete() { + cardInputWidget.usZipCodeRequired = false + postalCodeEditText.setText(POSTAL_CODE_VALUE) + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(0) + } + + @Test + fun usZipCodeRequired_whenTrue_withValidZip_shouldCallOnPostalCodeComplete() { + cardInputWidget.usZipCodeRequired = true + postalCodeEditText.setText(POSTAL_CODE_VALUE) + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(1) + } + + @Test + fun postalCodeEnabled_whenFalse_shouldNotCallOnPostalCodeComplete() { + cardInputWidget.postalCodeEnabled = false + postalCodeEditText.setText("123") + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(0) + } + + @Test + fun usZipCodeRequired_whenTrue_withInvalidZip_shouldNotCallOnPostalCodeComplete() { + cardInputWidget.usZipCodeRequired = true + postalCodeEditText.setText("1234") + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(0) + } + + @Test + fun postalCode_whenTrue_withNonEmptyZip_shouldCallOnPostalCodeComplete() { + cardInputWidget.postalCodeRequired = true + postalCodeEditText.setText("1234") + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(1) + } + + @Test + fun postalCode_whenTrue_withEmptyZip_shouldNotCallOnPostalCodeComplete() { + cardInputWidget.postalCodeRequired = true + postalCodeEditText.setText("") + assertThat(cardInputListener.onPostalCodeCompleteCalls).isEqualTo(0) + } + @Test fun `setCvcLabel is not reset when card number entered`() { cardInputWidget.setCvcLabel("123") @@ -1578,6 +1620,7 @@ internal class CardInputWidgetTest { var cardCompleteCalls = 0 var expirationCompleteCalls = 0 var cvcCompleteCalls = 0 + var onPostalCodeCompleteCalls = 0 override fun onFocusChange(focusField: CardInputListener.FocusField) { focusedFields.add(focusField) @@ -1594,6 +1637,10 @@ internal class CardInputWidgetTest { override fun onCvcComplete() { cvcCompleteCalls++ } + + override fun onPostalCodeComplete() { + onPostalCodeCompleteCalls++ + } } private companion object { diff --git a/payments-core/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt b/payments-core/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt index 12e0f699b35..f82684e4d86 100644 --- a/payments-core/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt +++ b/payments-core/src/test/java/com/stripe/android/view/CardMultilineWidgetTest.kt @@ -330,35 +330,42 @@ internal class CardMultilineWidgetTest { @Test fun paymentMethodCreateParams_whenPostalCodeIsRequiredAndValueIsBlank_returnsNull() { cardMultilineWidget.setShouldShowPostalCode(true) + cardMultilineWidget.setCardInputListener(fullCardListener) cardMultilineWidget.postalCodeRequired = true fullGroup.cardNumberEditText.setText(VISA_WITH_SPACES) fullGroup.expiryDateEditText.append("12") fullGroup.expiryDateEditText.append("50") fullGroup.cvcEditText.append(CVC_VALUE_COMMON) + fullGroup.postalCodeEditText.setText("") assertThat(cardMultilineWidget.paymentMethodCreateParams) .isNull() + verify(fullCardListener, never()).onPostalCodeComplete() } @Test fun paymentMethodCreateParams_whenPostalCodeIsRequiredAndValueIsNotBlank_returnsNotNull() { cardMultilineWidget.setShouldShowPostalCode(true) - cardMultilineWidget.postalCodeRequired = false + cardMultilineWidget.setCardInputListener(fullCardListener) + cardMultilineWidget.postalCodeRequired = true fullGroup.cardNumberEditText.setText(VISA_WITH_SPACES) fullGroup.expiryDateEditText.append("12") fullGroup.expiryDateEditText.append("50") fullGroup.cvcEditText.append(CVC_VALUE_COMMON) + fullGroup.postalCodeEditText.setText("1234") assertThat(cardMultilineWidget.paymentMethodCreateParams) .isNotNull() + verify(fullCardListener).onPostalCodeComplete() } @Test fun paymentMethodCreateParams_whenPostalCodeIsNotRequiredAndValueIsBlank_returnsNotNull() { cardMultilineWidget.setShouldShowPostalCode(true) cardMultilineWidget.postalCodeRequired = false + cardMultilineWidget.setCardInputListener(fullCardListener) fullGroup.cardNumberEditText.setText(VISA_WITH_SPACES) fullGroup.expiryDateEditText.append("12") @@ -367,6 +374,7 @@ internal class CardMultilineWidgetTest { assertThat(cardMultilineWidget.paymentMethodCreateParams) .isNotNull() + verify(fullCardListener, never()).onPostalCodeComplete() } @Test @@ -586,6 +594,9 @@ internal class CardMultilineWidgetTest { verify(noZipCardListener).onFocusChange(CardInputListener.FocusField.ExpiryDate) assertThat(noZipGroup.expiryDateEditText.hasFocus()) .isTrue() + + verify(fullCardListener, never()).onPostalCodeComplete() + verify(noZipCardListener, never()).onPostalCodeComplete() } @Test @@ -606,6 +617,9 @@ internal class CardMultilineWidgetTest { verify(noZipCardListener).onFocusChange(CardInputListener.FocusField.Cvc) assertThat(noZipGroup.cvcEditText.hasFocus()) .isTrue() + + verify(fullCardListener, never()).onPostalCodeComplete() + verify(noZipCardListener, never()).onPostalCodeComplete() } @Test @@ -630,6 +644,9 @@ internal class CardMultilineWidgetTest { verify(noZipCardListener, never()).onFocusChange(CardInputListener.FocusField.PostalCode) assertThat(noZipGroup.cvcEditText.hasFocus()) .isTrue() + + verify(fullCardListener, never()).onPostalCodeComplete() + verify(noZipCardListener, never()).onPostalCodeComplete() } @Test @@ -646,6 +663,10 @@ internal class CardMultilineWidgetTest { .isTrue() assertThat(fullGroup.cardNumberEditText.text?.toString()) .isEqualTo(VISA_WITH_SPACES.take(VISA_WITH_SPACES.length - 1)) + + verify(fullCardListener, never()).onPostalCodeComplete() + + verify(fullCardListener, never()).onPostalCodeComplete() } @Test @@ -662,6 +683,8 @@ internal class CardMultilineWidgetTest { .isTrue() assertThat(noZipGroup.cardNumberEditText.text?.toString()) .isEqualTo(VISA_WITH_SPACES.take(VISA_WITH_SPACES.length - 1)) + + verify(noZipCardListener, never()).onPostalCodeComplete() } @Test @@ -694,6 +717,9 @@ internal class CardMultilineWidgetTest { .isTrue() assertThat(noZipGroup.expiryDateEditText.fieldText) .isEqualTo("12/5") + + verify(fullCardListener, never()).onPostalCodeComplete() + verify(noZipCardListener, never()).onPostalCodeComplete() } @Test @@ -805,6 +831,7 @@ internal class CardMultilineWidgetTest { @Test fun usZipCodeRequired_whenFalse_shouldSetPostalCodeHint() { cardMultilineWidget.usZipCodeRequired = false + cardMultilineWidget.setCardInputListener(fullCardListener) assertThat(cardMultilineWidget.postalInputLayout.hint) .isEqualTo("Postal code") @@ -826,11 +853,14 @@ internal class CardMultilineWidgetTest { .build() ) ) + + verify(fullCardListener, never()).onPostalCodeComplete() } @Test fun usZipCodeRequired_whenTrue_withInvalidZipCode_shouldReturnNullCard() { cardMultilineWidget.usZipCodeRequired = true + cardMultilineWidget.setCardInputListener(fullCardListener) assertThat(cardMultilineWidget.postalInputLayout.hint) .isEqualTo("ZIP Code") @@ -843,11 +873,14 @@ internal class CardMultilineWidgetTest { fullGroup.postalCodeEditText.setText("1234") assertThat(cardMultilineWidget.cardParams) .isNull() + + verify(fullCardListener, never()).onPostalCodeComplete() } @Test fun usZipCodeRequired_whenTrue_withValidZipCode_shouldReturnNotNullCard() { cardMultilineWidget.usZipCodeRequired = true + cardMultilineWidget.setCardInputListener(fullCardListener) assertThat(cardMultilineWidget.postalInputLayout.hint) .isEqualTo("ZIP Code") @@ -872,6 +905,8 @@ internal class CardMultilineWidgetTest { .build() ) ) + + verify(fullCardListener).onPostalCodeComplete() } @Test diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/CardDataCollectionFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/CardDataCollectionFragment.kt index 7e1c253f54b..4fdda482adc 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/CardDataCollectionFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/paymentdatacollection/CardDataCollectionFragment.kt @@ -126,6 +126,8 @@ internal class CardDataCollectionFragment> // move to first field when CVC is complete billingAddressView.focusFirstField() } + + override fun onPostalCodeComplete() {} }) sheetViewModel.processing.observe(viewLifecycleOwner) { isProcessing ->