Credit card validator implementations and spec tests.

This commit is contained in:
Alex Hart
2022-10-17 13:47:51 -03:00
parent 27c3607099
commit 1174bc8e07
21 changed files with 948 additions and 6 deletions
@@ -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,
@@ -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
@@ -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
}
}
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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
}
}
}
}
@@ -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
)
@@ -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
}
}
}
@@ -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)
} }
@@ -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>
+29
View File
@@ -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>
@@ -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)
)
}
}
@@ -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)
)
}
}
@@ -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)
)
}
}
@@ -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),
)
}
}