diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt index 8b9e6248a1..da2be8eb7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.util.MappingViewHolder import org.thoughtcrime.securesms.util.ViewUtil import java.lang.Integer.min import java.text.DecimalFormatSymbols +import java.text.NumberFormat import java.util.Currency import java.util.Locale import java.util.regex.Pattern @@ -158,7 +159,7 @@ data class Boost( if (filter == null || filter?.currency != model.currency) { custom.removeTextChangedListener(filter) - filter = MoneyFilter(model.currency) { + filter = MoneyFilter(model.currency, custom) { model.onCustomAmountChanged(it) } @@ -191,12 +192,14 @@ data class Boost( } @VisibleForTesting - class MoneyFilter(val currency: Currency, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(false, true), TextWatcher { + class MoneyFilter(val currency: Currency, private val text: AppCompatEditText? = null, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(false, true), TextWatcher { val separator = DecimalFormatSymbols.getInstance().decimalSeparator val separatorCount = min(1, currency.defaultFractionDigits) - val prefix: String = currency.getSymbol(Locale.getDefault()) + val symbol: String = currency.getSymbol(Locale.getDefault()) val pattern: Pattern = "[0-9]*([$separator]){0,$separatorCount}[0-9]{0,${currency.defaultFractionDigits}}".toPattern() + val symbolPattern: Regex = """\s*${Regex.escape(symbol)}\s*""".toRegex() + val leadingZeroesPattern: Regex = """^0*""".toRegex() override fun filter( source: CharSequence, @@ -208,7 +211,7 @@ data class Boost( ): CharSequence? { val result = dest.subSequence(0, dstart).toString() + source.toString() + dest.subSequence(dend, dest.length) - val resultWithoutCurrencyPrefix = result.removePrefix(prefix) + val resultWithoutCurrencyPrefix = result.removePrefix(symbol).removeSuffix(symbol).trim() if (resultWithoutCurrencyPrefix.length == 1 && !resultWithoutCurrencyPrefix.isDigitsOnly() && resultWithoutCurrencyPrefix != separator.toString()) { return dest.subSequence(dstart, dend) @@ -230,14 +233,45 @@ data class Boost( override fun afterTextChanged(s: Editable?) { if (s.isNullOrEmpty()) return - val hasPrefix = s.startsWith(prefix) - if (hasPrefix && s.length == prefix.length) { + val hasSymbol = s.startsWith(symbol) || s.endsWith(symbol) + if (hasSymbol && symbolPattern.matchEntire(s.toString()) != null) { s.clear() - } else if (!hasPrefix) { - s.insert(0, prefix) + } else if (!hasSymbol) { + val formatter = NumberFormat.getCurrencyInstance() + formatter.currency = currency + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = currency.defaultFractionDigits + + val value = s.toString().toDoubleOrNull() + + if (value != null) { + val formatted = formatter.format(value) + + text?.removeTextChangedListener(this) + + s.replace(0, s.length, formatted) + if (formatted.endsWith(symbol)) { + val result: MatchResult? = symbolPattern.find(formatted) + if (result != null && result.range.first < s.length) { + text?.setSelection(result.range.first) + } + } + + text?.addTextChangedListener(this) + } } - onCustomAmountChanged(s.removePrefix(prefix).toString()) + val withoutSymbol = s.removePrefix(symbol).removeSuffix(symbol).trim().toString() + val withoutLeadingZeroes = withoutSymbol.replace(leadingZeroesPattern, "") + if (withoutSymbol != withoutLeadingZeroes) { + text?.removeTextChangedListener(this) + + s.replace(s.indexOf(withoutSymbol), withoutSymbol.length, withoutLeadingZeroes) + + text?.addTextChangedListener(this) + } + + onCustomAmountChanged(s.removePrefix(symbol).removeSuffix(symbol).trim().toString()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt index 40766d2fcf..3399d6c2eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt @@ -176,13 +176,14 @@ class BoostViewModel( store.update { it.copy(stage = BoostState.Stage.TOKEN_REQUEST) } - boostToPurchase = if (snapshot.isCustomAmountFocused) { + val boost = if (snapshot.isCustomAmountFocused) { Boost(snapshot.customAmount) } else { snapshot.selectedBoost } - donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedBoost.price, label, fetchTokenRequestCode) + boostToPurchase = boost + donationPaymentRepository.requestTokenFromGooglePay(boost.price, label, fetchTokenRequestCode) } fun setSelectedBoost(boost: Boost) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostTest__MoneyFilter.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostTest__MoneyFilter.kt index 218b9afe59..b46a17e36a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostTest__MoneyFilter.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostTest__MoneyFilter.kt @@ -49,7 +49,7 @@ class BoostTest__MoneyFilter { } @Test - fun `Given USD, when I enter 5dot00, then I expect 5dot00 from text change`() { + fun `Given USD, when I enter 5dot00, then I expect 5 from text change`() { var result = "" val testSubject = Boost.MoneyFilter(usd) { result = it @@ -58,11 +58,11 @@ class BoostTest__MoneyFilter { val editable = SpannableStringBuilder("5.00") testSubject.afterTextChanged(editable) - assertEquals("5.00", result) + assertEquals("5", result) } @Test - fun `Given USD, when I enter 5dot000, then I expect unsuccessful filter`() { + fun `Given USD, when I enter 5dot000, then I expect successful filter`() { val testSubject = Boost.MoneyFilter(yen) val editable = SpannableStringBuilder("5.000") val dest = SpannableStringBuilder() @@ -70,7 +70,7 @@ class BoostTest__MoneyFilter { testSubject.afterTextChanged(editable) val filterResult = testSubject.filter(editable, 0, editable.length, dest, 0, 0) - assertNotNull(filterResult) + assertNull(filterResult) } @Test @@ -124,7 +124,7 @@ class BoostTest__MoneyFilter { @Test fun `Given JPY, when I enter 5dot, then I expect unsuccessful filter`() { val testSubject = Boost.MoneyFilter(yen) - val editable = SpannableStringBuilder("5.") + val editable = SpannableStringBuilder("¥5.") val dest = SpannableStringBuilder() testSubject.afterTextChanged(editable)