Donations credit card formatting.

This commit is contained in:
Alex Hart
2022-11-03 17:22:03 -03:00
committed by Cody Henthorne
parent 16cbc971a5
commit b8e16353ab
11 changed files with 363 additions and 27 deletions

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
import android.text.Editable
import android.text.TextWatcher
class CreditCardExpirationTextWatcher : TextWatcher {
private var isBackspace = false
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
isBackspace = count == 0
}
override fun afterTextChanged(s: Editable) {
val text = s.toString()
val formattedText = when (text.length) {
1 -> formatForSingleCharacter(text)
2 -> formatForTwoCharacters(text)
else -> text
}
val finalText = if (isBackspace && text.length < formattedText.length && formattedText.endsWith("/")) {
formattedText.dropLast(2)
} else {
formattedText
}
if (finalText != text) {
s.replace(0, s.length, finalText)
}
}
private fun formatForSingleCharacter(text: String): String {
val number = text.toIntOrNull() ?: return text
return if (number > 1) {
"0$number/"
} else {
text
}
}
private fun formatForTwoCharacters(text: String): String {
val number = text.toIntOrNull() ?: return text
return if (number <= 12) {
"%02d/".format(number)
} else {
text
}
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.c
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes
import androidx.core.os.bundleOf
import androidx.core.widget.addTextChangedListener
@@ -13,6 +14,7 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
@@ -26,13 +28,21 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
private val lifecycleDisposable = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.title.text = getString(R.string.CreditCardFragment__donation_amount_s, FiatMoneyUtil.format(resources, args.request.fiat))
binding.title.text = if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
getString(
R.string.CreditCardFragment__donation_amount_s_per_month,
FiatMoneyUtil.format(resources, args.request.fiat, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
)
} else {
getString(R.string.CreditCardFragment__donation_amount_s, FiatMoneyUtil.format(resources, args.request.fiat))
}
binding.cardNumber.addTextChangedListener(afterTextChanged = {
viewModel.onNumberChanged(it?.toString() ?: "")
viewModel.onNumberChanged(it?.toString()?.filter { it != ' ' } ?: "")
})
binding.cardNumber.addTextChangedListener(CreditCardTextWatcher())
binding.cardNumber.setOnFocusChangeListener { v, hasFocus ->
viewModel.onNumberFocusChanged(hasFocus)
}
@@ -45,10 +55,21 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
viewModel.onCodeFocusChanged(hasFocus)
}
binding.cardCvv.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
binding.continueButton.performClick()
true
} else {
false
}
}
binding.cardExpiry.addTextChangedListener(afterTextChanged = {
viewModel.onExpirationChanged(it?.toString() ?: "")
})
binding.cardExpiry.addTextChangedListener(CreditCardExpirationTextWatcher())
binding.cardExpiry.setOnFocusChangeListener { v, hasFocus ->
viewModel.onExpirationFocusChanged(hasFocus)
}
@@ -112,7 +133,13 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
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
CreditCardExpirationValidator.Validity.FULLY_VALID -> {
if (binding.cardExpiry.isFocused) {
binding.cardCvv.requestFocus()
}
NO_ERROR
}
}
binding.cardExpiryWrapper.error = errorState.resolveErrorText(requireContext())

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
import android.text.Editable
import android.text.TextWatcher
/**
* Formats a credit card by type as the user modifies it.
*/
class CreditCardTextWatcher : TextWatcher {
private var isBackspace: Boolean = false
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
isBackspace = count == 0
}
override fun afterTextChanged(s: Editable) {
val userInput = s.toString()
val normalizedNumber = userInput.filter { it != ' ' }
val formattedNumber = when (CreditCardType.fromCardNumber(normalizedNumber)) {
CreditCardType.AMERICAN_EXPRESS -> applyAmexFormatting(normalizedNumber)
CreditCardType.UNIONPAY -> applyUnionPayFormatting(normalizedNumber)
CreditCardType.OTHER -> applyOtherFormatting(normalizedNumber)
}
val backspaceHandled = if (isBackspace && formattedNumber.endsWith(' ') && formattedNumber.length > userInput.length) {
formattedNumber.dropLast(2)
} else {
formattedNumber
}
if (userInput != backspaceHandled) {
s.replace(0, s.length, backspaceHandled)
}
}
private fun applyAmexFormatting(normalizedNumber: String): String {
return applyGrouping(normalizedNumber, listOf(4, 6, 5))
}
private fun applyUnionPayFormatting(normalizedNumber: String): String {
return when {
normalizedNumber.length <= 13 -> applyGrouping(normalizedNumber, listOf(4, 4, 5))
normalizedNumber.length <= 16 -> applyGrouping(normalizedNumber, listOf(4, 4, 4, 4))
else -> applyGrouping(normalizedNumber, listOf(5, 5, 5, 4))
}
}
private fun applyOtherFormatting(normalizedNumber: String): String {
return if (normalizedNumber.length <= 16) {
applyGrouping(normalizedNumber, listOf(4, 4, 4, 4))
} else {
applyGrouping(normalizedNumber, listOf(5, 5, 5, 4))
}
}
private fun applyGrouping(normalizedNumber: String, groups: List<Int>): String {
val maxCardLength = groups.sum()
return groups.fold(0 to emptyList<String>()) { acc, limit ->
val offset = acc.first
val section = normalizedNumber.drop(offset).take(limit)
val segment = if (limit == section.length && offset + limit != maxCardLength) {
"$section "
} else {
section
}
(offset + limit) to acc.second + segment
}.second.filter { it.isNotEmpty() }.joinToString("")
}
}