diff --git a/stripe/src/main/java/com/stripe/android/cards/CardNumber.kt b/stripe/src/main/java/com/stripe/android/cards/CardNumber.kt index 5aad5ca0a7f..5aee83afe40 100644 --- a/stripe/src/main/java/com/stripe/android/cards/CardNumber.kt +++ b/stripe/src/main/java/com/stripe/android/cards/CardNumber.kt @@ -93,7 +93,7 @@ internal sealed class CardNumber { internal fun getSpacePositions(panLength: Int) = SPACE_POSITIONS[panLength] ?: DEFAULT_SPACE_POSITIONS - private const val MIN_PAN_LENGTH = 14 + internal const val MIN_PAN_LENGTH = 14 internal const val MAX_PAN_LENGTH = 19 internal const val DEFAULT_PAN_LENGTH = 16 private val DEFAULT_SPACE_POSITIONS = setOf(4, 9, 14) diff --git a/stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt b/stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt index ff2b90ddd94..fa2bc9720b0 100644 --- a/stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt +++ b/stripe/src/main/java/com/stripe/android/view/CardInputWidget.kt @@ -892,7 +892,7 @@ class CardInputWidget @JvmOverloads constructor( panLength: Int ): String { val formattedNumber = CardNumber.Unvalidated( - List(panLength) { "0" }.joinToString(separator = "") + "0".repeat(panLength) ).getFormatted(panLength) return formattedNumber.take( @@ -1109,7 +1109,7 @@ class CardInputWidget @JvmOverloads constructor( 14 -> 2 else -> 4 }.let { peekSize -> - List(peekSize) { "0" }.joinToString(separator = "") + "0".repeat(peekSize) } } diff --git a/stripe/src/main/java/com/stripe/android/view/CardNumberEditText.kt b/stripe/src/main/java/com/stripe/android/view/CardNumberEditText.kt index 115dcf3e18a..418d79329ba 100644 --- a/stripe/src/main/java/com/stripe/android/view/CardNumberEditText.kt +++ b/stripe/src/main/java/com/stripe/android/view/CardNumberEditText.kt @@ -173,8 +173,8 @@ class CardNumberEditText internal constructor( } @JvmSynthetic - internal fun updateLengthFilter() { - filters = arrayOf(InputFilter.LengthFilter(formattedPanLength)) + internal fun updateLengthFilter(maxLength: Int = formattedPanLength) { + filters = arrayOf(InputFilter.LengthFilter(maxLength)) } /** @@ -185,13 +185,15 @@ class CardNumberEditText internal constructor( * @param editActionStart the position in the string at which the edit action starts * @param editActionAddition the number of new characters going into the string (zero for * delete) + * @param panLength the maximum normalized length of the PAN * @return an index within the string at which to put the cursor */ @JvmSynthetic internal fun updateSelectionIndex( newLength: Int, editActionStart: Int, - editActionAddition: Int + editActionAddition: Int, + panLength: Int = this.panLength ): Int { var gapsJumped = 0 val gapSet = CardNumber.getSpacePositions(panLength) @@ -232,8 +234,11 @@ class CardNumberEditText internal constructor( private var beforeCardNumber = unvalidatedCardNumber + private var isPastedPan = false + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { if (!ignoreChanges) { + isPastedPan = false beforeCardNumber = unvalidatedCardNumber latestChangeStart = start @@ -249,13 +254,26 @@ class CardNumberEditText internal constructor( val cardNumber = CardNumber.Unvalidated(s?.toString().orEmpty()) updateAccountRange(cardNumber) - val formattedNumber = cardNumber.getFormatted(panLength) - this.newCursorPosition = updateSelectionIndex( - formattedNumber.length, - latestChangeStart, - latestInsertionSize - ) - this.formattedNumber = formattedNumber + isPastedPan = isPastedPan(start, cardNumber) + + if (isPastedPan) { + updateLengthFilter(cardNumber.getFormatted(cardNumber.length).length) + } + + if (isPastedPan) { + cardNumber.length + } else { + panLength + }.let { maxPanLength -> + val formattedNumber = cardNumber.getFormatted(maxPanLength) + newCursorPosition = updateSelectionIndex( + formattedNumber.length, + latestChangeStart, + latestInsertionSize, + maxPanLength + ) + this.formattedNumber = formattedNumber + } } override fun afterTextChanged(s: Editable?) { @@ -310,6 +328,10 @@ class CardNumberEditText internal constructor( unvalidatedCardNumber.isMaxLength || (isValid && accountRange != null) ) + + private fun isPastedPan(start: Int, cardNumber: CardNumber.Unvalidated): Boolean { + return start == 0 && cardNumber.normalized.length >= CardNumber.MIN_PAN_LENGTH + } } ) } diff --git a/stripe/src/test/java/com/stripe/android/view/CardNumberEditTextTest.kt b/stripe/src/test/java/com/stripe/android/view/CardNumberEditTextTest.kt index 92eb4fa7bf4..22b27b323ed 100644 --- a/stripe/src/test/java/com/stripe/android/view/CardNumberEditTextTest.kt +++ b/stripe/src/test/java/com/stripe/android/view/CardNumberEditTextTest.kt @@ -274,6 +274,26 @@ internal class CardNumberEditTextTest { .isEqualTo(1) } + @Test + fun `when 19 digit PAN is pasted, full PAN is accepted and formatted`() { + val cardNumberEditText = CardNumberEditText( + context, + workDispatcher = testDispatcher, + cardAccountRangeRepository = NullCardAccountRangeRepository(), + staticCardAccountRanges = object : StaticCardAccountRanges { + override fun match( + cardNumber: CardNumber.Unvalidated + ): AccountRange? = null + } + ) + + cardNumberEditText.setText("6216828050000000000") + idleLooper() + + assertThat(cardNumberEditText.fieldText) + .isEqualTo("6216 8280 5000 0000 000") + } + @Test fun `updating text with null account range should format text correctly but not set card brand`() { val cardNumberEditText = CardNumberEditText(