mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Donations credit card formatting.
This commit is contained in:
committed by
Cody Henthorne
parent
16cbc971a5
commit
b8e16353ab
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user