diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef571c5..33fb9c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.10.0](https://github.com/Basis-Theory/basistheory-android/compare/0.9.0...0.10.0) (2022-12-14) + + +### Features + +* Allows custom masks, transforms, and validators on TextElement ([#19](https://github.com/Basis-Theory/basistheory-android/issues/19)) ([ceaf62c](https://github.com/Basis-Theory/basistheory-android/commit/ceaf62c48249df94be7f4b3ec850c965d46af0b2)) + + ## [0.9.0](https://github.com/Basis-Theory/basistheory-android/compare/0.8.0...0.9.0) (2022-12-13) diff --git a/README.md b/README.md index 8fd93be8..15d4287a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add this dependency to your project's build file: } dependencies { - implementation 'com.github.basis-theory.basistheory-android:lib:0.9.0' + implementation 'com.github.basis-theory.basistheory-android:lib:0.10.0' } ``` diff --git a/example/src/main/java/com/basistheory/android/example/view/custom_form/CustomFormFragment.kt b/example/src/main/java/com/basistheory/android/example/view/custom_form/CustomFormFragment.kt index dfa6cce2..c8e809dd 100644 --- a/example/src/main/java/com/basistheory/android/example/view/custom_form/CustomFormFragment.kt +++ b/example/src/main/java/com/basistheory/android/example/view/custom_form/CustomFormFragment.kt @@ -10,6 +10,7 @@ import com.basistheory.android.example.databinding.FragmentCustomFormBinding import com.basistheory.android.example.util.tokenExpirationTimestamp import com.basistheory.android.example.viewmodel.TokenizeViewModel import com.basistheory.android.model.KeyboardType +import com.basistheory.android.view.mask.ElementMask class CustomFormFragment : Fragment() { private val binding: FragmentCustomFormBinding by lazy { @@ -18,9 +19,7 @@ class CustomFormFragment : Fragment() { private val viewModel: TokenizeViewModel by viewModels() override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { super.onCreateView(inflater, container, savedInstanceState) binding.lifecycleOwner = this @@ -31,9 +30,30 @@ class CustomFormFragment : Fragment() { // illustrates that keyboardType can be set programmatically (or in xml) binding.phoneNumber.keyboardType = KeyboardType.NUMBER - binding.phoneNumber.mask = listOf("+", "1", "(", digitRegex,digitRegex,digitRegex, ")", " ", digitRegex, digitRegex, digitRegex, "-", digitRegex, digitRegex , digitRegex, digitRegex ) + binding.phoneNumber.mask = ElementMask( + listOf( + "+", + "1", + "(", + digitRegex, + digitRegex, + digitRegex, + ")", + " ", + digitRegex, + digitRegex, + digitRegex, + "-", + digitRegex, + digitRegex, + digitRegex, + digitRegex + ) + ) - binding.orderNumber.mask = listOf(charRegex, charRegex, charRegex, "-", digitRegex, digitRegex, digitRegex) + binding.orderNumber.mask = ElementMask( + listOf(charRegex, charRegex, charRegex, "-", digitRegex, digitRegex, digitRegex) + ) binding.tokenizeButton.setOnClickListener { tokenize() } binding.autofillButton.setOnClickListener { autofill() } @@ -41,23 +61,21 @@ class CustomFormFragment : Fragment() { return binding.root } - private fun autofill() { - binding.name.setText("John Doe") - binding.phoneNumber.setText("2345678900") - binding.orderNumber.setText("ABC123") + private fun autofill() { + binding.name.setText("John Doe") + binding.phoneNumber.setText("2345678900") + binding.orderNumber.setText("ABC123") } - private fun tokenize() = - viewModel.tokenize( - object { - val type = "token" - val data = object { - val name = binding.name - val phoneNumber = binding.phoneNumber - val orderNumber = binding.orderNumber - } - val expires_at = tokenExpirationTimestamp() - }).observe(viewLifecycleOwner) {} + private fun tokenize() = viewModel.tokenize(object { + val type = "token" + val data = object { + val name = binding.name + val phoneNumber = binding.phoneNumber + val orderNumber = binding.orderNumber + } + val expires_at = tokenExpirationTimestamp() + }).observe(viewLifecycleOwner) {} } diff --git a/lib/build.gradle b/lib/build.gradle index a4f83be2..2494c6ea 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -11,7 +11,7 @@ android { defaultConfig { minSdk 21 targetSdk 32 - versionName = '0.9.0' + versionName = '0.10.0' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" diff --git a/lib/src/main/java/com/basistheory/android/service/CardBrandEnricher.kt b/lib/src/main/java/com/basistheory/android/service/CardBrandEnricher.kt index 4a9f345c..115b8899 100644 --- a/lib/src/main/java/com/basistheory/android/service/CardBrandEnricher.kt +++ b/lib/src/main/java/com/basistheory/android/service/CardBrandEnricher.kt @@ -7,6 +7,8 @@ class CardBrandEnricher { object CardMasks { const val MASK_4_8_12GAPS_19LENGTH = "#### #### #### #######" const val MASK_4_8_12GAPS_16LENGTH = "#### #### #### ####" + const val MASK_4_10GAPS_15LENGTH = "#### ###### #####" + const val MASK_4_10GAPS_19LENGTH = "#### ###### #########" } object CvcMasks { @@ -18,8 +20,8 @@ class CardBrandEnricher { var brand: String = "", var identifierRanges: List>, var validLengths: IntArray, - var cvcMask: String = "", - var cardMask: String = "" + var cvcMask: String, + var cardMask: String ) class CardResult( @@ -48,14 +50,20 @@ class CardBrandEnricher { "23" to "26", "270" to "271", "2720" to null - ), intArrayOf(16), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_16LENGTH + ), + intArrayOf(16), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_16LENGTH ), CardDetails( CardBrands.AMERICAN_EXPRESS.label, listOf( "34" to null, "37" to null - ), intArrayOf(15), CvcMasks.FOUR_DIGIT, "#### ###### #####" + ), + intArrayOf(15), + CvcMasks.FOUR_DIGIT, + CardMasks.MASK_4_10GAPS_15LENGTH ), CardDetails( @@ -63,7 +71,10 @@ class CardBrandEnricher { "36" to null, "38" to "39", "300" to "305" - ), intArrayOf(14, 16, 19), CvcMasks.THREE_DIGIT, "#### ###### #########" + ), + intArrayOf(14, 16, 19), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_10GAPS_19LENGTH ), CardDetails( @@ -71,7 +82,10 @@ class CardBrandEnricher { "65" to null, "6011" to "39", "644" to "649" - ), intArrayOf(16, 19), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_19LENGTH + ), + intArrayOf(16, 19), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_19LENGTH ), CardDetails( @@ -79,7 +93,10 @@ class CardBrandEnricher { "2131" to null, "1800" to "39", "3528" to "3589" - ), intArrayOf(16, 17, 18, 19), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_19LENGTH + ), + intArrayOf(16, 17, 18, 19), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_19LENGTH ), CardDetails( @@ -100,7 +117,10 @@ class CardBrandEnricher { "627781" to "627799", "6282" to "6289", "8110" to "8171", - ), intArrayOf(14, 15, 16, 17, 18, 19), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_19LENGTH + ), + intArrayOf(14, 15, 16, 17, 18, 19), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_19LENGTH ), CardDetails( @@ -113,7 +133,10 @@ class CardBrandEnricher { "504176" to "506698", "506779" to "508999", "56" to "59", - ), intArrayOf(12, 13, 14, 15, 16, 17, 18, 19), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_19LENGTH + ), + intArrayOf(12, 13, 14, 15, 16, 17, 18, 19), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_19LENGTH ), CardDetails( @@ -143,7 +166,10 @@ class CardBrandEnricher { "651652" to "651679", "655000" to "655019", "655021" to "655058", - ), intArrayOf(16), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_16LENGTH + ), + intArrayOf(16), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_16LENGTH ), CardDetails( @@ -163,13 +189,19 @@ class CardBrandEnricher { "637599" to null, "637609" to null, "637612" to null, - ), intArrayOf(16), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_16LENGTH + ), + intArrayOf(16), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_16LENGTH ), CardDetails( CardBrands.HIPERCARD.label, listOf( "606282" to null, - ), intArrayOf(16), CvcMasks.THREE_DIGIT, CardMasks.MASK_4_8_12GAPS_16LENGTH + ), + intArrayOf(16), + CvcMasks.THREE_DIGIT, + CardMasks.MASK_4_8_12GAPS_16LENGTH ) ) @@ -199,6 +231,10 @@ class CardBrandEnricher { identifierMatch: String, number: String ): CardResult = - if (currentBestMatch.identifierLength < identifierMatch.length) CardResult(cardDetails, number.length, identifierMatch.length) + if (currentBestMatch.identifierLength < identifierMatch.length) CardResult( + cardDetails, + number.length, + identifierMatch.length + ) else currentBestMatch } \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/android/view/CardExpirationDateElement.kt b/lib/src/main/java/com/basistheory/android/view/CardExpirationDateElement.kt index 331d98a2..cfa9a0be 100644 --- a/lib/src/main/java/com/basistheory/android/view/CardExpirationDateElement.kt +++ b/lib/src/main/java/com/basistheory/android/view/CardExpirationDateElement.kt @@ -1,11 +1,11 @@ package com.basistheory.android.view import android.content.Context -import android.text.Editable import android.util.AttributeSet import com.basistheory.android.model.ElementValueReference import com.basistheory.android.model.KeyboardType -import com.basistheory.android.view.validation.futureDateValidator +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.validation.FutureDateValidator class CardExpirationDateElement @JvmOverloads constructor( context: Context, @@ -19,20 +19,9 @@ class CardExpirationDateElement @JvmOverloads constructor( init { super.keyboardType = KeyboardType.NUMBER super.mask = defaultMask - super.validate = { futureDateValidator(it) } + super.validator = FutureDateValidator() } - private fun getMonthValue(): String? = - getText() - ?.split("/") - ?.elementAtOrNull(0) - - private fun getYearValue(): String? = - getText() - ?.split("/") - ?.elementAtOrNull(1) - ?.let { "20$it" } - /** * If the user entered a leading digit > 1, auto insert a leading 0 */ @@ -45,10 +34,22 @@ class CardExpirationDateElement @JvmOverloads constructor( return if (firstDigit > 1) "0$value" else value } + private fun getMonthValue(): String? = + getText() + ?.split("/") + ?.elementAtOrNull(0) + + private fun getYearValue(): String? = + getText() + ?.split("/") + ?.elementAtOrNull(1) + ?.let { "20$it" } + companion object { private val digit = Regex("""\d""") - val defaultMask: List = + val defaultMask = ElementMask( listOf(digit, digit, "/", digit, digit) + ) } } diff --git a/lib/src/main/java/com/basistheory/android/view/CardNumberElement.kt b/lib/src/main/java/com/basistheory/android/view/CardNumberElement.kt index 729904e7..ff2e693b 100644 --- a/lib/src/main/java/com/basistheory/android/view/CardNumberElement.kt +++ b/lib/src/main/java/com/basistheory/android/view/CardNumberElement.kt @@ -6,13 +6,15 @@ import com.basistheory.android.event.ChangeEvent import com.basistheory.android.event.EventDetails import com.basistheory.android.model.KeyboardType import com.basistheory.android.service.CardBrandEnricher -import com.basistheory.android.view.transform.regexReplaceElementTransform -import com.basistheory.android.view.validation.luhnValidator +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.transform.RegexReplaceElementTransform +import com.basistheory.android.view.validation.LuhnValidator class CardNumberElement @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : TextElement(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0 +) : TextElement(context, attrs, defStyleAttr) { var cardDetails: CardBrandEnricher.CardDetails? = null private set @@ -22,8 +24,8 @@ class CardNumberElement @JvmOverloads constructor( init { super.keyboardType = KeyboardType.NUMBER super.mask = defaultMask - super.transform = regexReplaceElementTransform(Regex("""\s"""), "") - super.validate = ::luhnValidator + super.transform = RegexReplaceElementTransform(Regex("""\s"""), "") + super.validator = LuhnValidator() } override fun beforeTextChanged(value: String?): String? { @@ -31,7 +33,7 @@ class CardNumberElement @JvmOverloads constructor( cardDetails = cardResult.cardDetails if (cardResult.cardDetails?.cardMask != null) - mask = cardResult.cardDetails!!.cardMask.toList() + mask = ElementMask(cardResult.cardDetails!!.cardMask) return value } @@ -63,16 +65,17 @@ class CardNumberElement @JvmOverloads constructor( } private fun getDigitsOnly(text: String?): String? { - val maskedValue = maskValue?.evaluate(text, inputAction) - return transform(maskedValue) + val maskedValue = mask?.evaluate(text, inputAction) + return transform?.apply(maskedValue) ?: maskedValue } companion object { private val digit = Regex("""\d""") - val defaultMask: List = + val defaultMask = ElementMask( (1..19).map { if (it % 5 == 0 && it > 0) " " else digit } + ) } } diff --git a/lib/src/main/java/com/basistheory/android/view/CardVerificationCodeElement.kt b/lib/src/main/java/com/basistheory/android/view/CardVerificationCodeElement.kt index 9f74502b..31004a87 100644 --- a/lib/src/main/java/com/basistheory/android/view/CardVerificationCodeElement.kt +++ b/lib/src/main/java/com/basistheory/android/view/CardVerificationCodeElement.kt @@ -3,18 +3,22 @@ package com.basistheory.android.view import android.content.Context import android.util.AttributeSet import com.basistheory.android.model.KeyboardType +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.validation.RegexValidator class CardVerificationCodeElement @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : TextElement(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0 +) : TextElement(context, attrs, defStyleAttr) { var cardNumberElement: CardNumberElement? = null set(value) { field = value if (value != null && cardNumberElement != value) { - super.mask = field?.cardDetails?.cvcMask?.toList() ?: defaultMask + super.mask = + cardNumberElement?.cardDetails?.cvcMask?.let { ElementMask(it) } ?: defaultMask field?.addChangeEventListener { updateMask() } } } @@ -22,17 +26,18 @@ class CardVerificationCodeElement @JvmOverloads constructor( init { super.keyboardType = KeyboardType.NUMBER super.mask = defaultMask - super.validate = { Regex("""^\d{3,4}$""").matches(it ?: "") } + super.validator = RegexValidator("""^\d{3,4}$""") } companion object { private val digit = Regex("""\d""") - val defaultMask: List = + val defaultMask = ElementMask( listOf(digit, digit, digit) + ) } private fun updateMask() { - super.mask = cardNumberElement?.cardDetails?.cvcMask?.toList() ?: defaultMask + super.mask = cardNumberElement?.cardDetails?.cvcMask?.let { ElementMask(it) } ?: defaultMask } } diff --git a/lib/src/main/java/com/basistheory/android/view/TextElement.kt b/lib/src/main/java/com/basistheory/android/view/TextElement.kt index d7dbc0eb..591dbbac 100644 --- a/lib/src/main/java/com/basistheory/android/view/TextElement.kt +++ b/lib/src/main/java/com/basistheory/android/view/TextElement.kt @@ -21,7 +21,9 @@ import com.basistheory.android.event.ElementEventListeners import com.basistheory.android.event.FocusEvent import com.basistheory.android.model.InputAction import com.basistheory.android.model.KeyboardType -import com.basistheory.android.view.mask.Mask +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.transform.ElementTransform +import com.basistheory.android.view.validation.ElementValidator open class TextElement @JvmOverloads constructor( context: Context, @@ -33,7 +35,6 @@ open class TextElement @JvmOverloads constructor( private val eventListeners = ElementEventListeners() private var isInternalChange: Boolean = false - internal var maskValue: Mask? = null internal var inputAction: InputAction = InputAction.INSERT init { @@ -51,8 +52,7 @@ open class TextElement @JvmOverloads constructor( hint = getString(R.styleable.TextElement_hint) removeDefaultStyles = getBoolean(R.styleable.TextElement_removeDefaultStyles, false) - mask = getString(R.styleable.TextElement_mask)?.split("") - ?.filter { it.isNotEmpty() } + mask = getString(R.styleable.TextElement_mask)?.let { ElementMask(it) } keyboardType = KeyboardType.fromInt( getInt( R.styleable.TextElement_keyboardType, @@ -70,16 +70,18 @@ open class TextElement @JvmOverloads constructor( // this MUST be internal to prevent host apps from accessing the raw input values internal fun getText(): String? = - transform(editText.text?.toString()) + editText.text?.toString().let { + transform?.apply(it) ?: it + } fun setText(value: String?) = editText.setText(value) - internal var validate: (value: String?) -> Boolean = - { _ -> true } + var mask: ElementMask? = null + + var transform: ElementTransform? = null - internal var transform: (value: String?) -> String? = - { value -> value } + var validator: ElementValidator? = null var textColor: Int get() = editText.currentTextColor @@ -97,12 +99,6 @@ open class TextElement @JvmOverloads constructor( editText.inputType = value.inputType } - var mask: List? = null - set(value) { - field = value - maskValue = value?.let { Mask(it) } - } - var removeDefaultStyles: Boolean get() = editText.background == null set(value) { @@ -125,6 +121,22 @@ open class TextElement @JvmOverloads constructor( return editText.onCreateInputConnection(outAttrs) } + override fun onSaveInstanceState(): Parcelable { + return bundleOf( + STATE_SUPER to super.onSaveInstanceState(), + STATE_INPUT to editText.onSaveInstanceState() + ) + } + + override fun onRestoreInstanceState(state: Parcelable) { + if (state is Bundle) { + editText.onRestoreInstanceState(state.getParcelable(STATE_INPUT)) + super.onRestoreInstanceState(state.getParcelable(STATE_SUPER)) + } else { + super.onRestoreInstanceState(state) + } + } + protected open fun beforeTextChanged(value: String?): String? = value protected open fun createElementChangeEvent( @@ -181,7 +193,7 @@ open class TextElement @JvmOverloads constructor( val originalValue = editable?.toString() val transformedValue = beforeTextChanged(originalValue) - .let { maskValue?.evaluate(it, inputAction) ?: it } + .let { mask?.evaluate(it, inputAction) ?: it } if (originalValue != transformedValue) applyInternalChange(transformedValue) @@ -206,9 +218,9 @@ open class TextElement @JvmOverloads constructor( private fun publishChangeEvent(editable: Editable?) { val event = createElementChangeEvent( getText(), - maskValue?.isComplete(editable?.toString()) ?: false, + mask?.isComplete(editable?.toString()) ?: false, editable?.isEmpty() ?: false, - validate(getText()) + validator?.validate(getText()) ?: true ) eventListeners.change.forEach { @@ -216,22 +228,6 @@ open class TextElement @JvmOverloads constructor( } } - override fun onSaveInstanceState(): Parcelable { - return bundleOf( - STATE_SUPER to super.onSaveInstanceState(), - STATE_INPUT to editText.onSaveInstanceState() - ) - } - - override fun onRestoreInstanceState(state: Parcelable) { - if (state is Bundle) { - editText.onRestoreInstanceState(state.getParcelable(STATE_INPUT)) - super.onRestoreInstanceState(state.getParcelable(STATE_SUPER)) - } else { - super.onRestoreInstanceState(state) - } - } - internal companion object { private const val STATE_SUPER = "state_super" private const val STATE_INPUT = "state_input" diff --git a/lib/src/main/java/com/basistheory/android/view/mask/Mask.kt b/lib/src/main/java/com/basistheory/android/view/mask/ElementMask.kt similarity index 58% rename from lib/src/main/java/com/basistheory/android/view/mask/Mask.kt rename to lib/src/main/java/com/basistheory/android/view/mask/ElementMask.kt index 774a7008..3451ee27 100644 --- a/lib/src/main/java/com/basistheory/android/view/mask/Mask.kt +++ b/lib/src/main/java/com/basistheory/android/view/mask/ElementMask.kt @@ -2,8 +2,19 @@ package com.basistheory.android.view.mask import com.basistheory.android.model.InputAction -class Mask(mask: List) { - private val sanitizedMask = sanitizeAndValidateMask(mask) +class ElementMask { + val characterMasks: List + + constructor(value: List) { + characterMasks = sanitizeAndValidateMask(value) + } + + constructor(value: String) { + characterMasks = sanitizeAndValidateMask(value + .split("") + .filter { it.isNotEmpty() } + ) + } internal fun evaluate(text: String?, action: InputAction): String? { if (text.isNullOrEmpty()) @@ -13,18 +24,18 @@ class Mask(mask: List) { val maskedValue = mutableListOf() var inputChar = source.nextOrNull() - for (maskChar in sanitizedMask) { - if (maskChar !is Regex && maskChar == inputChar.toString()) { - maskedValue.add(maskChar.toString().single()) + for (charMask in characterMasks) { + if (charMask !is Regex && charMask == inputChar.toString()) { + maskedValue.add(charMask.toString().single()) // if text starts with maskedValue, then move the iterator forward if (text.toString().startsWith(maskedValue.joinToString(""))) inputChar = source.nextOrNull() - } else if (maskChar is Regex) { - inputChar = source.nextCharMatchingMask(inputChar, maskChar) + } else if (charMask is Regex) { + inputChar = source.nextCharMatchingMask(inputChar, charMask) if (inputChar == null) { break // do not render placeholders - } else if (!maskChar.matches(inputChar.toString())) { + } else if (!charMask.matches(inputChar.toString())) { inputChar = source.nextOrNull() } else { maskedValue.add(inputChar) @@ -32,7 +43,7 @@ class Mask(mask: List) { } } else { if (inputChar == null && action == InputAction.DELETE) break - maskedValue.add(maskChar.toString().single()) + maskedValue.add(charMask.toString().single()) } } @@ -40,7 +51,7 @@ class Mask(mask: List) { } fun isComplete(value: String?): Boolean = - !value.isNullOrEmpty() && value.length == sanitizedMask.count() + !value.isNullOrEmpty() && value.length == characterMasks.count() private fun sanitizeAndValidateMask( mask: List @@ -73,4 +84,30 @@ class Mask(mask: List) { return if (maskChar.matches(nextInputChar.toString())) nextInputChar else null } + + override fun equals(other: Any?): Boolean = + (other is ElementMask) && ElementMaskComparer(this) == ElementMaskComparer(other) + + override fun hashCode(): Int = + ElementMaskComparer(this).hashCode() + + /** + * Utility class to assist in comparing two ElementMask instances since + * Regex objects are not comparable within the list of character masks + */ + private data class ElementMaskComparer(val characterMaskPatterns: List) { + constructor(mask: ElementMask) : this(mask.toComparablePatterns()) + + companion object { + private fun ElementMask.toComparablePatterns(): List = + this.characterMasks.map { + when (it) { + is Regex -> it.pattern + else -> it.toString() + } + } + } + } } + + diff --git a/lib/src/main/java/com/basistheory/android/view/transform/ElementTransform.kt b/lib/src/main/java/com/basistheory/android/view/transform/ElementTransform.kt new file mode 100644 index 00000000..b302c74a --- /dev/null +++ b/lib/src/main/java/com/basistheory/android/view/transform/ElementTransform.kt @@ -0,0 +1,5 @@ +package com.basistheory.android.view.transform + +abstract class ElementTransform internal constructor() { + abstract fun apply(value: String?): String? +} \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/android/view/transform/RegexReplaceElementTransform.kt b/lib/src/main/java/com/basistheory/android/view/transform/RegexReplaceElementTransform.kt index 99b40565..a650eca3 100644 --- a/lib/src/main/java/com/basistheory/android/view/transform/RegexReplaceElementTransform.kt +++ b/lib/src/main/java/com/basistheory/android/view/transform/RegexReplaceElementTransform.kt @@ -1,8 +1,10 @@ package com.basistheory.android.view.transform -fun regexReplaceElementTransform( - regex: Regex, - replacement: String = "" -): (value: String?) -> String? = { - if (it == null) null else regex.replace(it, replacement) -} \ No newline at end of file +class RegexReplaceElementTransform( + private val regex: Regex, + private val replacement: String = "" +): ElementTransform() { + + override fun apply(value: String?): String? = + if (value == null) null else regex.replace(value, replacement) +} diff --git a/lib/src/main/java/com/basistheory/android/view/validation/ElementValidator.kt b/lib/src/main/java/com/basistheory/android/view/validation/ElementValidator.kt new file mode 100644 index 00000000..2d345c42 --- /dev/null +++ b/lib/src/main/java/com/basistheory/android/view/validation/ElementValidator.kt @@ -0,0 +1,5 @@ +package com.basistheory.android.view.validation + +abstract class ElementValidator internal constructor() { + abstract fun validate(value: String?): Boolean +} \ No newline at end of file diff --git a/lib/src/main/java/com/basistheory/android/view/validation/FutureDateValidator.kt b/lib/src/main/java/com/basistheory/android/view/validation/FutureDateValidator.kt index ba200947..67b1a9e1 100644 --- a/lib/src/main/java/com/basistheory/android/view/validation/FutureDateValidator.kt +++ b/lib/src/main/java/com/basistheory/android/view/validation/FutureDateValidator.kt @@ -2,18 +2,23 @@ package com.basistheory.android.view.validation import org.threeten.bp.LocalDate -fun futureDateValidator(value: String?, currentDate: LocalDate = LocalDate.now()): Boolean { - try { - if (value.isNullOrEmpty()) return false +class FutureDateValidator( + private val currentDate: LocalDate = LocalDate.now() +): ElementValidator() { - val segments = value.split("/") - if (segments.count() != 2) return false + override fun validate(value: String?): Boolean { + try { + if (value.isNullOrEmpty()) return false - val month = segments[0].toInt() - val year = 2000 + segments[1].toInt() + val segments = value.split("/") + if (segments.count() != 2) return false - return currentDate.withDayOfMonth(1) <= LocalDate.of(year, month, 1) - } catch (t: Throwable) { - return false + val month = segments[0].toInt() + val year = 2000 + segments[1].toInt() + + return currentDate.withDayOfMonth(1) <= LocalDate.of(year, month, 1) + } catch (t: Throwable) { + return false + } } -} \ No newline at end of file +} diff --git a/lib/src/main/java/com/basistheory/android/view/validation/LuhnValidator.kt b/lib/src/main/java/com/basistheory/android/view/validation/LuhnValidator.kt index 1b3ec8e1..7196f32d 100644 --- a/lib/src/main/java/com/basistheory/android/view/validation/LuhnValidator.kt +++ b/lib/src/main/java/com/basistheory/android/view/validation/LuhnValidator.kt @@ -1,29 +1,32 @@ package com.basistheory.android.view.validation -fun luhnValidator(value: String?): Boolean { - if (value.isNullOrEmpty() || value.any { !it.isDigit() }) return false +class LuhnValidator: ElementValidator() { - var sum = 0 - var isDoubled = false + override fun validate(value: String?): Boolean { + if (value.isNullOrEmpty() || value.any { !it.isDigit() }) return false - for (i in value.length - 1 downTo 0) { - val digit: Int = value[i] - '0' - if (digit < 0 || digit > 9) { - // Ignore non-digits - continue - } - var addend: Int - if (isDoubled) { - addend = digit * 2 - if (addend > 9) { - addend -= 9 + var sum = 0 + var isDoubled = false + + for (i in value.length - 1 downTo 0) { + val digit: Int = value[i] - '0' + if (digit < 0 || digit > 9) { + // Ignore non-digits + continue + } + var addend: Int + if (isDoubled) { + addend = digit * 2 + if (addend > 9) { + addend -= 9 + } + } else { + addend = digit } - } else { - addend = digit + sum += addend + isDoubled = !isDoubled } - sum += addend - isDoubled = !isDoubled - } - return sum % 10 == 0 + return sum % 10 == 0 + } } diff --git a/lib/src/main/java/com/basistheory/android/view/validation/RegexValidator.kt b/lib/src/main/java/com/basistheory/android/view/validation/RegexValidator.kt new file mode 100644 index 00000000..a5134028 --- /dev/null +++ b/lib/src/main/java/com/basistheory/android/view/validation/RegexValidator.kt @@ -0,0 +1,12 @@ +package com.basistheory.android.view.validation + +class RegexValidator( + private val regex: Regex +): ElementValidator() { + + constructor(pattern: String): this(Regex(pattern)) + + override fun validate(value: String?): Boolean { + return regex.matches(value ?: "") + } +} diff --git a/lib/src/test/java/com/basistheory/android/event/ChangeEventTests.kt b/lib/src/test/java/com/basistheory/android/event/ChangeEventTests.kt index 61b2dcee..8dbb17f5 100644 --- a/lib/src/test/java/com/basistheory/android/event/ChangeEventTests.kt +++ b/lib/src/test/java/com/basistheory/android/event/ChangeEventTests.kt @@ -2,6 +2,8 @@ package com.basistheory.android.event import android.app.Activity import com.basistheory.android.view.TextElement +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.validation.RegexValidator import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -87,7 +89,7 @@ class ChangeEventTests { fun `ChangeEvent computes isComplete based on mask`() { val changeEvents = mutableListOf() - textElement.mask = listOf(Regex("""\d"""), Regex("""\d""")) + textElement.mask = ElementMask(listOf(Regex("""\d"""), Regex("""\d"""))) textElement.addChangeEventListener { changeEvents.add(it) } textElement.setText("1") textElement.setText("12") @@ -113,7 +115,7 @@ class ChangeEventTests { fun `ChangeEvent computes isValid based on validator`() { val changeEvents = mutableListOf() - textElement.validate = { (it?.length ?: 0) % 2 == 0 } + textElement.validator = RegexValidator("""\d{2}""") textElement.addChangeEventListener { changeEvents.add(it) } textElement.setText("1") textElement.setText("12") diff --git a/lib/src/test/java/com/basistheory/android/service/CardBrandEnricherTests.kt b/lib/src/test/java/com/basistheory/android/service/CardBrandEnricherTests.kt index ed1bb907..ab7c74de 100644 --- a/lib/src/test/java/com/basistheory/android/service/CardBrandEnricherTests.kt +++ b/lib/src/test/java/com/basistheory/android/service/CardBrandEnricherTests.kt @@ -20,8 +20,8 @@ class CardBrandEnricherTests { value = [ "4242424242424242, visa, ${CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH}", "5555555555555555, mastercard, ${CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_16LENGTH}", - "378282246310005, americanExpress, #### ###### #####", - "36227206271667, dinersClub, #### ###### #########", + "378282246310005, americanExpress, ${CardBrandEnricher.CardMasks.MASK_4_10GAPS_15LENGTH}", + "36227206271667, dinersClub, ${CardBrandEnricher.CardMasks.MASK_4_10GAPS_19LENGTH}", "6011000990139424, discover, ${CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH}", "3566495867324859, jcb, ${CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH}", "620000000000000, unionPay, ${CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH}", diff --git a/lib/src/test/java/com/basistheory/android/view/CardNumberElementTests.kt b/lib/src/test/java/com/basistheory/android/view/CardNumberElementTests.kt index 80dfa32d..53493ea2 100644 --- a/lib/src/test/java/com/basistheory/android/view/CardNumberElementTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/CardNumberElementTests.kt @@ -3,6 +3,7 @@ package com.basistheory.android.view import android.app.Activity import com.basistheory.android.event.ChangeEvent import com.basistheory.android.service.CardBrandEnricher +import com.basistheory.android.view.mask.ElementMask import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,9 +39,17 @@ class CardNumberElementTests { @Test fun `applies mask based on card brand`() { - cardNumberElement.setText("4242abc4242def4242geh4242") - expectThat(cardNumberElement.mask?.joinToString("")) - .isEqualTo(CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH) + cardNumberElement.setText("4242424242424242") + expectThat(cardNumberElement.mask) + .isEqualTo(ElementMask(CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_19LENGTH)) + + cardNumberElement.setText("5555555555554444") + expectThat(cardNumberElement.mask) + .isEqualTo(ElementMask(CardBrandEnricher.CardMasks.MASK_4_8_12GAPS_16LENGTH)) + + cardNumberElement.setText("371449635398431") + expectThat(cardNumberElement.mask) + .isEqualTo(ElementMask(CardBrandEnricher.CardMasks.MASK_4_10GAPS_15LENGTH)) } @Test diff --git a/lib/src/test/java/com/basistheory/android/view/TextElementTests.kt b/lib/src/test/java/com/basistheory/android/view/TextElementTests.kt index 5ca89e33..dc3e38b1 100644 --- a/lib/src/test/java/com/basistheory/android/view/TextElementTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/TextElementTests.kt @@ -1,14 +1,16 @@ package com.basistheory.android.view import android.app.Activity -import com.basistheory.android.view.transform.regexReplaceElementTransform +import com.basistheory.android.event.ChangeEvent +import com.basistheory.android.view.mask.ElementMask +import com.basistheory.android.view.transform.RegexReplaceElementTransform import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import strikt.api.expectThat -import strikt.assertions.isEqualTo +import strikt.assertions.* @RunWith(RobolectricTestRunner::class) class TextElementTests { @@ -22,7 +24,7 @@ class TextElementTests { @Test fun `can clear the value`() { - textElement.transform = regexReplaceElementTransform(Regex("[\\s]")) + textElement.transform = RegexReplaceElementTransform(Regex("[\\s]")) textElement.setText(null) expectThat(textElement.getText()).isEqualTo("") // note: EditText transforms nulls to "" @@ -33,7 +35,7 @@ class TextElementTests { @Test fun `can apply transform`() { - textElement.transform = regexReplaceElementTransform(Regex("[()\\-\\s]")) + textElement.transform = RegexReplaceElementTransform(Regex("[()\\-\\s]")) textElement.setText("(123) 456-7890") expectThat(textElement.getText()).isEqualTo("1234567890") @@ -41,15 +43,15 @@ class TextElementTests { @Test fun `transform can be updated and text is transformed just in time`() { - textElement.transform = regexReplaceElementTransform(Regex("[^\\d]")) + textElement.transform = RegexReplaceElementTransform(Regex("[^\\d]")) textElement.setText("(1") expectThat(textElement.getText()).isEqualTo("1") - textElement.transform = regexReplaceElementTransform(Regex("[()\\s2]")) + textElement.transform = RegexReplaceElementTransform(Regex("[()\\s2]")) textElement.setText("(123) 4") expectThat(textElement.getText()).isEqualTo("134") - textElement.transform = regexReplaceElementTransform(Regex("[()]")) + textElement.transform = RegexReplaceElementTransform(Regex("[()]")) textElement.setText("(123) 456-7890") expectThat(textElement.getText()).isEqualTo("123 456-7890") } @@ -57,8 +59,25 @@ class TextElementTests { @Test fun `can apply mask`() { val digitRegex = Regex("""\d""") - textElement.mask = listOf("+", "1", "(", digitRegex,digitRegex,digitRegex, ")", " ", digitRegex, digitRegex, digitRegex, "-", digitRegex, digitRegex , digitRegex, digitRegex ) + textElement.mask = ElementMask( + listOf("+", "1", "(", digitRegex,digitRegex,digitRegex, ")", " ", digitRegex, digitRegex, digitRegex, "-", digitRegex, digitRegex , digitRegex, digitRegex ) + ) textElement.setText("2345678900") expectThat(textElement.getText()).isEqualTo("+1(234) 567-8900") } + + @Test + fun `can use TextElement without a mask, transform, or validator`() { + val changeEvents = mutableListOf() + + textElement.addChangeEventListener { changeEvents.add(it) } + + textElement.setText("123") + expectThat(textElement.getText()).isEqualTo("123") + + expectThat(changeEvents).single().and { + get { isComplete }.isFalse() + get { isValid }.isTrue() + } + } } diff --git a/lib/src/test/java/com/basistheory/android/view/mask/MaskTests.kt b/lib/src/test/java/com/basistheory/android/view/mask/ElementMaskTests.kt similarity index 89% rename from lib/src/test/java/com/basistheory/android/view/mask/MaskTests.kt rename to lib/src/test/java/com/basistheory/android/view/mask/ElementMaskTests.kt index 9c53f198..2158a9e7 100644 --- a/lib/src/test/java/com/basistheory/android/view/mask/MaskTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/mask/ElementMaskTests.kt @@ -8,32 +8,32 @@ import strikt.api.expectCatching import strikt.api.expectThat import strikt.assertions.* -class MaskTests { +class ElementMaskTests { @Test fun `throws IllegalArgumentException when mask contains strings longer than 1 char`() { - expectCatching { Mask(listOf("foo")) } + expectCatching { ElementMask(listOf("foo")) } .isFailure() .isA() } @Test fun `throws IllegalArgumentException when mask contains illegal mask input`() { - expectCatching { Mask(listOf('f', Regex("."), object {})) } + expectCatching { ElementMask(listOf('f', Regex("."), object {})) } .isFailure() .isA() } @Test fun `throws IllegalArgumentException when mask contains empty string`() { - expectCatching { Mask(listOf("")) } + expectCatching { ElementMask(listOf("")) } .isFailure() .isA() } @Test fun `throws IllegalArgumentException when mask is empty`() { - expectCatching { Mask(emptyList()) } + expectCatching { ElementMask(emptyList()) } .isFailure() .isA() } @@ -59,7 +59,7 @@ class MaskTests { digitRegex, digitRegex ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) val maskedValue = mask.evaluate("2345678900", InputAction.INSERT) expect { @@ -75,7 +75,7 @@ class MaskTests { digitRegex, digitRegex ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.isComplete("2")).isFalse() @@ -98,7 +98,7 @@ class MaskTests { "-", charRegex, ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expectThat(mask.evaluate("e2e2e", InputAction.INSERT)).isEqualTo("e-2-e-2-e") } @@ -111,7 +111,7 @@ class MaskTests { "_", charRegex ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.evaluate("AB", InputAction.INSERT)).isEqualTo("A_B") @@ -131,7 +131,7 @@ class MaskTests { digitRegex, ")" ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.evaluate("+1(1", InputAction.INSERT)).isEqualTo("+1(1") @@ -147,7 +147,7 @@ class MaskTests { "-", anyRegex ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.evaluate("12", InputAction.INSERT)).isEqualTo("1-2") @@ -170,7 +170,7 @@ class MaskTests { "-", charRegex ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.evaluate("ee23dd", InputAction.INSERT)).isEqualTo("e-2-d") @@ -182,7 +182,7 @@ class MaskTests { @Test fun `mask with numeric placeholders is applied correctly`() { val maskPattern = listOf("#", "#", "#", "-", '#', '#', "-", "#", "#", "#", "#") - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expectThat( mask.evaluate( @@ -195,7 +195,7 @@ class MaskTests { @Test fun `mask with mixed placeholders is applied correctly`() { val maskPattern = listOf("#", "#", "#", "-", "x", "x", "-", "*", "*", "*", "*") - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that( @@ -226,7 +226,7 @@ class MaskTests { Regex("""\d"""), ")" ) - val mask = Mask(maskPattern) + val mask = ElementMask(maskPattern) expect { that(mask.evaluate("", InputAction.INSERT)).isEqualTo("") diff --git a/lib/src/test/java/com/basistheory/android/view/transform/RegexReplaceElementTransformTests.kt b/lib/src/test/java/com/basistheory/android/view/transform/RegexReplaceElementTransformTests.kt index a655649d..b0dc9203 100644 --- a/lib/src/test/java/com/basistheory/android/view/transform/RegexReplaceElementTransformTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/transform/RegexReplaceElementTransformTests.kt @@ -11,30 +11,30 @@ import strikt.assertions.isNull class regexReplaceElementTransformTests { @Test fun `null values are not transformed`() { - val transform = regexReplaceElementTransform(Regex("[\\s]")) + val transform = RegexReplaceElementTransform(Regex("[\\s]")) - expectThat(transform(null)).isNull() + expectThat(transform.apply(null)).isNull() } @Test fun `empty values are not transformed`() { - val transform = regexReplaceElementTransform(Regex("[\\s]")) + val transform = RegexReplaceElementTransform(Regex("[\\s]")) - expectThat(transform("")).isEqualTo("") + expectThat(transform.apply("")).isEqualTo("") } @Test fun `text is not transformed when transform regex has no match`() { - val transform = regexReplaceElementTransform(Regex("[\\s]")) + val transform = RegexReplaceElementTransform(Regex("[\\s]")) - expectThat(transform("foo123")).isEqualTo("foo123") + expectThat(transform.apply("foo123")).isEqualTo("foo123") } @Test fun `text is transformed when transform regex has matches`() { - val transform = regexReplaceElementTransform(Regex("[^\\d]")) + val transform = RegexReplaceElementTransform(Regex("[^\\d]")) - expectThat(transform("(123) 456-7890")).isEqualTo("1234567890") + expectThat(transform.apply("(123) 456-7890")).isEqualTo("1234567890") } @Test @@ -44,7 +44,7 @@ class regexReplaceElementTransformTests { // design/compile-time validation val invalidRegex = "*".plus("[") - expectCatching { regexReplaceElementTransform(Regex(invalidRegex)) } + expectCatching { RegexReplaceElementTransform(Regex(invalidRegex)) } .isFailure() .isA() } diff --git a/lib/src/test/java/com/basistheory/android/view/validation/FutureDateValidationTests.kt b/lib/src/test/java/com/basistheory/android/view/validation/FutureDateValidationTests.kt index 80346b71..f036062a 100644 --- a/lib/src/test/java/com/basistheory/android/view/validation/FutureDateValidationTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/validation/FutureDateValidationTests.kt @@ -7,43 +7,44 @@ import strikt.assertions.isFalse import strikt.assertions.isTrue class FutureDateValidationTests { + private val validator = FutureDateValidator() @Test fun `returns false for null or empty values`() { expect { - that(futureDateValidator("")).isFalse() - that(futureDateValidator(null)).isFalse() + that(validator.validate("")).isFalse() + that(validator.validate(null)).isFalse() } } @Test fun `returns false for incomplete values`() { expect { - that(futureDateValidator("01")).isFalse() - that(futureDateValidator("01/")).isFalse() - that(futureDateValidator("/")).isFalse() - that(futureDateValidator("/22")).isFalse() + that(validator.validate("01")).isFalse() + that(validator.validate("01/")).isFalse() + that(validator.validate("/")).isFalse() + that(validator.validate("/22")).isFalse() } } @Test fun `returns false for invalid values`() { expect { - that(futureDateValidator("invalid")).isFalse() - that(futureDateValidator("1902873")).isFalse() - that(futureDateValidator("12//25")).isFalse() - that(futureDateValidator("00/00")).isFalse() - that(futureDateValidator("99/25")).isFalse() + that(validator.validate("invalid")).isFalse() + that(validator.validate("1902873")).isFalse() + that(validator.validate("12//25")).isFalse() + that(validator.validate("00/00")).isFalse() + that(validator.validate("99/25")).isFalse() } } @Test fun `returns true for valid dates in the past`() { expect { - that(futureDateValidator("12/12")).isFalse() - that(futureDateValidator("01/19")).isFalse() - that(futureDateValidator("01/00")).isFalse() - that(futureDateValidator("1/2")).isFalse() + that(validator.validate("12/12")).isFalse() + that(validator.validate("01/19")).isFalse() + that(validator.validate("01/00")).isFalse() + that(validator.validate("1/2")).isFalse() } } @@ -51,10 +52,12 @@ class FutureDateValidationTests { fun `returns true for valid dates in the future`() { // note: we inject the "current" time to avoid test flakiness over time val currentDate = LocalDate.of(2022, 6, 1) + val validator = FutureDateValidator(currentDate) + expect { - that(futureDateValidator("06/25", currentDate)).isTrue() - that(futureDateValidator("07/22", currentDate)).isTrue() - that(futureDateValidator("7/22", currentDate)).isTrue() + that(validator.validate("06/25", )).isTrue() + that(validator.validate("07/22")).isTrue() + that(validator.validate("7/22")).isTrue() } } @@ -63,22 +66,16 @@ class FutureDateValidationTests { // note: we inject the "current" time to avoid test flakiness over time expect { that( - futureDateValidator( - "12/22", - LocalDate.of(2022, 12, 1) - ) + FutureDateValidator(LocalDate.of(2022, 12, 1)) + .validate("12/22") ).isTrue() that( - futureDateValidator( - "12/22", - LocalDate.of(2022, 12, 10) - ) + FutureDateValidator(LocalDate.of(2022, 12, 10)) + .validate("12/22") ).isTrue() that( - futureDateValidator( - "12/22", - LocalDate.of(2022, 12, 31) - ) + FutureDateValidator(LocalDate.of(2022, 12, 31)) + .validate("12/22") ).isTrue() } } @@ -86,8 +83,8 @@ class FutureDateValidationTests { @Test fun `defaults to using current date`() { expect { - that(futureDateValidator("11/22")).isFalse() - that(futureDateValidator("11/99")).isTrue() // far enough into the future this should never fail + that(validator.validate("11/22")).isFalse() + that(validator.validate("11/99")).isTrue() // far enough into the future this should never fail } } } \ No newline at end of file diff --git a/lib/src/test/java/com/basistheory/android/view/validation/LuhnValidatorTests.kt b/lib/src/test/java/com/basistheory/android/view/validation/LuhnValidatorTests.kt index 496b26c7..4533b1e4 100644 --- a/lib/src/test/java/com/basistheory/android/view/validation/LuhnValidatorTests.kt +++ b/lib/src/test/java/com/basistheory/android/view/validation/LuhnValidatorTests.kt @@ -6,38 +6,39 @@ import strikt.assertions.isFalse import strikt.assertions.isTrue class LuhnValidatorTests { + private val validator = LuhnValidator() @Test fun `should return true for valid card numbers`() { expect { - that(luhnValidator("4242424242424242")).isTrue() - that(luhnValidator("5555555555554444")).isTrue() - that(luhnValidator("6011000990139424")).isTrue() - that(luhnValidator("378282246310005")).isTrue() + that(validator.validate("4242424242424242")).isTrue() + that(validator.validate("5555555555554444")).isTrue() + that(validator.validate("6011000990139424")).isTrue() + that(validator.validate("378282246310005")).isTrue() } } @Test fun `should return false for empty card numbers`() { expect { - that(luhnValidator("")).isFalse() - that(luhnValidator(null)).isFalse() + that(validator.validate("")).isFalse() + that(validator.validate(null)).isFalse() } } @Test fun `should return false for non numeric values`() { expect { - that(luhnValidator("foo")).isFalse() - that(luhnValidator("asdf123l;kj")).isFalse() + that(validator.validate("foo")).isFalse() + that(validator.validate("asdf123l;kj")).isFalse() } } @Test fun `should return false for non-Luhn valid cards`() { expect { - that(luhnValidator("5200828282828211")).isFalse() - that(luhnValidator("5555555555554443")).isFalse() + that(validator.validate("5200828282828211")).isFalse() + that(validator.validate("5555555555554443")).isFalse() } } } \ No newline at end of file diff --git a/lib/src/test/java/com/basistheory/android/view/validation/RegexValidatorTests.kt b/lib/src/test/java/com/basistheory/android/view/validation/RegexValidatorTests.kt new file mode 100644 index 00000000..19fe5585 --- /dev/null +++ b/lib/src/test/java/com/basistheory/android/view/validation/RegexValidatorTests.kt @@ -0,0 +1,55 @@ +package com.basistheory.android.view.validation + +import org.junit.Test +import strikt.api.expect +import strikt.api.expectCatching +import strikt.assertions.isA +import strikt.assertions.isFailure +import strikt.assertions.isFalse +import strikt.assertions.isTrue + +class RegexValidatorTests { + + @Test + fun `should return true for values matching regex`() { + val validator = RegexValidator("""^\d{3,4}$""") + + expect { + that(validator.validate("123")).isTrue() + that(validator.validate("000")).isTrue() + that(validator.validate("1234")).isTrue() + that(validator.validate("0000")).isTrue() + } + } + + @Test + fun `should return false for values not matching regex`() { + val validator = RegexValidator("""^\d{3,4}$""") + + expect { + that(validator.validate("foo")).isFalse() + that(validator.validate("1")).isFalse() + that(validator.validate("12345")).isFalse() + that(validator.validate(" 123")).isFalse() + that(validator.validate("a123")).isFalse() + that(validator.validate("123z")).isFalse() + } + } + + @Test + fun `should return false for empty values`() { + val validator = RegexValidator("""^\d{3,4}$""") + + expect { + that(validator.validate("")).isFalse() + that(validator.validate(null)).isFalse() + } + } + + @Test + fun `throws when initialized with invalid regex pattern`() { + expectCatching { RegexValidator("[") } + .isFailure() + .isA() + } +} diff --git a/settings.gradle b/settings.gradle index 9d9faf39..684fa07c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,6 @@ dependencyResolutionManagement { maven { url 'https://jitpack.io' } } } -rootProject.name = "Mobile Elements" +rootProject.name = "basistheory-android" include ':example' include ':lib'