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

Add CardValidCallback and add support in card forms #2093

Merged
merged 1 commit into from
Jan 23, 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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.stripe.android.PaymentConfiguration
import com.stripe.android.Stripe
import com.stripe.android.model.PaymentMethod
import com.stripe.android.view.CardValidCallback
import com.stripe.example.R
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
Expand Down Expand Up @@ -40,6 +41,15 @@ class CreateCardPaymentMethodActivity : AppCompatActivity() {
rv_payment_methods.layoutManager = LinearLayoutManager(this)
rv_payment_methods.adapter = adapter

card_multiline_widget.setCardValidCallback(object : CardValidCallback {
override fun onInputChanged(
isValid: Boolean,
invalidFields: Set<CardValidCallback.Fields>
) {
// added as an example - no-op
}
})

btn_create_payment_method.setOnClickListener { createPaymentMethod() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.stripe.android.PaymentConfiguration
import com.stripe.android.Stripe
import com.stripe.android.model.Card
import com.stripe.android.model.Token
import com.stripe.android.view.CardValidCallback
import com.stripe.example.R
import kotlinx.android.synthetic.main.card_token_activity.*

Expand Down Expand Up @@ -60,6 +61,15 @@ class CreateCardTokenActivity : AppCompatActivity() {
}
}

card_input_widget.setCardValidCallback(object : CardValidCallback {
override fun onInputChanged(
isValid: Boolean,
invalidFields: Set<CardValidCallback.Fields>
) {
// added as an example - no-op
}
})

card_input_widget.requestFocus()
}

Expand Down
35 changes: 35 additions & 0 deletions stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.Layout
import android.text.TextPaint
import android.text.TextWatcher
Expand Down Expand Up @@ -62,6 +63,27 @@ class CardInputWidget @JvmOverloads constructor(
private val postalCodeEditText: PostalCodeEditText

private var cardInputListener: CardInputListener? = null
private var cardValidCallback: CardValidCallback? = null
private val cardValidTextWatcher = object : StripeTextWatcher() {
override fun afterTextChanged(s: Editable?) {
super.afterTextChanged(s)
cardValidCallback?.onInputChanged(invalidFields.isEmpty(), invalidFields)
}
}
private val invalidFields: Set<CardValidCallback.Fields>
get() {
return listOfNotNull(
CardValidCallback.Fields.Number.takeIf {
cardNumberEditText.cardNumber == null
},
CardValidCallback.Fields.Expiry.takeIf {
expiryDateEditText.validDateFields == null
},
CardValidCallback.Fields.Cvc.takeIf {
this.cvcValue == null
}
).toSet()
}

@JvmSynthetic
internal var cardNumberIsViewed = true
Expand Down Expand Up @@ -252,6 +274,19 @@ class CardInputWidget @JvmOverloads constructor(
initView(attrs)
}

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

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

// call immediately after setting
cardValidCallback?.onInputChanged(invalidFields.isEmpty(), invalidFields)
}

/**
* Set a [CardInputListener] to be notified of card input events.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.View
Expand Down Expand Up @@ -52,6 +53,25 @@ class CardMultilineWidget @JvmOverloads constructor(
private val postalInputLayout: TextInputLayout

private var cardInputListener: CardInputListener? = null
private var cardValidCallback: CardValidCallback? = null
private val cardValidTextWatcher = object : StripeTextWatcher() {
override fun afterTextChanged(s: Editable?) {
super.afterTextChanged(s)
cardValidCallback?.onInputChanged(invalidFields.isEmpty(), invalidFields)
}
}
private val invalidFields: Set<CardValidCallback.Fields>
get() {
return listOfNotNull(
CardValidCallback.Fields.Number.takeIf {
cardNumber == null
},
CardValidCallback.Fields.Expiry.takeIf {
expiryDate == null
},
CardValidCallback.Fields.Cvc.takeUnless { isCvcLengthValid }
).toSet()
}

private var isEnabled: Boolean = false
private var hasAdjustedDrawable: Boolean = false
Expand All @@ -75,7 +95,7 @@ class CardMultilineWidget @JvmOverloads constructor(
return if (validateAllFields()) {
expiryDateEditText.validDateFields?.let { (month, year) ->
PaymentMethodCreateParams.Card(
number = cardNumberEditText.cardNumber,
number = cardNumber,
cvc = cvcEditText.text?.toString(),
expiryMonth = month,
expiryYear = year
Expand Down Expand Up @@ -139,7 +159,7 @@ class CardMultilineWidget @JvmOverloads constructor(
return null
}

val cardNumber = cardNumberEditText.cardNumber
val cardNumber = cardNumber
val cardDate = requireNotNull(expiryDateEditText.validDateFields)
val cvcValue = cvcEditText.text?.toString()
val postalCode = postalCodeEditText.text?.toString()
Expand All @@ -150,10 +170,24 @@ class CardMultilineWidget @JvmOverloads constructor(
.loggingTokens(listOf(CARD_MULTILINE_TOKEN))
}

private val cardNumber: String?
get() {
return cardNumberEditText.cardNumber
}
private val expiryDate: Pair<Int, Int>?
get() {
return expiryDateEditText.validDateFields
}
private val isCvcLengthValid: Boolean
get() {
return cardBrand.isValidCvc(cvcEditText.rawCvcValue)
}
private val allFields: Collection<StripeEditText>
get() {
return listOf(
cardNumberEditText, expiryDateEditText, cvcEditText, postalCodeEditText
)
}

private val cvcHelperText: Int
@StringRes
Expand Down Expand Up @@ -279,6 +313,19 @@ class CardMultilineWidget @JvmOverloads constructor(
this.cardInputListener = listener
}

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

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

// call immediately after setting
cardValidCallback?.onInputChanged(invalidFields.isEmpty(), invalidFields)
}

override fun setCardHint(cardHint: String) {
cardHintText = cardHint
}
Expand All @@ -289,22 +336,14 @@ class CardMultilineWidget @JvmOverloads constructor(
* @return `true` if all shown fields are valid, `false` otherwise
*/
fun validateAllFields(): Boolean {
val cardNumberIsValid = CardUtils.isValidCardNumber(cardNumberEditText.cardNumber)
val expiryIsValid = expiryDateEditText.validDateFields != null
val cardNumberIsValid = CardUtils.isValidCardNumber(cardNumber)
val expiryIsValid = expiryDate != null
val cvcIsValid = isCvcLengthValid
cardNumberEditText.shouldShowError = !cardNumberIsValid
expiryDateEditText.shouldShowError = !expiryIsValid
cvcEditText.shouldShowError = !cvcIsValid

val fields = listOf(
cardNumberEditText, expiryDateEditText, cvcEditText, postalCodeEditText
)
for (field in fields) {
if (field.shouldShowError) {
field.requestFocus()
break
}
}
allFields.firstOrNull { it.shouldShowError }?.requestFocus()

return cardNumberIsValid && expiryIsValid && cvcIsValid
}
Expand Down Expand Up @@ -353,7 +392,7 @@ class CardMultilineWidget @JvmOverloads constructor(
* Checks whether the current card number is valid
*/
fun validateCardNumber(): Boolean {
val cardNumberIsValid = CardUtils.isValidCardNumber(cardNumberEditText.cardNumber)
val cardNumberIsValid = CardUtils.isValidCardNumber(cardNumber)
cardNumberEditText.shouldShowError = !cardNumberIsValid
return cardNumberIsValid
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.stripe.android.view

interface CardValidCallback {
/**
* @param isValid if the current input is valid
* @param invalidFields if the current input is invalid, this [Set] will be populated with the
* fields that are invalid, represented by [Fields]; if the current input is valid,
* this [Set] will be empty
*/
fun onInputChanged(isValid: Boolean, invalidFields: Set<Fields>)

enum class Fields {
Number,
Expiry,
Cvc
}
}
2 changes: 2 additions & 0 deletions stripe/src/main/java/com/stripe/android/view/CardWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal interface CardWidget {
*/
val paymentMethodCreateParams: PaymentMethodCreateParams?

fun setCardValidCallback(callback: CardValidCallback?)

fun setCardInputListener(listener: CardInputListener?)

fun setCardHint(cardHint: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Build
import android.text.TextPaint
import android.view.ViewGroup
import android.widget.ImageView
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.verify
import com.stripe.android.CardNumberFixtures.VALID_AMEX_NO_SPACES
Expand All @@ -14,7 +15,6 @@ import com.stripe.android.CardNumberFixtures.VALID_VISA_NO_SPACES
import com.stripe.android.CardNumberFixtures.VALID_VISA_WITH_SPACES
import com.stripe.android.R
import com.stripe.android.model.Address
import com.stripe.android.model.Card
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
Expand All @@ -36,8 +36,6 @@ import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

Expand Down Expand Up @@ -66,14 +64,10 @@ internal class CardInputWidgetTest : BaseViewTest<CardInputTestActivity>(
private val postalCodeEditText: PostalCodeEditText by lazy {
cardInputWidget.findViewById<PostalCodeEditText>(R.id.et_postal_code)
}

@Mock
private lateinit var cardInputListener: CardInputListener
private val cardInputListener: CardInputListener = mock()

@BeforeTest
fun setup() {
MockitoAnnotations.initMocks(this)

cardInputWidget.setCardNumberTextWatcher(object : StripeTextWatcher() {})
cardInputWidget.setExpiryDateTextWatcher(object : StripeTextWatcher() {})
cardInputWidget.setCvcNumberTextWatcher(object : StripeTextWatcher() {})
Expand Down Expand Up @@ -1229,6 +1223,70 @@ internal class CardInputWidgetTest : BaseViewTest<CardInputTestActivity>(
assertNotEquals(cardInputWidget.standardFields, cardInputWidget.allFields)
}

@Test
fun testCardValidCallback() {
var currentIsValid = false
var currentInvalidFields = emptySet<CardValidCallback.Fields>()
cardInputWidget.setCardValidCallback(object : CardValidCallback {
override fun onInputChanged(
isValid: Boolean,
invalidFields: Set<CardValidCallback.Fields>
) {
currentIsValid = isValid
currentInvalidFields = invalidFields
}
})

assertFalse(currentIsValid)
assertEquals(
setOf(
CardValidCallback.Fields.Number,
CardValidCallback.Fields.Expiry,
CardValidCallback.Fields.Cvc
),
currentInvalidFields
)

cardInputWidget.setCardNumber(VALID_VISA_NO_SPACES)
assertFalse(currentIsValid)
assertEquals(
setOf(CardValidCallback.Fields.Expiry, CardValidCallback.Fields.Cvc),
currentInvalidFields
)

expiryEditText.append("12")
assertFalse(currentIsValid)
assertEquals(
setOf(CardValidCallback.Fields.Expiry, CardValidCallback.Fields.Cvc),
currentInvalidFields
)

expiryEditText.append("50")
assertFalse(currentIsValid)
assertEquals(
setOf(CardValidCallback.Fields.Cvc),
currentInvalidFields
)

cvcEditText.append("12")
assertFalse(currentIsValid)
assertEquals(
setOf(CardValidCallback.Fields.Cvc),
currentInvalidFields
)

cvcEditText.append("3")
assertTrue(currentIsValid)
assertTrue(currentInvalidFields.isEmpty())

cvcEditText.setText("0")
assertFalse(currentIsValid)
assertEquals(
setOf(CardValidCallback.Fields.Cvc),
currentInvalidFields
)
}

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