mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Credit card validator implementations and spec tests.
This commit is contained in:
+12
-3
@@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.components.settings.configure
|
|||||||
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
|
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||||
import org.thoughtcrime.securesms.util.Projection
|
import org.thoughtcrime.securesms.util.Projection
|
||||||
@@ -390,7 +391,7 @@ class DonateToSignalFragment : DSLSettingsFragment(
|
|||||||
when (gatewayResponse.gateway) {
|
when (gatewayResponse.gateway) {
|
||||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||||
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
|
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
|
||||||
GatewayResponse.Gateway.CREDIT_CARD -> error("Credit cards are not currently supported.")
|
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +427,7 @@ class DonateToSignalFragment : DSLSettingsFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
||||||
viewModel.provideGatewayRequest(gatewayResponse.request)
|
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
|
||||||
donationPaymentComponent.donationPaymentRepository.requestTokenFromGooglePay(
|
donationPaymentComponent.donationPaymentRepository.requestTokenFromGooglePay(
|
||||||
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
|
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
|
||||||
label = gatewayResponse.request.label,
|
label = gatewayResponse.request.label,
|
||||||
@@ -434,10 +435,18 @@ class DonateToSignalFragment : DSLSettingsFragment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchCreditCard(gatewayResponse: GatewayResponse) {
|
||||||
|
if (FeatureFlags.creditCardPayments()) {
|
||||||
|
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayResponse.request))
|
||||||
|
} else {
|
||||||
|
error("Credit cards are not currently enabled.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun registerGooglePayCallback() {
|
private fun registerGooglePayCallback() {
|
||||||
donationPaymentComponent.googlePayResultPublisher.subscribeBy(
|
donationPaymentComponent.googlePayResultPublisher.subscribeBy(
|
||||||
onNext = { paymentResult ->
|
onNext = { paymentResult ->
|
||||||
viewModel.consumeGatewayRequest()?.let {
|
viewModel.consumeGatewayRequestForGooglePay()?.let {
|
||||||
donationPaymentComponent.donationPaymentRepository.onActivityResult(
|
donationPaymentComponent.donationPaymentRepository.onActivityResult(
|
||||||
paymentResult.requestCode,
|
paymentResult.requestCode,
|
||||||
paymentResult.resultCode,
|
paymentResult.resultCode,
|
||||||
|
|||||||
+2
-2
@@ -348,13 +348,13 @@ class DonateToSignalViewModel(
|
|||||||
store.dispose()
|
store.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun provideGatewayRequest(request: GatewayRequest) {
|
fun provideGatewayRequestForGooglePay(request: GatewayRequest) {
|
||||||
Log.d(TAG, "Provided with a gateway request.")
|
Log.d(TAG, "Provided with a gateway request.")
|
||||||
Preconditions.checkState(gatewayRequest == null)
|
Preconditions.checkState(gatewayRequest == null)
|
||||||
gatewayRequest = request
|
gatewayRequest = request
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumeGatewayRequest(): GatewayRequest? {
|
fun consumeGatewayRequestForGooglePay(): GatewayRequest? {
|
||||||
val request = gatewayRequest
|
val request = gatewayRequest
|
||||||
gatewayRequest = null
|
gatewayRequest = null
|
||||||
return request
|
return request
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
|
||||||
|
object CreditCardCodeValidator {
|
||||||
|
|
||||||
|
fun getValidity(code: String, cardType: CreditCardType, isFocused: Boolean): Validity {
|
||||||
|
val validLength = if (cardType == CreditCardType.AMERICAN_EXPRESS) {
|
||||||
|
4
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
!code.isDigitsOnly() -> Validity.INVALID_CHARACTERS
|
||||||
|
code.length > validLength -> Validity.TOO_LONG
|
||||||
|
code.length < validLength && isFocused -> Validity.POTENTIALLY_VALID
|
||||||
|
code.isEmpty() -> Validity.POTENTIALLY_VALID
|
||||||
|
code.length < validLength -> Validity.TOO_SHORT
|
||||||
|
else -> Validity.FULLY_VALID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Validity {
|
||||||
|
TOO_LONG,
|
||||||
|
TOO_SHORT,
|
||||||
|
INVALID_CHARACTERS,
|
||||||
|
POTENTIALLY_VALID,
|
||||||
|
FULLY_VALID
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
data class CreditCardExpiration(
|
||||||
|
val month: String = "",
|
||||||
|
val year: String = ""
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean {
|
||||||
|
return month.isEmpty() && year.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInput(expiration: String): CreditCardExpiration {
|
||||||
|
val expirationParts = expiration.split("/", limit = 2)
|
||||||
|
val month = expirationParts.first()
|
||||||
|
val year = expirationParts.drop(1).firstOrNull() ?: ""
|
||||||
|
|
||||||
|
return CreditCardExpiration(month, year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
|
||||||
|
object CreditCardExpirationValidator {
|
||||||
|
|
||||||
|
fun getValidity(creditCardExpiration: CreditCardExpiration, currentMonth: Int, currentYear: Int, isFocused: Boolean): Validity {
|
||||||
|
if (creditCardExpiration.isEmpty()) {
|
||||||
|
return Validity.POTENTIALLY_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
val monthValidity = isExpirationMonthValid(creditCardExpiration.month, isFocused)
|
||||||
|
val yearValidity = isExpirationYearValid(creditCardExpiration.year, isFocused)
|
||||||
|
|
||||||
|
if (monthValidity.isInvalid) {
|
||||||
|
return monthValidity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yearValidity.isInvalid) {
|
||||||
|
return yearValidity
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Validity.POTENTIALLY_VALID in listOf(monthValidity, yearValidity)) {
|
||||||
|
return Validity.POTENTIALLY_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputMonthInt = creditCardExpiration.month.toInt()
|
||||||
|
val inputYearIntTwoDigits = creditCardExpiration.year.toInt()
|
||||||
|
val century = currentYear.floorDiv(100) * 100
|
||||||
|
var inputYearInt = inputYearIntTwoDigits + century
|
||||||
|
if (inputYearInt < currentYear) {
|
||||||
|
// This is for edge-of-century scenarios. If the year is 2099 and the user inputs '01' for the year, this lets us interpret that as 2101.
|
||||||
|
inputYearInt += 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (inputYearInt == currentYear) {
|
||||||
|
if (inputMonthInt < currentMonth) {
|
||||||
|
Validity.INVALID_EXPIRED
|
||||||
|
} else {
|
||||||
|
Validity.FULLY_VALID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (inputYearInt > currentYear + 20) {
|
||||||
|
Validity.INVALID_EXPIRED
|
||||||
|
} else {
|
||||||
|
Validity.FULLY_VALID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isExpirationMonthValid(inputMonth: String, isFocused: Boolean): Validity {
|
||||||
|
if (inputMonth.length > 2 || !inputMonth.isDigitsOnly()) {
|
||||||
|
return Validity.INVALID_MONTH
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputMonth in listOf("", "0")) {
|
||||||
|
return if (isFocused) {
|
||||||
|
Validity.POTENTIALLY_VALID
|
||||||
|
} else {
|
||||||
|
Validity.INVALID_MONTH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val month = inputMonth.toInt()
|
||||||
|
if (month in 1..12) {
|
||||||
|
return Validity.FULLY_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validity.INVALID_MONTH
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isExpirationYearValid(inputYear: String, isFocused: Boolean): Validity {
|
||||||
|
if (inputYear.length > 2 || !inputYear.isDigitsOnly()) {
|
||||||
|
return Validity.INVALID_YEAR
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputYear.length < 2) {
|
||||||
|
return if (isFocused) {
|
||||||
|
Validity.POTENTIALLY_VALID
|
||||||
|
} else if (inputYear.isEmpty()) {
|
||||||
|
Validity.INVALID_MISSING_YEAR
|
||||||
|
} else {
|
||||||
|
Validity.INVALID_YEAR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validity.FULLY_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Validity(val isInvalid: Boolean) {
|
||||||
|
INVALID_EXPIRED(true),
|
||||||
|
INVALID_MISSING_YEAR(true),
|
||||||
|
INVALID_MONTH(true),
|
||||||
|
INVALID_YEAR(true),
|
||||||
|
POTENTIALLY_VALID(false),
|
||||||
|
FULLY_VALID(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
data class CreditCardFormState(
|
||||||
|
val focusedField: FocusedField = FocusedField.NONE,
|
||||||
|
val number: String = "",
|
||||||
|
val expiration: CreditCardExpiration = CreditCardExpiration(),
|
||||||
|
val code: String = ""
|
||||||
|
) {
|
||||||
|
enum class FocusedField {
|
||||||
|
NONE,
|
||||||
|
NUMBER,
|
||||||
|
EXPIRATION,
|
||||||
|
CODE
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||||
|
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
|
||||||
|
class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||||
|
|
||||||
|
private val binding by ViewBinderDelegate(CreditCardFragmentBinding::bind)
|
||||||
|
private val args: CreditCardFragmentArgs by navArgs()
|
||||||
|
private val viewModel: CreditCardViewModel by viewModels()
|
||||||
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
binding.cardNumber.addTextChangedListener(afterTextChanged = {
|
||||||
|
viewModel.onNumberChanged(it?.toString() ?: "")
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.cardNumber.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
viewModel.onNumberFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cardCvv.addTextChangedListener(afterTextChanged = {
|
||||||
|
viewModel.onCodeChanged(it?.toString() ?: "")
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.cardCvv.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
viewModel.onCodeFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cardExpiry.addTextChangedListener(afterTextChanged = {
|
||||||
|
viewModel.onExpirationChanged(it?.toString() ?: "")
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.cardExpiry.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
viewModel.onExpirationFocusChanged(hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||||
|
lifecycleDisposable += viewModel.state.subscribe {
|
||||||
|
// TODO [alex] -- type
|
||||||
|
// TODO [alex] -- all fields valid
|
||||||
|
presentCardNumberWrapper(it.numberValidity)
|
||||||
|
presentCardExpiryWrapper(it.expirationValidity)
|
||||||
|
presentCardCodeWrapper(it.codeValidity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
when (viewModel.currentFocusField) {
|
||||||
|
CreditCardFormState.FocusedField.NONE -> ViewUtil.focusAndShowKeyboard(binding.cardNumber)
|
||||||
|
CreditCardFormState.FocusedField.NUMBER -> ViewUtil.focusAndShowKeyboard(binding.cardNumber)
|
||||||
|
CreditCardFormState.FocusedField.EXPIRATION -> ViewUtil.focusAndShowKeyboard(binding.cardExpiry)
|
||||||
|
CreditCardFormState.FocusedField.CODE -> ViewUtil.focusAndShowKeyboard(binding.cardCvv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun presentCardNumberWrapper(validity: CreditCardNumberValidator.Validity) {
|
||||||
|
val errorState = when (validity) {
|
||||||
|
CreditCardNumberValidator.Validity.INVALID -> ErrorState(messageResId = R.string.CreditCardFragment__invalid_card_number)
|
||||||
|
CreditCardNumberValidator.Validity.POTENTIALLY_VALID -> NO_ERROR
|
||||||
|
CreditCardNumberValidator.Validity.FULLY_VALID -> NO_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cardNumberWrapper.error = errorState.resolveErrorText(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun presentCardExpiryWrapper(validity: CreditCardExpirationValidator.Validity) {
|
||||||
|
val errorState = when (validity) {
|
||||||
|
CreditCardExpirationValidator.Validity.INVALID_EXPIRED -> ErrorState(messageResId = R.string.CreditCardFragment__card_has_expired)
|
||||||
|
CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR -> ErrorState(messageResId = R.string.CreditCardFragment__year_required)
|
||||||
|
CreditCardExpirationValidator.Validity.INVALID_MONTH -> ErrorState(messageResId = R.string.CreditCardFragment__invalid_month)
|
||||||
|
CreditCardExpirationValidator.Validity.INVALID_YEAR -> ErrorState(messageResId = R.string.CreditCardFragment__invalid_year)
|
||||||
|
CreditCardExpirationValidator.Validity.POTENTIALLY_VALID -> NO_ERROR
|
||||||
|
CreditCardExpirationValidator.Validity.FULLY_VALID -> NO_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cardExpiryWrapper.error = errorState.resolveErrorText(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun presentCardCodeWrapper(validity: CreditCardCodeValidator.Validity) {
|
||||||
|
val errorState = when (validity) {
|
||||||
|
CreditCardCodeValidator.Validity.TOO_LONG -> ErrorState(messageResId = R.string.CreditCardFragment__code_is_too_long)
|
||||||
|
CreditCardCodeValidator.Validity.TOO_SHORT -> ErrorState(messageResId = R.string.CreditCardFragment__code_is_too_short)
|
||||||
|
CreditCardCodeValidator.Validity.INVALID_CHARACTERS -> ErrorState(messageResId = R.string.CreditCardFragment__invalid_code)
|
||||||
|
CreditCardCodeValidator.Validity.POTENTIALLY_VALID -> NO_ERROR
|
||||||
|
CreditCardCodeValidator.Validity.FULLY_VALID -> NO_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.cardCvvWrapper.error = errorState.resolveErrorText(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ErrorState(
|
||||||
|
private val isEnabled: Boolean = true,
|
||||||
|
@StringRes private val messageResId: Int
|
||||||
|
) {
|
||||||
|
fun resolveErrorText(context: Context): String? {
|
||||||
|
return if (isEnabled) {
|
||||||
|
context.getString(messageResId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val NO_ERROR = ErrorState(false, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
|
||||||
|
object CreditCardNumberValidator {
|
||||||
|
|
||||||
|
private const val MAX_CARD_NUMBER_LENGTH = 19
|
||||||
|
private const val MIN_CARD_NUMBER_LENGTH = 12
|
||||||
|
|
||||||
|
fun getValidity(cardNumber: String, isCardNumberFieldFocused: Boolean): Validity {
|
||||||
|
if (cardNumber.length > MAX_CARD_NUMBER_LENGTH || !cardNumber.isDigitsOnly()) {
|
||||||
|
return Validity.INVALID
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardNumber.length < MIN_CARD_NUMBER_LENGTH) {
|
||||||
|
return Validity.POTENTIALLY_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
val isValid = CreditCardType.fromCardNumber(cardNumber) == CreditCardType.UNIONPAY || isLuhnValid(cardNumber)
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isValid -> Validity.FULLY_VALID
|
||||||
|
isCardNumberFieldFocused -> Validity.POTENTIALLY_VALID
|
||||||
|
else -> Validity.INVALID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of the [Luhn Algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm) which
|
||||||
|
* performs a checksum to check for validity of non-Unionpay cards.
|
||||||
|
*/
|
||||||
|
private fun isLuhnValid(cardNumber: String): Boolean {
|
||||||
|
var checksum = 0
|
||||||
|
var double = false
|
||||||
|
|
||||||
|
cardNumber.reversed().forEach { char ->
|
||||||
|
var digit = char.digitToInt()
|
||||||
|
if (double) {
|
||||||
|
digit *= 2
|
||||||
|
}
|
||||||
|
double = !double
|
||||||
|
if (digit >= 10) {
|
||||||
|
digit -= 9
|
||||||
|
}
|
||||||
|
checksum += digit
|
||||||
|
}
|
||||||
|
|
||||||
|
return checksum % 10 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Validity {
|
||||||
|
INVALID,
|
||||||
|
POTENTIALLY_VALID,
|
||||||
|
FULLY_VALID
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
enum class CreditCardType {
|
||||||
|
AMERICAN_EXPRESS,
|
||||||
|
UNIONPAY,
|
||||||
|
OTHER;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val AMERICAN_EXPRESS_PREFIXES = listOf("34", "37")
|
||||||
|
private val UNIONPAY_PREFIXES = listOf("62", "81")
|
||||||
|
|
||||||
|
fun fromCardNumber(cardNumber: String): CreditCardType {
|
||||||
|
return when {
|
||||||
|
AMERICAN_EXPRESS_PREFIXES.any { cardNumber.startsWith(it) } -> AMERICAN_EXPRESS
|
||||||
|
UNIONPAY_PREFIXES.any { cardNumber.startsWith(it) } -> UNIONPAY
|
||||||
|
else -> OTHER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
data class CreditCardValidationState(
|
||||||
|
val type: CreditCardType,
|
||||||
|
val numberValidity: CreditCardNumberValidator.Validity,
|
||||||
|
val expirationValidity: CreditCardExpirationValidator.Validity,
|
||||||
|
val codeValidity: CreditCardCodeValidator.Validity
|
||||||
|
)
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||||
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
class CreditCardViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val formStore = RxStore(CreditCardFormState())
|
||||||
|
private val validationProcessor: BehaviorProcessor<CreditCardValidationState> = BehaviorProcessor.create()
|
||||||
|
private val currentYear: Int
|
||||||
|
private val currentMonth: Int
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
|
||||||
|
currentYear = calendar.get(Calendar.YEAR)
|
||||||
|
currentMonth = calendar.get(Calendar.MONTH) + 1
|
||||||
|
|
||||||
|
disposables += formStore.stateFlowable.subscribe { formState ->
|
||||||
|
val type = CreditCardType.fromCardNumber(formState.number)
|
||||||
|
validationProcessor.onNext(
|
||||||
|
CreditCardValidationState(
|
||||||
|
type = type,
|
||||||
|
numberValidity = CreditCardNumberValidator.getValidity(formState.number, formState.focusedField == CreditCardFormState.FocusedField.NUMBER),
|
||||||
|
expirationValidity = CreditCardExpirationValidator.getValidity(formState.expiration, currentMonth, currentYear, formState.focusedField == CreditCardFormState.FocusedField.EXPIRATION),
|
||||||
|
codeValidity = CreditCardCodeValidator.getValidity(formState.code, type, formState.focusedField == CreditCardFormState.FocusedField.CODE)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val state: Flowable<CreditCardValidationState> = validationProcessor.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
|
||||||
|
val currentFocusField: CreditCardFormState.FocusedField
|
||||||
|
get() = formStore.state.focusedField
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
disposables.clear()
|
||||||
|
formStore.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNumberChanged(number: String) {
|
||||||
|
formStore.update {
|
||||||
|
it.copy(number = number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNumberFocusChanged(isFocused: Boolean) {
|
||||||
|
updateFocus(CreditCardFormState.FocusedField.NUMBER, isFocused)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onExpirationChanged(expiration: String) {
|
||||||
|
formStore.update {
|
||||||
|
it.copy(expiration = CreditCardExpiration.fromInput(expiration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onExpirationFocusChanged(isFocused: Boolean) {
|
||||||
|
updateFocus(CreditCardFormState.FocusedField.EXPIRATION, isFocused)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCodeChanged(code: String) {
|
||||||
|
formStore.update {
|
||||||
|
it.copy(code = code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCodeFocusChanged(isFocused: Boolean) {
|
||||||
|
updateFocus(CreditCardFormState.FocusedField.CODE, isFocused)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFocus(
|
||||||
|
newFocusedField: CreditCardFormState.FocusedField,
|
||||||
|
isFocused: Boolean
|
||||||
|
) {
|
||||||
|
formStore.update {
|
||||||
|
it.copy(focusedField = getUpdatedFocus(it.focusedField, newFocusedField, isFocused))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUpdatedFocus(
|
||||||
|
currentFocusedField: CreditCardFormState.FocusedField,
|
||||||
|
newFocusedField: CreditCardFormState.FocusedField,
|
||||||
|
isFocused: Boolean
|
||||||
|
): CreditCardFormState.FocusedField {
|
||||||
|
return if (currentFocusedField == newFocusedField && !isFocused) {
|
||||||
|
CreditCardFormState.FocusedField.NONE
|
||||||
|
} else if (isFocused) {
|
||||||
|
newFocusedField
|
||||||
|
} else {
|
||||||
|
currentFocusedField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -77,7 +77,20 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PayPal
|
// PayPal
|
||||||
|
|
||||||
// Credit Card
|
// Credit Card
|
||||||
|
if (state.isCreditCardAvailable) {
|
||||||
|
space(12.dp)
|
||||||
|
|
||||||
|
primaryButton(
|
||||||
|
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),
|
||||||
|
onClick = {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
val response = GatewayResponse(GatewayResponse.Gateway.CREDIT_CARD, args.request)
|
||||||
|
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
space(16.dp)
|
space(16.dp)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
|
||||||
data class GatewaySelectorState(
|
data class GatewaySelectorState(
|
||||||
val badge: Badge,
|
val badge: Badge,
|
||||||
val isGooglePayAvailable: Boolean = false,
|
val isGooglePayAvailable: Boolean = false,
|
||||||
val isPayPalAvailable: Boolean = false,
|
val isPayPalAvailable: Boolean = false,
|
||||||
val isCreditCardAvailable: Boolean = false
|
val isCreditCardAvailable: Boolean = FeatureFlags.creditCardPayments()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,8 +104,15 @@ public final class FeatureFlags {
|
|||||||
private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4";
|
private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4";
|
||||||
public static final String STORIES_LOCALE = "android.stories.locale.1";
|
public static final String STORIES_LOCALE = "android.stories.locale.1";
|
||||||
private static final String HIDE_CONTACTS = "android.hide.contacts";
|
private static final String HIDE_CONTACTS = "android.hide.contacts";
|
||||||
|
<<<<<<< HEAD
|
||||||
private static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
private static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
||||||
private static final String SMS_EXPORT_MEGAPHONE_DELAY_DAYS = "android.smsExport.megaphoneDelayDays";
|
private static final String SMS_EXPORT_MEGAPHONE_DELAY_DAYS = "android.smsExport.megaphoneDelayDays";
|
||||||
|
||||||| parent of 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
|
public static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
||||||
|
=======
|
||||||
|
public static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
||||||
|
public static final String CREDIT_CARD_PAYMENTS = "android.credit.card.payments";
|
||||||
|
>>>>>>> 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
@@ -162,8 +169,15 @@ public final class FeatureFlags {
|
|||||||
CDS_V2_COMPAT,
|
CDS_V2_COMPAT,
|
||||||
STORIES_LOCALE,
|
STORIES_LOCALE,
|
||||||
HIDE_CONTACTS,
|
HIDE_CONTACTS,
|
||||||
|
<<<<<<< HEAD
|
||||||
MEDIA_PREVIEW_V2,
|
MEDIA_PREVIEW_V2,
|
||||||
SMS_EXPORT_MEGAPHONE_DELAY_DAYS
|
SMS_EXPORT_MEGAPHONE_DELAY_DAYS
|
||||||
|
||||||| parent of 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
|
MEDIA_PREVIEW_V2
|
||||||
|
=======
|
||||||
|
MEDIA_PREVIEW_V2,
|
||||||
|
CREDIT_CARD_PAYMENTS
|
||||||
|
>>>>>>> 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
);
|
);
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -227,8 +241,15 @@ public final class FeatureFlags {
|
|||||||
CDS_V2_LOAD_TEST,
|
CDS_V2_LOAD_TEST,
|
||||||
CDS_V2_COMPAT,
|
CDS_V2_COMPAT,
|
||||||
STORIES,
|
STORIES,
|
||||||
|
<<<<<<< HEAD
|
||||||
MEDIA_PREVIEW_V2,
|
MEDIA_PREVIEW_V2,
|
||||||
SMS_EXPORT_MEGAPHONE_DELAY_DAYS
|
SMS_EXPORT_MEGAPHONE_DELAY_DAYS
|
||||||
|
||||||| parent of 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
|
MEDIA_PREVIEW_V2
|
||||||
|
=======
|
||||||
|
MEDIA_PREVIEW_V2,
|
||||||
|
CREDIT_CARD_PAYMENTS
|
||||||
|
>>>>>>> 90b7447a79 (Credit card validator implementations and spec tests.)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -595,6 +616,15 @@ public final class FeatureFlags {
|
|||||||
return getInteger(SMS_EXPORT_MEGAPHONE_DELAY_DAYS, 14);
|
return getInteger(SMS_EXPORT_MEGAPHONE_DELAY_DAYS, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we should allow credit card payments for donations
|
||||||
|
*
|
||||||
|
* WARNING: This feature is not done, and this should not be enabled.
|
||||||
|
*/
|
||||||
|
public static boolean creditCardPayments() {
|
||||||
|
return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING);
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/signal_m3_toolbar_height"
|
||||||
|
android:background="@null"
|
||||||
|
android:minHeight="@dimen/signal_m3_toolbar_height"
|
||||||
|
android:theme="?attr/settingsToolbarStyle"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navigationContentDescription="@string/DSLSettingsToolbar__navigate_up"
|
||||||
|
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||||
|
app:titleTextAppearance="@style/Signal.Text.TitleLarge" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:textAppearance="@style/Signal.Text.TitleLarge"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
|
tools:text="Donation amount: $20" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingHorizontal="32dp"
|
||||||
|
android:text="@string/CreditCardFragment__enter_your_card_information_below"
|
||||||
|
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/card_number_wrapper"
|
||||||
|
style="@style/Widget.Signal.TextInputLayout.FilledBox.ContactNameEditor"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="36dp"
|
||||||
|
android:hint="@string/CreditCardFragment__card_number"
|
||||||
|
app:boxStrokeColor="@color/signal_colorPrimary"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:hintTextColor="@color/signal_colorPrimary"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/card_number"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="19"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/card_expiry_wrapper"
|
||||||
|
style="@style/Widget.Signal.TextInputLayout.FilledBox.ContactNameEditor"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:hint="@string/CreditCardFragment__mm_yy"
|
||||||
|
android:paddingEnd="18dp"
|
||||||
|
app:boxStrokeColor="@color/signal_colorPrimary"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:hintTextColor="@color/signal_colorPrimary"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/card_cvv_wrapper"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/card_number_wrapper">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/card_expiry"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="datetime|date"
|
||||||
|
android:maxLength="5"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:nextFocusDown="@id/card_cvv" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/card_cvv_wrapper"
|
||||||
|
style="@style/Widget.Signal.TextInputLayout.FilledBox.ContactNameEditor"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:hint="@string/CreditCardFragment__cvv"
|
||||||
|
android:paddingStart="18dp"
|
||||||
|
app:boxStrokeColor="@color/signal_colorPrimary"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:hintTextColor="@color/signal_colorPrimary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/card_expiry_wrapper"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/card_number_wrapper">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/card_cvv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="4"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -36,6 +36,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_donateToSignalFragment_to_subscribeLearnMoreBottomSheetDialog"
|
android:id="@+id/action_donateToSignalFragment_to_subscribeLearnMoreBottomSheetDialog"
|
||||||
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
app:destination="@id/subscribeLearnMoreBottomSheetDialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_donateToSignalFragment_to_creditCardFragment"
|
||||||
|
app:destination="@id/creditCardFragment" />
|
||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
@@ -107,4 +110,16 @@
|
|||||||
android:label="subscribe_learn_more_bottom_sheet_dialog"
|
android:label="subscribe_learn_more_bottom_sheet_dialog"
|
||||||
tools:layout="@layout/subscribe_learn_more_bottom_sheet_dialog_fragment" />
|
tools:layout="@layout/subscribe_learn_more_bottom_sheet_dialog_fragment" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/creditCardFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment"
|
||||||
|
android:label="credit_card_fragment"
|
||||||
|
tools:layout="@layout/credit_card_fragment">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="request"
|
||||||
|
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||||
|
app:nullable="false" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
@@ -125,6 +125,33 @@
|
|||||||
<string name="BlockedUsersActivity__do_you_want_to_unblock_s">Do you want to unblock \"%1$s\"?</string>
|
<string name="BlockedUsersActivity__do_you_want_to_unblock_s">Do you want to unblock \"%1$s\"?</string>
|
||||||
<string name="BlockedUsersActivity__unblock">Unblock</string>
|
<string name="BlockedUsersActivity__unblock">Unblock</string>
|
||||||
|
|
||||||
|
<!-- CreditCardFragment -->
|
||||||
|
<!-- Title of fragment detailing the donation amount, displayed above the credit card text fields -->
|
||||||
|
<string name="CreditCardFragment__donation_amount_s">Donation amount: %1$s</string>
|
||||||
|
<!-- Explanation of how to fill in the form, displayed above the credit card text fields -->
|
||||||
|
<string name="CreditCardFragment__enter_your_card_information_below">Enter your card information below</string>
|
||||||
|
<!-- Displayed as a hint in the card number text field -->
|
||||||
|
<string name="CreditCardFragment__card_number">Card number</string>
|
||||||
|
<!-- Displayed as a hint in the card expiry text field -->
|
||||||
|
<string name="CreditCardFragment__mm_yy">MM/YY</string>
|
||||||
|
<!-- Displayed as a hint in the card cvv text field -->
|
||||||
|
<string name="CreditCardFragment__cvv">CVV</string>
|
||||||
|
<!-- Error displayed under the card number text field when there is an invalid card number entered -->
|
||||||
|
<string name="CreditCardFragment__invalid_card_number">Invalid card number</string>
|
||||||
|
<!-- Error displayed under the card expiry text field when the card is expired -->
|
||||||
|
<string name="CreditCardFragment__card_has_expired">Card has expired</string>
|
||||||
|
<!-- Error displayed under the card cvv text field when the cvv is too short -->
|
||||||
|
<string name="CreditCardFragment__code_is_too_short">Code is too short</string>
|
||||||
|
<!-- Error displayed under the card cvv text field when the cvv is too long -->
|
||||||
|
<string name="CreditCardFragment__code_is_too_long">Code is too long</string>
|
||||||
|
<!-- Error displayed under the card cvv text field when the cvv is invalid -->
|
||||||
|
<string name="CreditCardFragment__invalid_code">Invalid code</string>
|
||||||
|
<!-- Error displayed under the card expiry text field when the expiry month is invalid -->
|
||||||
|
<string name="CreditCardFragment__invalid_month">Invalid month</string>
|
||||||
|
<!-- Error displayed under the card expiry text field when the expiry is missing the year -->
|
||||||
|
<string name="CreditCardFragment__year_required">Year required</string>
|
||||||
|
<!-- Error displayed under the card expiry text field when the expiry year is invalid -->
|
||||||
|
<string name="CreditCardFragment__invalid_year">Invalid year</string>
|
||||||
|
|
||||||
<!-- BlockUnblockDialog -->
|
<!-- BlockUnblockDialog -->
|
||||||
<string name="BlockUnblockDialog_block_and_leave_s">Block and leave %1$s?</string>
|
<string name="BlockUnblockDialog_block_and_leave_s">Block and leave %1$s?</string>
|
||||||
@@ -5502,6 +5529,8 @@
|
|||||||
<string name="GatewaySelectorBottomSheet__donate_s_to_signal">Donate %1$s to Signal</string>
|
<string name="GatewaySelectorBottomSheet__donate_s_to_signal">Donate %1$s to Signal</string>
|
||||||
<!-- Sheet summary when giving a one-time donation -->
|
<!-- Sheet summary when giving a one-time donation -->
|
||||||
<string name="GatewaySelectorBottomSheet__get_a_s_badge_for_d_days">Get a %1$s badge for %2$d days</string>
|
<string name="GatewaySelectorBottomSheet__get_a_s_badge_for_d_days">Get a %1$s badge for %2$d days</string>
|
||||||
|
<!-- Button label for paying with a credit card -->
|
||||||
|
<string name="GatewaySelectorBottomSheet__credit_or_debit_card">Credit or debit card</string>
|
||||||
|
|
||||||
<!-- StripePaymentInProgressFragment -->
|
<!-- StripePaymentInProgressFragment -->
|
||||||
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||||
|
@Config(application = Application::class)
|
||||||
|
class CreditCardCodeValidatorTest(
|
||||||
|
private val code: String,
|
||||||
|
private val cardType: CreditCardType,
|
||||||
|
private val isFocused: Boolean,
|
||||||
|
private val validity: CreditCardCodeValidator.Validity
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getValidity() {
|
||||||
|
assertEquals(validity, CreditCardCodeValidator.getValidity(code, cardType, isFocused))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@ParameterizedRobolectricTestRunner.Parameters(name = "{index}: getValidity(..) = {0}, {1}, {2}, {3}")
|
||||||
|
fun data(): Iterable<Array<Any>> = arrayListOf(
|
||||||
|
// Unfocused
|
||||||
|
arrayOf("", CreditCardType.UNIONPAY, false, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.UNIONPAY, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("12", CreditCardType.UNIONPAY, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("123", CreditCardType.UNIONPAY, false, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("1234", CreditCardType.UNIONPAY, false, CreditCardCodeValidator.Validity.TOO_LONG),
|
||||||
|
arrayOf("", CreditCardType.OTHER, false, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.OTHER, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("12", CreditCardType.OTHER, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("123", CreditCardType.OTHER, false, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("1234", CreditCardType.OTHER, false, CreditCardCodeValidator.Validity.TOO_LONG),
|
||||||
|
arrayOf("", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("12", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("123", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.TOO_SHORT),
|
||||||
|
arrayOf("1234", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("12345", CreditCardType.AMERICAN_EXPRESS, false, CreditCardCodeValidator.Validity.TOO_LONG),
|
||||||
|
|
||||||
|
// Focused
|
||||||
|
arrayOf("", CreditCardType.UNIONPAY, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.UNIONPAY, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("12", CreditCardType.UNIONPAY, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("123", CreditCardType.UNIONPAY, true, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("1234", CreditCardType.UNIONPAY, true, CreditCardCodeValidator.Validity.TOO_LONG),
|
||||||
|
arrayOf("", CreditCardType.OTHER, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.OTHER, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("12", CreditCardType.OTHER, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("123", CreditCardType.OTHER, true, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("1234", CreditCardType.OTHER, true, CreditCardCodeValidator.Validity.TOO_LONG),
|
||||||
|
arrayOf("", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("12", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("123", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("1234", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("12345", CreditCardType.AMERICAN_EXPRESS, true, CreditCardCodeValidator.Validity.TOO_LONG)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||||
|
@Config(application = Application::class)
|
||||||
|
class CreditCardExpirationValidatorTest(
|
||||||
|
private val creditCardExpiration: CreditCardExpiration,
|
||||||
|
private val currentYear: Int,
|
||||||
|
private val isFocused: Boolean,
|
||||||
|
private val validity: CreditCardExpirationValidator.Validity
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getValidity() {
|
||||||
|
assertEquals(validity, CreditCardExpirationValidator.getValidity(creditCardExpiration, 3, currentYear, isFocused))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@ParameterizedRobolectricTestRunner.Parameters(name = "{index}: getValidity(..) = {0}, {1}, {2}, {3}, {4}")
|
||||||
|
fun data(): Iterable<Array<Any>> = arrayListOf(
|
||||||
|
// Unfocused
|
||||||
|
arrayOf(CreditCardExpiration("", ""), 2020, false, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("0", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("1", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("9", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("01", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("09", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("12", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MISSING_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("", "0"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "1"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "9"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "00"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "01"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "99"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("3", "20"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "20"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("4", "20"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("12", "20"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("3", "21"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("3", "40"), 2020, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("2", "20"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_EXPIRED),
|
||||||
|
arrayOf(CreditCardExpiration("3", "41"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_EXPIRED),
|
||||||
|
arrayOf(CreditCardExpiration("3", "41"), 2021, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("01", "99"), 2098, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "00"), 2099, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("04", "00"), 2099, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "19"), 2099, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("12", "19"), 2099, false, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("00", "20"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("X", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("1X", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("123", ""), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "X"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "2X"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "202"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "2020"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("X", "X"), 2020, false, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
|
||||||
|
// Focused
|
||||||
|
arrayOf(CreditCardExpiration("", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("0", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("1", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("9", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("01", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("09", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("12", ""), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "0"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "1"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "9"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "00"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "01"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("", "99"), 2020, true, CreditCardExpirationValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("3", "20"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "20"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("4", "20"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("12", "20"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("3", "21"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("3", "40"), 2020, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("2", "20"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_EXPIRED),
|
||||||
|
arrayOf(CreditCardExpiration("3", "41"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_EXPIRED),
|
||||||
|
arrayOf(CreditCardExpiration("3", "41"), 2021, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("01", "99"), 2098, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "00"), 2099, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("04", "00"), 2099, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("03", "19"), 2099, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("12", "19"), 2099, true, CreditCardExpirationValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf(CreditCardExpiration("00", "20"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("X", ""), 2020, true, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("1X", ""), 2020, true, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("123", ""), 2020, true, CreditCardExpirationValidator.Validity.INVALID_MONTH),
|
||||||
|
arrayOf(CreditCardExpiration("", "X"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("", "2X"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("", "202"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("", "2020"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_YEAR),
|
||||||
|
arrayOf(CreditCardExpiration("X", "X"), 2020, true, CreditCardExpirationValidator.Validity.INVALID_MONTH)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||||
|
@Config(application = Application::class)
|
||||||
|
class CreditCardNumberValidatorTest(
|
||||||
|
private val creditCardNumber: String,
|
||||||
|
private val creditCardNumberFieldFocused: Boolean,
|
||||||
|
private val validity: CreditCardNumberValidator.Validity
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getValidity() {
|
||||||
|
assertEquals(validity, CreditCardNumberValidator.getValidity(creditCardNumber, creditCardNumberFieldFocused))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@ParameterizedRobolectricTestRunner.Parameters(name = "{index}: getValidity(..) = {0}, {1}, {2}")
|
||||||
|
fun data(): Iterable<Array<Any>> = arrayListOf(
|
||||||
|
arrayOf("", false, CreditCardNumberValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("4", false, CreditCardNumberValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("42", false, CreditCardNumberValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("42424242424", false, CreditCardNumberValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("424242424242", false, CreditCardNumberValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("424242424242424242", false, CreditCardNumberValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("4242424242424", false, CreditCardNumberValidator.Validity.INVALID),
|
||||||
|
arrayOf("4242424242424", true, CreditCardNumberValidator.Validity.POTENTIALLY_VALID),
|
||||||
|
arrayOf("6200000000000004", false, CreditCardNumberValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("6200000000000005", false, CreditCardNumberValidator.Validity.FULLY_VALID),
|
||||||
|
arrayOf("42424242424242424242", false, CreditCardNumberValidator.Validity.INVALID),
|
||||||
|
arrayOf("X", false, CreditCardNumberValidator.Validity.INVALID),
|
||||||
|
arrayOf("42X", false, CreditCardNumberValidator.Validity.INVALID),
|
||||||
|
arrayOf("424242424242X", false, CreditCardNumberValidator.Validity.INVALID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class CreditCardTypeTest(
|
||||||
|
private val creditCardNumber: String,
|
||||||
|
private val creditCardType: CreditCardType
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromCardNumber() {
|
||||||
|
assertEquals(creditCardType, CreditCardType.fromCardNumber(cardNumber = creditCardNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters(name = "{index}: fromCardNumber(..) = {0}, {1}")
|
||||||
|
fun data(): Iterable<Array<Any>> = arrayListOf(
|
||||||
|
arrayOf("34", CreditCardType.AMERICAN_EXPRESS),
|
||||||
|
arrayOf("37", CreditCardType.AMERICAN_EXPRESS),
|
||||||
|
arrayOf("343452000000306", CreditCardType.AMERICAN_EXPRESS),
|
||||||
|
arrayOf("371449635398431", CreditCardType.AMERICAN_EXPRESS),
|
||||||
|
arrayOf("378282246310005", CreditCardType.AMERICAN_EXPRESS),
|
||||||
|
arrayOf("62", CreditCardType.UNIONPAY),
|
||||||
|
arrayOf("81", CreditCardType.UNIONPAY),
|
||||||
|
arrayOf("6200000000000004", CreditCardType.UNIONPAY),
|
||||||
|
arrayOf("", CreditCardType.OTHER),
|
||||||
|
arrayOf("X", CreditCardType.OTHER),
|
||||||
|
arrayOf("4", CreditCardType.OTHER),
|
||||||
|
arrayOf("4111111111111111", CreditCardType.OTHER),
|
||||||
|
arrayOf("4242424242424242", CreditCardType.OTHER),
|
||||||
|
arrayOf("5555555555554444", CreditCardType.OTHER),
|
||||||
|
arrayOf("5555555555554444", CreditCardType.OTHER),
|
||||||
|
arrayOf("2223003122003222", CreditCardType.OTHER),
|
||||||
|
arrayOf("6011111111111117", CreditCardType.OTHER),
|
||||||
|
arrayOf("3056930009020004", CreditCardType.OTHER),
|
||||||
|
arrayOf("3566002020360505", CreditCardType.OTHER),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user