mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 19:56:00 +00:00
Credit card validator implementations and spec tests.
This commit is contained in:
@@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
@@ -390,7 +391,7 @@ class DonateToSignalFragment : DSLSettingsFragment(
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
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) {
|
||||
viewModel.provideGatewayRequest(gatewayResponse.request)
|
||||
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
|
||||
donationPaymentComponent.donationPaymentRepository.requestTokenFromGooglePay(
|
||||
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
|
||||
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() {
|
||||
donationPaymentComponent.googlePayResultPublisher.subscribeBy(
|
||||
onNext = { paymentResult ->
|
||||
viewModel.consumeGatewayRequest()?.let {
|
||||
viewModel.consumeGatewayRequestForGooglePay()?.let {
|
||||
donationPaymentComponent.donationPaymentRepository.onActivityResult(
|
||||
paymentResult.requestCode,
|
||||
paymentResult.resultCode,
|
||||
|
||||
@@ -348,13 +348,13 @@ class DonateToSignalViewModel(
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
fun provideGatewayRequest(request: GatewayRequest) {
|
||||
fun provideGatewayRequestForGooglePay(request: GatewayRequest) {
|
||||
Log.d(TAG, "Provided with a gateway request.")
|
||||
Preconditions.checkState(gatewayRequest == null)
|
||||
gatewayRequest = request
|
||||
}
|
||||
|
||||
fun consumeGatewayRequest(): GatewayRequest? {
|
||||
fun consumeGatewayRequestForGooglePay(): GatewayRequest? {
|
||||
val request = gatewayRequest
|
||||
gatewayRequest = null
|
||||
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
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val badge: Badge,
|
||||
val isGooglePayAvailable: 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";
|
||||
public static final String STORIES_LOCALE = "android.stories.locale.1";
|
||||
private static final String HIDE_CONTACTS = "android.hide.contacts";
|
||||
<<<<<<< HEAD
|
||||
private static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
||||
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
|
||||
@@ -162,8 +169,15 @@ public final class FeatureFlags {
|
||||
CDS_V2_COMPAT,
|
||||
STORIES_LOCALE,
|
||||
HIDE_CONTACTS,
|
||||
<<<<<<< HEAD
|
||||
MEDIA_PREVIEW_V2,
|
||||
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
|
||||
@@ -227,8 +241,15 @@ public final class FeatureFlags {
|
||||
CDS_V2_LOAD_TEST,
|
||||
CDS_V2_COMPAT,
|
||||
STORIES,
|
||||
<<<<<<< HEAD
|
||||
MEDIA_PREVIEW_V2,
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
Reference in New Issue
Block a user