diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b549fd..3143ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,23 @@ # Android Card Form Release Notes ## unreleased + * Bump `compileSdkVersion` and `targetSdkVersion` to API level 33 * Move all classes to `com.braintreepayments.api` package + * Remove Jetifier now that AndroidX is fully supported + * Make `CardType` a pure enum and make card parsing logic internal + * Breaking Changes + * The following `CardType` methods are no longer supported: + * `getPattern()` + * `getRelaxedPrefixPattern()` + * `getFrontResource()` + * `getSecurityCodeName()` + * `getSecurityCodeLength()` + * `getMinCardLength()` + * `getMaxCardLength()` + * `getSpaceIndices()` + * `isLuhnValid()` + * `validate()` + * `CardType#forCardNumber()` ## 5.4.0 * Update Visa card icon diff --git a/CardForm/build.gradle b/CardForm/build.gradle index 6b7d37f..1f9281e 100644 --- a/CardForm/build.gradle +++ b/CardForm/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'kotlin-android' id 'de.marcphilipp.nexus-publish' id 'signing' } @@ -28,7 +29,7 @@ android { } dependencies { - implementation 'com.google.android.material:material:1.0.0' + implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' diff --git a/CardForm/src/main/java/com/braintreepayments/api/CardAttributes.kt b/CardForm/src/main/java/com/braintreepayments/api/CardAttributes.kt new file mode 100644 index 0000000..2aed769 --- /dev/null +++ b/CardForm/src/main/java/com/braintreepayments/api/CardAttributes.kt @@ -0,0 +1,171 @@ +package com.braintreepayments.api + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.braintreepayments.api.cardform.R +import java.util.regex.Pattern + +/** + * @property pattern The regex matching this card type. + * @property relaxedPrefixPattern The relaxed prefix regex matching this card type. To be used in determining card type if no pattern matches. + * @property frontResource The android resource id for the front card image, highlighting card number format. + * @property securityCodeName The android resource id for the security code name for this card type. + * @property securityCodeLength The length of the current card's security code. + * @property minCardLength minimum length of a card for this {@link CardType} + * @property maxCardLength max length of a card for this {@link CardType} + * @property spaceIndices the locations where spaces should be inserted when formatting the card in a user friendly way. Only for display purposes. + */ +internal data class CardAttributes constructor( + val cardType: CardType, + @DrawableRes val frontResource: Int, + val maxCardLength: Int, + val minCardLength: Int, + val pattern: Pattern, + val relaxedPrefixPattern: Pattern? = null, + val securityCodeLength: Int, + @StringRes val securityCodeName: Int, +) { + + val spaceIndices: IntArray = + if (cardType == CardType.AMEX) AMEX_SPACE_INDICES else DEFAULT_SPACE_INDICES + + fun matches(cardNumber: String) = matchesStrict(cardNumber) || matchesRelaxed(cardNumber) + fun matchesStrict(cardNumber: String) = pattern.matcher(cardNumber).matches() + fun matchesRelaxed(cardNumber: String) = + relaxedPrefixPattern?.matcher(cardNumber)?.matches() ?: false + + companion object { + + private val AMEX_SPACE_INDICES = intArrayOf(4, 10) + private val DEFAULT_SPACE_INDICES = intArrayOf(4, 8, 12) + + @JvmField + val EMPTY = CardAttributes( + cardType = CardType.EMPTY, + pattern = Pattern.compile("^$"), + frontResource = R.drawable.bt_ic_unknown, + minCardLength = 12, + maxCardLength = 19, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvv, + ) + + val UNKNOWN = CardAttributes( + cardType = CardType.UNKNOWN, + pattern = Pattern.compile("\\d+"), + frontResource = R.drawable.bt_ic_unknown, + minCardLength = 12, + maxCardLength = 19, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvv, + ) + + private val knownCardBrandAttributes = createCardAttributeMap( + CardAttributes( + cardType = CardType.HIPERCARD, + pattern = Pattern.compile("^606282\\d*"), + frontResource = R.drawable.bt_ic_hipercard, + minCardLength = 16, + maxCardLength = 16, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvc, + ), + CardAttributes( + cardType = CardType.HIPER, + pattern = Pattern.compile("^637(095|568|599|609|612)\\d*"), + frontResource = R.drawable.bt_ic_hiper, + minCardLength = 16, + maxCardLength = 16, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvc, + ), + CardAttributes( + cardType = CardType.UNIONPAY, + pattern = Pattern.compile("^62\\d*"), + frontResource = R.drawable.bt_ic_unionpay, + minCardLength = 16, + maxCardLength = 19, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvn, + ), + CardAttributes( + cardType = CardType.VISA, + pattern = Pattern.compile("^4\\d*"), + frontResource = R.drawable.bt_ic_visa, + minCardLength = 16, + maxCardLength = 16, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvv, + ), + CardAttributes( + cardType = CardType.MASTERCARD, + pattern = Pattern.compile("^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[0-1]|2720)\\d*"), + frontResource = R.drawable.bt_ic_mastercard, + minCardLength = 16, + maxCardLength = 16, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvc, + ), + CardAttributes( + cardType = CardType.DISCOVER, + pattern = Pattern.compile("^(6011|65|64[4-9]|622)\\d*"), + frontResource = R.drawable.bt_ic_discover, + minCardLength = 16, + maxCardLength = 19, + securityCodeLength = 3, + securityCodeName = R.string.bt_cid, + ), + CardAttributes( + cardType = CardType.AMEX, + pattern = Pattern.compile("^3[47]\\d*"), + frontResource = R.drawable.bt_ic_amex, + minCardLength = 15, + maxCardLength = 15, + securityCodeLength = 4, + securityCodeName = R.string.bt_cid, + ), + CardAttributes( + cardType = CardType.DINERS_CLUB, + pattern = Pattern.compile("^(36|38|30[0-5])\\d*"), + frontResource = R.drawable.bt_ic_diners_club, + minCardLength = 14, + maxCardLength = 14, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvv, + ), + CardAttributes( + cardType = CardType.JCB, + pattern = Pattern.compile("^35\\d*"), + frontResource = R.drawable.bt_ic_jcb, + minCardLength = 16, + maxCardLength = 16, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvv, + ), + CardAttributes( + cardType = CardType.MAESTRO, + pattern = Pattern.compile("^(5018|5020|5038|5043|5[6-9]|6020|6304|6703|6759|676[1-3])\\d*"), + frontResource = R.drawable.bt_ic_maestro, + minCardLength = 12, + maxCardLength = 19, + securityCodeLength = 3, + securityCodeName = R.string.bt_cvc, + relaxedPrefixPattern = Pattern.compile("^6\\d*") + ) + ) + + val allKnownCardBrands = knownCardBrandAttributes.values + + private fun createCardAttributeMap(vararg items: CardAttributes): Map { + val result = mutableMapOf() + for (item in items) { + result[item.cardType] = item + } + return result + } + + @JvmStatic + fun forCardType(cardType: CardType): CardAttributes = + knownCardBrandAttributes[cardType] ?: UNKNOWN + } +} \ No newline at end of file diff --git a/CardForm/src/main/java/com/braintreepayments/api/CardEditText.java b/CardForm/src/main/java/com/braintreepayments/api/CardEditText.java index 48e0d49..90b2b75 100644 --- a/CardForm/src/main/java/com/braintreepayments/api/CardEditText.java +++ b/CardForm/src/main/java/com/braintreepayments/api/CardEditText.java @@ -24,11 +24,13 @@ public interface OnCardTypeChangedListener { void onCardTypeChanged(CardType cardType); } - private boolean mDisplayCardIcon = true; - private boolean mMask = false; - private CardType mCardType; - private OnCardTypeChangedListener mOnCardTypeChangedListener; - private TransformationMethod mSavedTranformationMethod; + private boolean displayCardIcon = true; + private boolean mask = false; + private CardAttributes cardAttributes = CardAttributes.EMPTY; + private OnCardTypeChangedListener onCardTypeChangedListener; + private TransformationMethod savedTransformationMethod; + + private CardParser cardParser = new CardParser(); public CardEditText(Context context) { super(context); @@ -50,7 +52,7 @@ private void init() { setCardIcon(R.drawable.bt_ic_unknown); addTextChangedListener(this); updateCardType(); - mSavedTranformationMethod = getTransformationMethod(); + savedTransformationMethod = getTransformationMethod(); } /** @@ -61,9 +63,9 @@ private void init() { * type icons. */ public void displayCardTypeIcon(boolean display) { - mDisplayCardIcon = display; + displayCardIcon = display; - if (!mDisplayCardIcon) { + if (!displayCardIcon) { setCardIcon(-1); } } @@ -73,7 +75,7 @@ public void displayCardTypeIcon(boolean display) { * the {@link android.widget.EditText} */ public CardType getCardType() { - return mCardType; + return cardAttributes.getCardType(); } /** @@ -82,7 +84,7 @@ public CardType getCardType() { * something like "4111111111111111" to "•••• 1111". */ public void setMask(boolean mask) { - mMask = mask; + this.mask = mask; } @Override @@ -95,7 +97,7 @@ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFoc if (getText().toString().length() > 0) { setSelection(getText().toString().length()); } - } else if (mMask && isValid()) { + } else if (mask && isValid()) { maskNumber(); } } @@ -106,7 +108,7 @@ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFoc * changes */ public void setOnCardTypeChangedListener(OnCardTypeChangedListener listener) { - mOnCardTypeChangedListener = listener; + onCardTypeChangedListener = listener; } @Override @@ -117,11 +119,11 @@ public void afterTextChanged(Editable editable) { } updateCardType(); - setCardIcon(mCardType.getFrontResource()); - addSpans(editable, mCardType.getSpaceIndices()); + setCardIcon(cardAttributes.getFrontResource()); + addSpans(editable, cardAttributes.getSpaceIndices()); - if (mCardType.getMaxCardLength() == getSelectionStart()) { + if (cardAttributes.getMaxCardLength() == getSelectionStart()) { validate(); if (isValid()) { @@ -130,7 +132,7 @@ public void afterTextChanged(Editable editable) { unmaskNumber(); } } else if (!hasFocus()) { - if (mMask) { + if (mask) { maskNumber(); } } @@ -138,7 +140,7 @@ public void afterTextChanged(Editable editable) { @Override public boolean isValid() { - return isOptional() || mCardType.validate(getText().toString()); + return isOptional() || cardParser.validate(getText().toString()); } @Override @@ -152,29 +154,30 @@ public String getErrorMessage() { private void maskNumber() { if (!(getTransformationMethod() instanceof CardNumberTransformation)) { - mSavedTranformationMethod = getTransformationMethod(); + savedTransformationMethod = getTransformationMethod(); setTransformationMethod(new CardNumberTransformation()); } } private void unmaskNumber() { - if (getTransformationMethod() != mSavedTranformationMethod) { - setTransformationMethod(mSavedTranformationMethod); + if (getTransformationMethod() != savedTransformationMethod) { + setTransformationMethod(savedTransformationMethod); } } private void updateCardType() { - CardType type = CardType.forCardNumber(getText().toString()); - if (mCardType != type) { - mCardType = type; + CardAttributes attrs = cardParser.parseCardAttributes(getText().toString()); + + if (cardAttributes.getCardType() != attrs.getCardType()) { + cardAttributes = attrs; - InputFilter[] filters = { new LengthFilter(mCardType.getMaxCardLength()) }; + InputFilter[] filters = { new LengthFilter(cardAttributes.getMaxCardLength()) }; setFilters(filters); invalidate(); - if (mOnCardTypeChangedListener != null) { - mOnCardTypeChangedListener.onCardTypeChanged(mCardType); + if (onCardTypeChangedListener != null) { + onCardTypeChangedListener.onCardTypeChanged(cardAttributes.getCardType()); } } } @@ -190,7 +193,7 @@ private void addSpans(Editable editable, int[] spaceIndices) { } private void setCardIcon(int icon) { - if (!mDisplayCardIcon || getText().length() == 0) { + if (!displayCardIcon || getText().length() == 0) { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, 0, 0, 0, 0); } else { TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, 0, 0, icon, 0); diff --git a/CardForm/src/main/java/com/braintreepayments/api/CardParser.kt b/CardForm/src/main/java/com/braintreepayments/api/CardParser.kt new file mode 100644 index 0000000..3f704a4 --- /dev/null +++ b/CardForm/src/main/java/com/braintreepayments/api/CardParser.kt @@ -0,0 +1,68 @@ +package com.braintreepayments.api + +import android.text.TextUtils + +internal class CardParser { + + fun parseCardAttributes(cardNumber: String): CardAttributes { + val cardAttributes = + findStrictCardTypeMatch(cardNumber) ?: findRelaxedCardTypeMatch(cardNumber) + + return cardAttributes + ?: (if (cardNumber.isEmpty()) CardAttributes.EMPTY else CardAttributes.UNKNOWN) + } + + /** + * Returns the card type matching this account, or [CardType.UNKNOWN] for no match. + * + * A partial account type may be given, with the caveat that it may not have enough digits to match. + */ + fun parseCardType(cardNumber: String): CardType = parseCardAttributes(cardNumber).cardType + + private fun findStrictCardTypeMatch(cardNumber: String) = + CardAttributes.allKnownCardBrands.find { it.matchesStrict(cardNumber) } + + private fun findRelaxedCardTypeMatch(cardNumber: String) = + CardAttributes.allKnownCardBrands.find { it.matchesRelaxed(cardNumber) } + + /** + * Performs the Luhn check on the given card number. + * + * @param cardNumber a String consisting of numeric digits (only). + * @return `true` if the sequence passes the checksum + * @throws IllegalArgumentException if `cardNumber` contained a non-digit (where [Character.isDigit] is `false`). + * @see [Luhn Algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm) + */ + fun isLuhnValid(cardNumber: String?): Boolean { + val reversed = (cardNumber ?: "").reversed() + val len = reversed.length + var oddSum = 0 + var evenSum = 0 + for (i in 0 until len) { + val c = reversed[i] + val digit = + requireNotNull(c.digitToIntOrNull()) { String.format("Not a digit: '%s'", c) } + if (i % 2 == 0) { + oddSum += digit + } else { + evenSum += digit / 5 + (2 * digit) % 10 + } + } + return (oddSum + evenSum) % 10 == 0 + } + + /** + * @param cardNumber The card number to validate. + * @return `true` if this card number is locally valid. + */ + fun validate(cardNumber: String): Boolean { + if (TextUtils.isEmpty(cardNumber) || !TextUtils.isDigitsOnly(cardNumber)) { + return false + } + val cardAttributes = parseCardAttributes(cardNumber) + val isValidLength = cardAttributes.run { + (minCardLength..maxCardLength).contains(cardNumber.length) + } + return isValidLength && cardAttributes.matches(cardNumber) && isLuhnValid(cardNumber) + } +} \ No newline at end of file diff --git a/CardForm/src/main/java/com/braintreepayments/api/CardType.java b/CardForm/src/main/java/com/braintreepayments/api/CardType.java index d74de30..57e5977 100644 --- a/CardForm/src/main/java/com/braintreepayments/api/CardType.java +++ b/CardForm/src/main/java/com/braintreepayments/api/CardType.java @@ -11,228 +11,16 @@ */ public enum CardType { - VISA("^4\\d*", - R.drawable.bt_ic_visa, - 16, 16, - 3, R.string.bt_cvv, null), - MASTERCARD("^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[0-1]|2720)\\d*", - R.drawable.bt_ic_mastercard, - 16, 16, - 3, R.string.bt_cvc, null), - DISCOVER("^(6011|65|64[4-9]|622)\\d*", - R.drawable.bt_ic_discover, - 16, 19, - 3, R.string.bt_cid, null), - AMEX("^3[47]\\d*", - R.drawable.bt_ic_amex, - 15, 15, - 4, R.string.bt_cid, null), - DINERS_CLUB("^(36|38|30[0-5])\\d*", - R.drawable.bt_ic_diners_club, - 14, 14, - 3, R.string.bt_cvv, null), - JCB("^35\\d*", - R.drawable.bt_ic_jcb, - 16, 16, - 3, R.string.bt_cvv, null), - MAESTRO("^(5018|5020|5038|5043|5[6-9]|6020|6304|6703|6759|676[1-3])\\d*", - R.drawable.bt_ic_maestro, - 12, 19, - 3, R.string.bt_cvc, - "^6\\d*"), - UNIONPAY("^62\\d*", - R.drawable.bt_ic_unionpay, - 16, 19, - 3, R.string.bt_cvn, null), - HIPER("^637(095|568|599|609|612)\\d*", - R.drawable.bt_ic_hiper, - 16, 16, - 3, R.string.bt_cvc, null), - HIPERCARD("^606282\\d*", - R.drawable.bt_ic_hipercard, - 16, 16, - 3, R.string.bt_cvc, null), - UNKNOWN("\\d+", - R.drawable.bt_ic_unknown, - 12, 19, - 3, R.string.bt_cvv, null), - EMPTY("^$", - R.drawable.bt_ic_unknown, - 12, 19, - 3, R.string.bt_cvv, null); - - private static final int[] AMEX_SPACE_INDICES = { 4, 10 }; - private static final int[] DEFAULT_SPACE_INDICES = { 4, 8, 12 }; - - private final Pattern mPattern; - private final Pattern mRelaxedPrefixPattern; - private final int mFrontResource; - private final int mMinCardLength; - private final int mMaxCardLength; - private final int mSecurityCodeLength; - private final int mSecurityCodeName; - - CardType(String regex, int frontResource, int minCardLength, int maxCardLength, int securityCodeLength, - int securityCodeName, String relaxedPrefixPattern) { - mPattern = Pattern.compile(regex); - mRelaxedPrefixPattern = relaxedPrefixPattern == null ? null : Pattern.compile(relaxedPrefixPattern); - mFrontResource = frontResource; - mMinCardLength = minCardLength; - mMaxCardLength = maxCardLength; - mSecurityCodeLength = securityCodeLength; - mSecurityCodeName = securityCodeName; - } - - /** - * Returns the card type matching this account, or {@link CardType#UNKNOWN} - * for no match. - *

- * A partial account type may be given, with the caveat that it may not have enough digits to - * match. - */ - public static CardType forCardNumber(String cardNumber) { - CardType patternMatch = forCardNumberPattern(cardNumber); - if (patternMatch != EMPTY && patternMatch != UNKNOWN) { - return patternMatch; - } - - CardType relaxedPrefixPatternMatch = forCardNumberRelaxedPrefixPattern(cardNumber); - if (relaxedPrefixPatternMatch != EMPTY && relaxedPrefixPatternMatch != UNKNOWN) { - return relaxedPrefixPatternMatch; - } - - if (!cardNumber.isEmpty()) { - return UNKNOWN; - } - - return EMPTY; - } - - private static CardType forCardNumberPattern(String cardNumber) { - for (CardType cardType : values()) { - if (cardType.getPattern().matcher(cardNumber).matches()) { - return cardType; - } - } - - return EMPTY; - } - - private static CardType forCardNumberRelaxedPrefixPattern(String cardNumber) { - for (CardType cardTypeRelaxed : values()) { - if (cardTypeRelaxed.getRelaxedPrefixPattern() != null) { - if (cardTypeRelaxed.getRelaxedPrefixPattern().matcher(cardNumber).matches()) { - return cardTypeRelaxed; - } - } - } - - return EMPTY; - } - - /** - * @return The regex matching this card type. - */ - public Pattern getPattern() { - return mPattern; - } - - /** - * @return The relaxed prefix regex matching this card type. To be used in determining card type if no pattern matches. - */ - public Pattern getRelaxedPrefixPattern() { - return mRelaxedPrefixPattern; - } - - /** - * @return The android resource id for the front card image, highlighting card number format. - */ - public int getFrontResource() { - return mFrontResource; - } - - /** - * @return The android resource id for the security code name for this card type. - */ - public int getSecurityCodeName() { - return mSecurityCodeName; - } - - /** - * @return The length of the current card's security code. - */ - public int getSecurityCodeLength() { - return mSecurityCodeLength; - } - - /** - * @return minimum length of a card for this {@link CardType} - */ - public int getMinCardLength() { - return mMinCardLength; - } - - /** - * @return max length of a card for this {@link CardType} - */ - public int getMaxCardLength() { - return mMaxCardLength; - } - - /** - * @return the locations where spaces should be inserted when formatting the card in a user - * friendly way. Only for display purposes. - */ - public int[] getSpaceIndices() { - return this == AMEX ? AMEX_SPACE_INDICES : DEFAULT_SPACE_INDICES; - } - - /** - * Performs the Luhn check on the given card number. - * - * @param cardNumber a String consisting of numeric digits (only). - * @return {@code true} if the sequence passes the checksum - * @throws IllegalArgumentException if {@code cardNumber} contained a non-digit (where {@link - * Character#isDefined(char)} is {@code false}). - * @see Luhn Algorithm (Wikipedia) - */ - public static boolean isLuhnValid(String cardNumber) { - final String reversed = new StringBuffer(cardNumber).reverse().toString(); - final int len = reversed.length(); - int oddSum = 0; - int evenSum = 0; - for (int i = 0; i < len; i++) { - final char c = reversed.charAt(i); - if (!Character.isDigit(c)) { - throw new IllegalArgumentException(String.format("Not a digit: '%s'", c)); - } - final int digit = Character.digit(c, 10); - if (i % 2 == 0) { - oddSum += digit; - } else { - evenSum += digit / 5 + (2 * digit) % 10; - } - } - return (oddSum + evenSum) % 10 == 0; - } - - /** - * @param cardNumber The card number to validate. - * @return {@code true} if this card number is locally valid. - */ - public boolean validate(String cardNumber) { - if (TextUtils.isEmpty(cardNumber)) { - return false; - } else if (!TextUtils.isDigitsOnly(cardNumber)) { - return false; - } - - final int numberLength = cardNumber.length(); - if (numberLength < mMinCardLength || numberLength > mMaxCardLength) { - return false; - } else if (!mPattern.matcher(cardNumber).matches() && mRelaxedPrefixPattern != null && !mRelaxedPrefixPattern.matcher(cardNumber).matches()) { - return false; - } - return isLuhnValid(cardNumber); - } + VISA, + MASTERCARD, + DISCOVER, + AMEX, + DINERS_CLUB, + JCB, + MAESTRO, + UNIONPAY, + HIPER, + HIPERCARD, + UNKNOWN, + EMPTY; } diff --git a/CardForm/src/main/java/com/braintreepayments/api/CvvEditText.java b/CardForm/src/main/java/com/braintreepayments/api/CvvEditText.java index 8b1be9a..c276579 100644 --- a/CardForm/src/main/java/com/braintreepayments/api/CvvEditText.java +++ b/CardForm/src/main/java/com/braintreepayments/api/CvvEditText.java @@ -20,7 +20,7 @@ public class CvvEditText extends ErrorEditText implements TextWatcher { private static final int DEFAULT_MAX_LENGTH = 3; - private CardType mCardType; + private CardAttributes cardAttributes; public CvvEditText(Context context) { super(context); @@ -51,12 +51,12 @@ private void init() { * @param cardType Type of card represented by the current value of card number input. */ public void setCardType(CardType cardType) { - mCardType = cardType; + cardAttributes = CardAttributes.forCardType(cardType); - InputFilter[] filters = { new LengthFilter(cardType.getSecurityCodeLength()) }; + InputFilter[] filters = { new LengthFilter(cardAttributes.getSecurityCodeLength()) }; setFilters(filters); - setFieldHint(cardType.getSecurityCodeName()); + setFieldHint(cardAttributes.getSecurityCodeName()); invalidate(); } @@ -74,11 +74,11 @@ public void setMask(boolean mask) { @Override public void afterTextChanged(Editable editable) { - if (mCardType == null) { + if (cardAttributes == null) { return; } - if (mCardType.getSecurityCodeLength() == editable.length() && getSelectionStart() == editable.length()) { + if (cardAttributes.getSecurityCodeLength() == editable.length() && getSelectionStart() == editable.length()) { validate(); if (isValid()) { @@ -95,10 +95,10 @@ public boolean isValid() { @Override public String getErrorMessage() { String securityCodeName; - if (mCardType == null) { + if (cardAttributes == null) { securityCodeName = getContext().getString(R.string.bt_cvv); } else { - securityCodeName = getContext().getString(mCardType.getSecurityCodeName()); + securityCodeName = getContext().getString(cardAttributes.getSecurityCodeName()); } if (TextUtils.isEmpty(getText())) { @@ -109,10 +109,10 @@ public String getErrorMessage() { } private int getSecurityCodeLength() { - if (mCardType == null) { + if (cardAttributes == null) { return DEFAULT_MAX_LENGTH; } else { - return mCardType.getSecurityCodeLength(); + return cardAttributes.getSecurityCodeLength(); } } diff --git a/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesAdapter.java b/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesAdapter.java index c59d4aa..bd4dbb7 100644 --- a/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesAdapter.java +++ b/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesAdapter.java @@ -47,7 +47,8 @@ public SupportedCardTypesViewHolder onCreateViewHolder(ViewGroup viewGroup, int @Override public void onBindViewHolder(SupportedCardTypesViewHolder viewHolder, final int position) { SelectableCardType selectableCardType = supportedCardTypes[position]; - viewHolder.getImageView().setImageResource(selectableCardType.getCardType().getFrontResource()); + CardAttributes cardAttributes = CardAttributes.forCardType(selectableCardType.getCardType()); + viewHolder.getImageView().setImageResource(cardAttributes.getFrontResource()); viewHolder.getImageView().setContentDescription(selectableCardType.getCardType().toString()); if (selectableCardType.isDisabled()) { diff --git a/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesView.java b/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesView.java index b495328..6db47b8 100644 --- a/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesView.java +++ b/CardForm/src/main/java/com/braintreepayments/api/SupportedCardTypesView.java @@ -74,7 +74,8 @@ public void setSelected(@Nullable CardType... cardTypes) { SpannableString spannableString = new SpannableString(new String(new char[mSupportedCardTypes.size()])); PaddedImageSpan span; for (int i = 0; i < mSupportedCardTypes.size(); i++) { - span = new PaddedImageSpan(getContext(), mSupportedCardTypes.get(i).getFrontResource()); + CardAttributes cardAttributes = CardAttributes.forCardType(mSupportedCardTypes.get(i)); + span = new PaddedImageSpan(getContext(), cardAttributes.getFrontResource()); span.setDisabled(!Arrays.asList(cardTypes).contains(mSupportedCardTypes.get(i))); spannableString.setSpan(span, i, i + 1, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/CardForm/src/test/java/com/braintreepayments/api/CardAttributesUnitTest.kt b/CardForm/src/test/java/com/braintreepayments/api/CardAttributesUnitTest.kt new file mode 100644 index 0000000..d81f543 --- /dev/null +++ b/CardForm/src/test/java/com/braintreepayments/api/CardAttributesUnitTest.kt @@ -0,0 +1,71 @@ +package com.braintreepayments.api + +import org.junit.Assert.assertTrue +import org.junit.Test + +// TODO: Consider refactoring and converting to a parameterized test to get better coverage +// since each card brand has its own cvv requirements, etc. +class CardAttributesUnitTest { + + companion object { + private const val MIN_MIN_CARD_LENGTH = 12 + private const val MAX_MAX_CARD_LENGTH = 19 + + private const val MIN_SECURITY_CODE_LENGTH = 3 + private const val MAX_SECURITY_CODE_LENGTH = 4 + } + + @Test + fun allParametersSane() { + for (cardType in CardType.values()) { + val attributes = CardAttributes.forCardType(cardType) + + attributes.apply { + assertTrue( + String.format("%s: Min card length %s too small", cardType, minCardLength), + minCardLength >= MIN_MIN_CARD_LENGTH + ) + assertTrue( + String.format("%s: Max card length %s too large", cardType, maxCardLength), + maxCardLength <= MAX_MAX_CARD_LENGTH + ) + assertTrue( + String.format( + "%s: Min card length %s greater than its max %s", + cardType, + minCardLength, + maxCardLength + ), minCardLength <= maxCardLength + ) + assertTrue( + String.format( + "%s: Unusual security code length %s", + cardType, + securityCodeLength + ), + securityCodeLength in MIN_SECURITY_CODE_LENGTH..MAX_SECURITY_CODE_LENGTH + ) + assertTrue( + String.format("%s: No front resource declared", cardType), + frontResource != 0 + ) + assertTrue( + String.format("%s: No Security code resource declared", cardType), + securityCodeName != 0 + ) + + if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { + val regex = pattern.toString() + assertTrue( + String.format("%s: Pattern must start with ^", cardType), + regex.startsWith("^") + ) + assertTrue( + String.format("%s: Pattern must end with \\d*", cardType), + regex.endsWith("\\d*") + ) + } + } + } + } +} \ No newline at end of file diff --git a/CardForm/src/test/java/com/braintreepayments/api/CardParserUnitTest.kt b/CardForm/src/test/java/com/braintreepayments/api/CardParserUnitTest.kt new file mode 100644 index 0000000..5536de6 --- /dev/null +++ b/CardForm/src/test/java/com/braintreepayments/api/CardParserUnitTest.kt @@ -0,0 +1,139 @@ +package com.braintreepayments.api + +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CardParserUnitTest { + + private val sampleCards: Map = mapOf( + // Visa + "4111111111111111" to CardType.VISA, + "4005519200000004" to CardType.VISA, + "4009348888881881" to CardType.VISA, + "4012000033330026" to CardType.VISA, + "4012000077777777" to CardType.VISA, + "4012888888881881" to CardType.VISA, + "4217651111111119" to CardType.VISA, + "4500600000000061" to CardType.VISA, + + // Mastercard + "5555555555554444" to CardType.MASTERCARD, + "5105105105105100" to CardType.MASTERCARD, + "2221000000000009" to CardType.MASTERCARD, + "2223000048400011" to CardType.MASTERCARD, + "2230000000000008" to CardType.MASTERCARD, + "2300000000000003" to CardType.MASTERCARD, + "2500000000000001" to CardType.MASTERCARD, + "2600000000000000" to CardType.MASTERCARD, + "2700000000000009" to CardType.MASTERCARD, + "2720990000000007" to CardType.MASTERCARD, + + // Discover + "6011111111111117" to CardType.DISCOVER, + "6011000990139424" to CardType.DISCOVER, + "6500000000000000003" to CardType.DISCOVER, + + // Amex + "378282246310005" to CardType.AMEX, + "371449635398431" to CardType.AMEX, + + // Diner's club + "30000000000004" to CardType.DINERS_CLUB, + + // JCB + "3530111333300000" to CardType.JCB, + "3566002020360505" to CardType.JCB, + + // Maestro + "5018000000000009" to CardType.MAESTRO, + "5018000000000000122" to CardType.MAESTRO, + "6703000000000007" to CardType.MAESTRO, + "6020111111111116" to CardType.MAESTRO, + "6764111111111116" to CardType.MAESTRO, + "560000000003" to CardType.MAESTRO, + "5600000000000000002" to CardType.MAESTRO, + "570000000002" to CardType.MAESTRO, + "5700000000000000018" to CardType.MAESTRO, + "580000000001" to CardType.MAESTRO, + "5800000000000000008" to CardType.MAESTRO, + "590000000000" to CardType.MAESTRO, + "5900000000000000006" to CardType.MAESTRO, + "5043111111111111" to CardType.MAESTRO, + + // Union Pay + "6240888888888885" to CardType.UNIONPAY, + "6240888888888885127" to CardType.UNIONPAY, + + // Hiper + "6370950000000005" to CardType.HIPER, + "6375680000000003" to CardType.HIPER, + "6375990000000006" to CardType.HIPER, + "6376090000000004" to CardType.HIPER, + "6376120000000009" to CardType.HIPER, + + // Hipercard + "6062820524845321" to CardType.HIPERCARD, + + // Unknown + "2721000000000004" to CardType.UNKNOWN, + "1" to CardType.UNKNOWN, + + // Empty + "" to CardType.EMPTY, + ) + + @Test + fun sampleCardsAreLuhnValid() { + val sut = CardParser() + for ((cardNumber, cardType) in sampleCards) { + val actualType: CardType = sut.parseCardType(cardNumber) + assertEquals( + String.format("CardType.forAccountNumber failed for %s", cardNumber), + cardType, + actualType + ) + if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { + assertTrue( + String.format("%s: Luhn check failed for [%s]", cardType, cardNumber), + sut.isLuhnValid(cardNumber) + ) + } + } + } + + @Test + fun validateSampleCards() { + val sut = CardParser() + for ((cardNumber, cardType) in sampleCards) { + val actualType: CardType = sut.parseCardType(cardNumber) + assertEquals( + String.format("CardType.forAccountNumber failed for %s", cardNumber), + cardType, + actualType + ) + if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { + assertTrue( + String.format("%s: Validate check failed for [%s]", cardType, cardNumber), + sut.validate(cardNumber) + ) + } + } + } + + @Test + fun validate_whenGivenNonDigits_returnsFalse() { + val sut = CardParser() + assertFalse(sut.validate("")) + assertFalse(sut.validate("Not-A-Number")) + assertFalse(sut.validate("@#$%^&")) + } + + @Test + fun validate_whenPatternFailsAndNoRelaxedPatternExists_returnsFalse() { + val sut = CardParser() + assertFalse(sut.validate("9999999999999999")) + } +} \ No newline at end of file diff --git a/CardForm/src/test/java/com/braintreepayments/api/CardTypeTest.java b/CardForm/src/test/java/com/braintreepayments/api/CardTypeTest.java deleted file mode 100644 index 1c72c12..0000000 --- a/CardForm/src/test/java/com/braintreepayments/api/CardTypeTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.braintreepayments.api; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Pattern; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; -import static org.junit.Assert.assertFalse; - -@RunWith(RobolectricTestRunner.class) -public class CardTypeTest { - - private static final int MIN_MIN_CARD_LENGTH = 12; - private static final int MAX_MAX_CARD_LENGTH = 19; - - private static final int MIN_SECURITY_CODE_LENGTH = 3; - private static final int MAX_SECURITY_CODE_LENGTH = 4; - - private static final Map SAMPLE_CARDS = new HashMap<>(); - - static { - // Visa - SAMPLE_CARDS.put("4111111111111111", CardType.VISA); - SAMPLE_CARDS.put("4005519200000004", CardType.VISA); - SAMPLE_CARDS.put("4009348888881881", CardType.VISA); - SAMPLE_CARDS.put("4012000033330026", CardType.VISA); - SAMPLE_CARDS.put("4012000077777777", CardType.VISA); - SAMPLE_CARDS.put("4012888888881881", CardType.VISA); - SAMPLE_CARDS.put("4217651111111119", CardType.VISA); - SAMPLE_CARDS.put("4500600000000061", CardType.VISA); - - // Mastercard - SAMPLE_CARDS.put("5555555555554444", CardType.MASTERCARD); - SAMPLE_CARDS.put("5105105105105100", CardType.MASTERCARD); - SAMPLE_CARDS.put("2221000000000009", CardType.MASTERCARD); - SAMPLE_CARDS.put("2223000048400011", CardType.MASTERCARD); - SAMPLE_CARDS.put("2230000000000008", CardType.MASTERCARD); - SAMPLE_CARDS.put("2300000000000003", CardType.MASTERCARD); - SAMPLE_CARDS.put("2500000000000001", CardType.MASTERCARD); - SAMPLE_CARDS.put("2600000000000000", CardType.MASTERCARD); - SAMPLE_CARDS.put("2700000000000009", CardType.MASTERCARD); - SAMPLE_CARDS.put("2720990000000007", CardType.MASTERCARD); - - // Discover - SAMPLE_CARDS.put("6011111111111117", CardType.DISCOVER); - SAMPLE_CARDS.put("6011000990139424", CardType.DISCOVER); - SAMPLE_CARDS.put("6500000000000000003", CardType.DISCOVER); - - // Amex - SAMPLE_CARDS.put("378282246310005", CardType.AMEX); - SAMPLE_CARDS.put("371449635398431", CardType.AMEX); - - // Diner's club - SAMPLE_CARDS.put("30000000000004", CardType.DINERS_CLUB); - - // JCB - SAMPLE_CARDS.put("3530111333300000", CardType.JCB); - SAMPLE_CARDS.put("3566002020360505", CardType.JCB); - - // Maestro - SAMPLE_CARDS.put("5018000000000009", CardType.MAESTRO); - SAMPLE_CARDS.put("5018000000000000122", CardType.MAESTRO); - SAMPLE_CARDS.put("6703000000000007", CardType.MAESTRO); - SAMPLE_CARDS.put("6020111111111116", CardType.MAESTRO); - SAMPLE_CARDS.put("6764111111111116", CardType.MAESTRO); - SAMPLE_CARDS.put("560000000003", CardType.MAESTRO); - SAMPLE_CARDS.put("5600000000000000002", CardType.MAESTRO); - SAMPLE_CARDS.put("570000000002", CardType.MAESTRO); - SAMPLE_CARDS.put("5700000000000000018", CardType.MAESTRO); - SAMPLE_CARDS.put("580000000001", CardType.MAESTRO); - SAMPLE_CARDS.put("5800000000000000008", CardType.MAESTRO); - SAMPLE_CARDS.put("590000000000", CardType.MAESTRO); - SAMPLE_CARDS.put("5900000000000000006", CardType.MAESTRO); - SAMPLE_CARDS.put("5043111111111111", CardType.MAESTRO); - - // Union Pay - SAMPLE_CARDS.put("6240888888888885", CardType.UNIONPAY); - SAMPLE_CARDS.put("6240888888888885127", CardType.UNIONPAY); - - // Hiper - SAMPLE_CARDS.put("6370950000000005", CardType.HIPER); - SAMPLE_CARDS.put("6375680000000003", CardType.HIPER); - SAMPLE_CARDS.put("6375990000000006", CardType.HIPER); - SAMPLE_CARDS.put("6376090000000004", CardType.HIPER); - SAMPLE_CARDS.put("6376120000000009", CardType.HIPER); - - // Hipercard - SAMPLE_CARDS.put("6062820524845321", CardType.HIPERCARD); - - // Unknown - SAMPLE_CARDS.put("2721000000000004", CardType.UNKNOWN); - SAMPLE_CARDS.put("1", CardType.UNKNOWN); - - // Empty - SAMPLE_CARDS.put("", CardType.EMPTY); - } - - @Test - public void allParametersSane() { - for (final CardType cardType : CardType.values()) { - final int minCardLength = cardType.getMinCardLength(); - assertTrue(String.format("%s: Min card length %s too small", - cardType, minCardLength), minCardLength >= MIN_MIN_CARD_LENGTH); - - final int maxCardLength = cardType.getMaxCardLength(); - assertTrue(String.format("%s: Max card length %s too large", - cardType, maxCardLength), maxCardLength <= MAX_MAX_CARD_LENGTH); - - assertTrue(String.format("%s: Min card length %s greater than its max %s", - cardType, minCardLength, maxCardLength), - minCardLength <= maxCardLength - ); - - final int securityCodeLength = cardType.getSecurityCodeLength(); - assertTrue(String.format("%s: Unusual security code length %s", - cardType, securityCodeLength), - securityCodeLength >= MIN_SECURITY_CODE_LENGTH && - securityCodeLength <= MAX_SECURITY_CODE_LENGTH - ); - - assertTrue(String.format("%s: No front resource declared", cardType), - cardType.getFrontResource() != 0); - assertTrue(String.format("%s: No Security code resource declared", cardType), - cardType.getSecurityCodeName() != 0); - - if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { - final Pattern pattern = cardType.getPattern(); - final String regex = pattern.toString(); - assertTrue(String.format("%s: Pattern must start with ^", cardType), - regex.startsWith("^")); - assertTrue(String.format("%s: Pattern must end with \\d*", cardType), - regex.endsWith("\\d*")); - } - } - } - - @Test - public void sampleCardsAreLuhnValid() { - for (final Map.Entry entry : SAMPLE_CARDS.entrySet()) { - final String cardNumber = entry.getKey(); - final CardType cardType = entry.getValue(); - final CardType actualType = CardType.forCardNumber(cardNumber); - - assertEquals(String.format("CardType.forAccountNumber failed for %s", cardNumber), cardType, actualType); - - if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { - assertTrue(String.format("%s: Luhn check failed for [%s]", cardType, cardNumber), - CardType.isLuhnValid(cardNumber)); - } - } - } - - @Test - public void validateSampleCards() { - for (final Map.Entry entry : SAMPLE_CARDS.entrySet()) { - final String cardNumber = entry.getKey(); - final CardType cardType = entry.getValue(); - final CardType actualType = CardType.forCardNumber(cardNumber); - - assertEquals(String.format("CardType.forAccountNumber failed for %s", cardNumber), cardType, actualType); - - if (cardType != CardType.UNKNOWN && cardType != CardType.EMPTY) { - assertTrue(String.format("%s: Validate check failed for [%s]", cardType, cardNumber), - cardType.validate(cardNumber)); - } - } - } - - @Test - public void validate_whenGivenNonDigits_returnsFalse() { - assertFalse(CardType.UNKNOWN.validate("")); - assertFalse(CardType.UNKNOWN.validate("Not-A-Number")); - assertFalse(CardType.UNKNOWN.validate("@#$%^&")); - } - - @Test - public void validate_whenPatternFailsAndNoRelaxedPatternExists_returnsFalse() { - assertFalse(CardType.VISA.validate("9999999999999999")); - } -} diff --git a/CardForm/src/test/java/com/braintreepayments/api/CvvEditTextTest.java b/CardForm/src/test/java/com/braintreepayments/api/CvvEditTextTest.java index bd2af36..db0503d 100644 --- a/CardForm/src/test/java/com/braintreepayments/api/CvvEditTextTest.java +++ b/CardForm/src/test/java/com/braintreepayments/api/CvvEditTextTest.java @@ -64,7 +64,8 @@ public void hintChangesForCardType() { for (CardType cardType : CardType.values()) { mView.setCardType(cardType); - assertEquals(RuntimeEnvironment.application.getString(cardType.getSecurityCodeName()), + CardAttributes cardAttributes = CardAttributes.forCardType(cardType); + assertEquals(RuntimeEnvironment.application.getString(cardAttributes.getSecurityCodeName()), mView.getTextInputLayoutParent().getHint()); } } @@ -119,8 +120,9 @@ public void getErrorMessage_returnsCorrectErrorMessageForCardTypeWhenEmpty() { for (CardType cardType : CardType.values()) { mView.setCardType(cardType); + CardAttributes cardAttributes = CardAttributes.forCardType(cardType); String expectedMessage = RuntimeEnvironment.application.getString(R.string.bt_cvv_required, - RuntimeEnvironment.application.getString(cardType.getSecurityCodeName())); + RuntimeEnvironment.application.getString(cardAttributes.getSecurityCodeName())); assertEquals(expectedMessage, mView.getErrorMessage()); } } @@ -141,8 +143,9 @@ public void getErrorMessage_returnsCorrectErrorMessageForCardTypeWhenNotEmpty() for (CardType cardType : CardType.values()) { mView.setCardType(cardType); + CardAttributes cardAttributes = CardAttributes.forCardType(cardType); String expectedMessage = RuntimeEnvironment.application.getString(R.string.bt_cvv_invalid, - RuntimeEnvironment.application.getString(cardType.getSecurityCodeName())); + RuntimeEnvironment.application.getString(cardAttributes.getSecurityCodeName())); assertEquals(expectedMessage, mView.getErrorMessage()); } } diff --git a/CardForm/src/test/java/com/braintreepayments/api/ErrorEditTextTest.java b/CardForm/src/test/java/com/braintreepayments/api/ErrorEditTextTest.java index 9b81b2f..e6fe9fb 100644 --- a/CardForm/src/test/java/com/braintreepayments/api/ErrorEditTextTest.java +++ b/CardForm/src/test/java/com/braintreepayments/api/ErrorEditTextTest.java @@ -1,9 +1,17 @@ package com.braintreepayments.api; -import com.google.android.material.textfield.TextInputLayout; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; import com.braintreepayments.api.cardform.R; import com.braintreepayments.api.test.TestActivity; +import com.braintreepayments.api.test.TestApplication; +import com.google.android.material.textfield.TextInputLayout; import org.junit.Before; import org.junit.Test; @@ -11,21 +19,17 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(application = TestApplication.class) public class ErrorEditTextTest { private ErrorEditText mView; @Before public void setup() { - mView = new ErrorEditText(RuntimeEnvironment.application); + mView = new ErrorEditText(ApplicationProvider.getApplicationContext()); } @Test diff --git a/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesAdapterTest.java b/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesAdapterTest.java index 8324e50..653aa65 100644 --- a/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesAdapterTest.java +++ b/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesAdapterTest.java @@ -94,6 +94,6 @@ public void onBindViewHolder_setsImageViewBasedOnCardType() { sut.onBindViewHolder(viewHolder, 0); - verify(imageView).setImageResource(CardType.AMEX.getFrontResource()); + verify(imageView).setImageResource(CardAttributes.forCardType(CardType.AMEX).getFrontResource()); } } diff --git a/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesViewTest.java b/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesViewTest.java index 15292be..fe6cef7 100644 --- a/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesViewTest.java +++ b/CardForm/src/test/java/com/braintreepayments/api/SupportedCardTypesViewTest.java @@ -21,22 +21,19 @@ public class SupportedCardTypesViewTest { public void setSupportedCardTypes_addsAllCardTypes() { SupportedCardTypesView supportedCardTypesView = new SupportedCardTypesView(RuntimeEnvironment.application); - supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, - CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, - CardType.HIPER, CardType.HIPERCARD); - - List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()) - .getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); - assertEquals(CardType.VISA.getFrontResource(), allSpans.get(0).getResourceId()); - assertEquals(CardType.MASTERCARD.getFrontResource(), allSpans.get(1).getResourceId()); - assertEquals(CardType.DISCOVER.getFrontResource(), allSpans.get(2).getResourceId()); - assertEquals(CardType.AMEX.getFrontResource(), allSpans.get(3).getResourceId()); - assertEquals(CardType.DINERS_CLUB.getFrontResource(), allSpans.get(4).getResourceId()); - assertEquals(CardType.JCB.getFrontResource(), allSpans.get(5).getResourceId()); - assertEquals(CardType.MAESTRO.getFrontResource(), allSpans.get(6).getResourceId()); - assertEquals(CardType.UNIONPAY.getFrontResource(), allSpans.get(7).getResourceId()); - assertEquals(CardType.HIPER.getFrontResource(), allSpans.get(8).getResourceId()); - assertEquals(CardType.HIPERCARD.getFrontResource(), allSpans.get(9).getResourceId()); + supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, CardType.HIPER, CardType.HIPERCARD); + + List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()).getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); + assertEquals(CardAttributes.forCardType(CardType.VISA).getFrontResource(), allSpans.get(0).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.MASTERCARD).getFrontResource(), allSpans.get(1).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.DISCOVER).getFrontResource(), allSpans.get(2).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.AMEX).getFrontResource(), allSpans.get(3).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.DINERS_CLUB).getFrontResource(), allSpans.get(4).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.JCB).getFrontResource(), allSpans.get(5).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.MAESTRO).getFrontResource(), allSpans.get(6).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.UNIONPAY).getFrontResource(), allSpans.get(7).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.HIPER).getFrontResource(), allSpans.get(8).getResourceId()); + assertEquals(CardAttributes.forCardType(CardType.HIPERCARD).getFrontResource(), allSpans.get(9).getResourceId()); } @Test @@ -45,22 +42,18 @@ public void setSupportedCardTypes_handlesNull() { supportedCardTypesView.setSupportedCardTypes((CardType[]) null); - List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()) - .getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); + List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()).getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); assertEquals(0, allSpans.size()); } @Test public void setSelectedCardTypes_disablesNonSelectedCardTypes() { SupportedCardTypesView supportedCardTypesView = new SupportedCardTypesView(RuntimeEnvironment.application); - supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, - CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, - CardType.HIPER, CardType.HIPERCARD); + supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, CardType.HIPER, CardType.HIPERCARD); supportedCardTypesView.setSelected(CardType.VISA); - List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()) - .getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); + List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()).getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); assertFalse(allSpans.get(0).isDisabled()); assertTrue(allSpans.get(1).isDisabled()); assertTrue(allSpans.get(2).isDisabled()); @@ -76,14 +69,11 @@ public void setSelectedCardTypes_disablesNonSelectedCardTypes() { @Test public void setSelectedCardTypes_handlesNull() { SupportedCardTypesView supportedCardTypesView = new SupportedCardTypesView(RuntimeEnvironment.application); - supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, - CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, - CardType.HIPER, CardType.HIPERCARD); + supportedCardTypesView.setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.DISCOVER, CardType.AMEX, CardType.DINERS_CLUB, CardType.JCB, CardType.MAESTRO, CardType.UNIONPAY, CardType.HIPER, CardType.HIPERCARD); supportedCardTypesView.setSelected((CardType[]) null); - List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()) - .getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); + List allSpans = Arrays.asList(new SpannableString(supportedCardTypesView.getText()).getSpans(0, supportedCardTypesView.length(), PaddedImageSpan.class)); assertTrue(allSpans.get(0).isDisabled()); assertTrue(allSpans.get(1).isDisabled()); assertTrue(allSpans.get(2).isDisabled()); diff --git a/CardForm/src/test/java/com/braintreepayments/api/test/TestApplication.kt b/CardForm/src/test/java/com/braintreepayments/api/test/TestApplication.kt new file mode 100644 index 0000000..f915f1e --- /dev/null +++ b/CardForm/src/test/java/com/braintreepayments/api/test/TestApplication.kt @@ -0,0 +1,12 @@ +package com.braintreepayments.api.test + +import android.app.Application +import androidx.appcompat.R + +class TestApplication: Application() { + + override fun onCreate() { + super.onCreate() + setTheme(R.style.Theme_AppCompat) + } +} \ No newline at end of file diff --git a/Sample/build.gradle b/Sample/build.gradle index 53cb6ef..c0a52c6 100644 --- a/Sample/build.gradle +++ b/Sample/build.gradle @@ -25,13 +25,13 @@ android { dependencies { implementation project(':CardForm') - implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'com.facebook.stetho:stetho:1.5.0' - implementation 'com.google.android.material:material:1.0.0' - implementation 'com.squareup.leakcanary:leakcanary-android:1.4' + implementation 'com.google.android.material:material:1.7.0' + implementation 'com.squareup.leakcanary:leakcanary-android:2.7' - androidTestImplementation 'androidx.test:core:1.0.0' - androidTestImplementation 'androidx.test:rules:1.1.0' - androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test:core:1.5.0' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'com.braintreepayments:device-automator:1.0.0' } diff --git a/Sample/src/main/AndroidManifest.xml b/Sample/src/main/AndroidManifest.xml index b5f7c53..c5aa5be 100644 --- a/Sample/src/main/AndroidManifest.xml +++ b/Sample/src/main/AndroidManifest.xml @@ -3,26 +3,30 @@ + android:supportsRtl="true" + android:theme="@android:style/Theme.Holo.Light"> - + - - diff --git a/Sample/src/main/java/com/braintreepayments/sample/SampleApplication.java b/Sample/src/main/java/com/braintreepayments/sample/SampleApplication.java index 694a193..55e258e 100644 --- a/Sample/src/main/java/com/braintreepayments/sample/SampleApplication.java +++ b/Sample/src/main/java/com/braintreepayments/sample/SampleApplication.java @@ -3,14 +3,12 @@ import android.app.Application; import com.facebook.stetho.Stetho; -import com.squareup.leakcanary.LeakCanary; public class SampleApplication extends Application { @Override public void onCreate() { super.onCreate(); - LeakCanary.install(this); Stetho.initializeWithDefaults(this); } } diff --git a/build.gradle b/build.gradle index a798758..675460f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath 'de.marcphilipp.gradle:nexus-publish-plugin:0.4.0' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20' classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2' } } @@ -23,9 +24,9 @@ ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_KEY_FILE') ?: '' version '5.4.1-SNAPSHOT' ext { - compileSdkVersion = 30 + compileSdkVersion = 33 minSdkVersion = 21 - targetSdkVersion = 30 + targetSdkVersion = 33 versionCode = 52 versionName = version } diff --git a/gradle.properties b/gradle.properties index dbb7bf7..5bac8ac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1 @@ -android.enableJetifier=true android.useAndroidX=true