kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add minimum amount error for boosts.
rodzic
1618141342
commit
0bef37bfc1
|
@ -85,6 +85,16 @@ fun DonationsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration>
|
||||||
return levels.filterKeys { SUBSCRIPTION_LEVELS.contains(it) }.toSortedMap()
|
return levels.filterKeys { SUBSCRIPTION_LEVELS.contains(it) }.toSortedMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a map describing the minimum donation amounts per currency.
|
||||||
|
* This returns only the currencies available to the user.
|
||||||
|
*/
|
||||||
|
fun DonationsConfiguration.getMinimumDonationAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
|
||||||
|
return getFilteredCurrencies(paymentMethodAvailability)
|
||||||
|
.mapKeys { Currency.getInstance(it.key.uppercase()) }
|
||||||
|
.mapValues { FiatMoney(it.value.minimum, it.key) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun DonationsConfiguration.getFilteredCurrencies(paymentMethodAvailability: PaymentMethodAvailability): Map<String, DonationsConfiguration.CurrencyConfiguration> {
|
private fun DonationsConfiguration.getFilteredCurrencies(paymentMethodAvailability: PaymentMethodAvailability): Map<String, DonationsConfiguration.CurrencyConfiguration> {
|
||||||
val userPaymentMethods = paymentMethodAvailability.toSet()
|
val userPaymentMethods = paymentMethodAvailability.toSet()
|
||||||
val availableCurrencyCodes = PlatformCurrencyUtil.getAvailableCurrencyCodes()
|
val availableCurrencyCodes = PlatformCurrencyUtil.getAvailableCurrencyCodes()
|
||||||
|
|
|
@ -64,6 +64,13 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||||
.map { it.getBoostBadges().first() }
|
.map { it.getBoostBadges().first() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMinimumDonationAmounts(): Single<Map<Currency, FiatMoney>> {
|
||||||
|
return Single.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||||
|
.flatMap { it.flattenResult() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.map { it.getMinimumDonationAmounts() }
|
||||||
|
}
|
||||||
|
|
||||||
fun waitForOneTimeRedemption(
|
fun waitForOneTimeRedemption(
|
||||||
price: FiatMoney,
|
price: FiatMoney,
|
||||||
paymentIntentId: String,
|
paymentIntentId: String,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.text.Spanned
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.text.method.DigitsKeyListener
|
import android.text.method.DigitsKeyListener
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
|
@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.visible
|
||||||
import java.lang.Integer.min
|
import java.lang.Integer.min
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
@ -102,7 +104,9 @@ data class Boost(
|
||||||
val currency: Currency,
|
val currency: Currency,
|
||||||
override val isEnabled: Boolean,
|
override val isEnabled: Boolean,
|
||||||
val onBoostClick: (View, Boost) -> Unit,
|
val onBoostClick: (View, Boost) -> Unit,
|
||||||
|
val minimumAmount: FiatMoney,
|
||||||
val isCustomAmountFocused: Boolean,
|
val isCustomAmountFocused: Boolean,
|
||||||
|
val isCustomAmountTooSmall: Boolean,
|
||||||
val onCustomAmountChanged: (String) -> Unit,
|
val onCustomAmountChanged: (String) -> Unit,
|
||||||
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
||||||
) : PreferenceModel<SelectionModel>(isEnabled = isEnabled) {
|
) : PreferenceModel<SelectionModel>(isEnabled = isEnabled) {
|
||||||
|
@ -113,7 +117,10 @@ data class Boost(
|
||||||
newItem.boosts == boosts &&
|
newItem.boosts == boosts &&
|
||||||
newItem.selectedBoost == selectedBoost &&
|
newItem.selectedBoost == selectedBoost &&
|
||||||
newItem.currency == currency &&
|
newItem.currency == currency &&
|
||||||
newItem.isCustomAmountFocused == isCustomAmountFocused
|
newItem.isCustomAmountFocused == isCustomAmountFocused &&
|
||||||
|
newItem.isCustomAmountTooSmall == isCustomAmountTooSmall &&
|
||||||
|
newItem.minimumAmount.amount == minimumAmount.amount &&
|
||||||
|
newItem.minimumAmount.currency == minimumAmount.currency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +133,7 @@ data class Boost(
|
||||||
private val boost5: MaterialButton = itemView.findViewById(R.id.boost_5)
|
private val boost5: MaterialButton = itemView.findViewById(R.id.boost_5)
|
||||||
private val boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
private val boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
||||||
private val custom: AppCompatEditText = itemView.findViewById(R.id.boost_custom)
|
private val custom: AppCompatEditText = itemView.findViewById(R.id.boost_custom)
|
||||||
|
private val error: TextView = itemView.findViewById(R.id.boost_custom_too_small)
|
||||||
|
|
||||||
private val boostButtons: List<MaterialButton>
|
private val boostButtons: List<MaterialButton>
|
||||||
get() {
|
get() {
|
||||||
|
@ -145,6 +153,16 @@ data class Boost(
|
||||||
override fun bind(model: SelectionModel) {
|
override fun bind(model: SelectionModel) {
|
||||||
itemView.isEnabled = model.isEnabled
|
itemView.isEnabled = model.isEnabled
|
||||||
|
|
||||||
|
error.text = context.getString(
|
||||||
|
R.string.Boost__the_minimum_amount_you_can_donate_is_s,
|
||||||
|
FiatMoneyUtil.format(
|
||||||
|
context.resources, model.minimumAmount,
|
||||||
|
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
error.visible = model.isCustomAmountTooSmall
|
||||||
|
|
||||||
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
||||||
val isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
val isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||||
button.isSelected = isSelected
|
button.isSelected = isSelected
|
||||||
|
|
|
@ -242,6 +242,7 @@ class DonateToSignalFragment :
|
||||||
when (state.donateToSignalType) {
|
when (state.donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||||
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||||
|
DonateToSignalType.GIFT -> error("This fragment does not support gifts.")
|
||||||
}
|
}
|
||||||
|
|
||||||
space(20.dp)
|
space(20.dp)
|
||||||
|
@ -310,6 +311,8 @@ class DonateToSignalFragment :
|
||||||
selectedBoost = state.selectedBoost,
|
selectedBoost = state.selectedBoost,
|
||||||
currency = state.customAmount.currency,
|
currency = state.customAmount.currency,
|
||||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||||
|
isCustomAmountTooSmall = state.shouldDisplayCustomAmountTooSmallError,
|
||||||
|
minimumAmount = state.minimumDonationAmountOfSelectedCurrency,
|
||||||
isEnabled = areFieldsEnabled,
|
isEnabled = areFieldsEnabled,
|
||||||
onBoostClick = { view, boost ->
|
onBoostClick = { view, boost ->
|
||||||
startAnimationAboveSelectedBoost(view)
|
startAnimationAboveSelectedBoost(view)
|
||||||
|
|
|
@ -81,9 +81,15 @@ data class DonateToSignalState(
|
||||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, selectedCurrency),
|
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, selectedCurrency),
|
||||||
val isCustomAmountFocused: Boolean = false,
|
val isCustomAmountFocused: Boolean = false,
|
||||||
val donationStage: DonationStage = DonationStage.INIT,
|
val donationStage: DonationStage = DonationStage.INIT,
|
||||||
val selectableCurrencyCodes: List<String> = emptyList()
|
val selectableCurrencyCodes: List<String> = emptyList(),
|
||||||
|
private val minimumDonationAmounts: Map<Currency, FiatMoney> = emptyMap()
|
||||||
) {
|
) {
|
||||||
val isSelectionValid: Boolean = if (isCustomAmountFocused) customAmount.amount > BigDecimal.ZERO else selectedBoost != null
|
val minimumDonationAmountOfSelectedCurrency: FiatMoney = minimumDonationAmounts[selectedCurrency] ?: FiatMoney(BigDecimal.ZERO, selectedCurrency)
|
||||||
|
private val isCustomAmountTooSmall: Boolean = if (isCustomAmountFocused) customAmount.amount < minimumDonationAmountOfSelectedCurrency.amount else false
|
||||||
|
private val isCustomAmountZero: Boolean = customAmount.amount == BigDecimal.ZERO
|
||||||
|
|
||||||
|
val isSelectionValid: Boolean = if (isCustomAmountFocused) !isCustomAmountTooSmall else selectedBoost != null
|
||||||
|
val shouldDisplayCustomAmountTooSmallError: Boolean = isCustomAmountTooSmall && !isCustomAmountZero
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MonthlyDonationState(
|
data class MonthlyDonationState(
|
||||||
|
|
|
@ -214,6 +214,15 @@ class DonateToSignalViewModel(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
oneTimeDonationDisposables += oneTimeDonationRepository.getMinimumDonationAmounts().subscribeBy(
|
||||||
|
onSuccess = { amountMap ->
|
||||||
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(minimumDonationAmounts = amountMap)) }
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
Log.w(TAG, "Could not load minimum custom donation amounts.", it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeDonationRepository.getBoosts().toObservable()
|
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeDonationRepository.getBoosts().toObservable()
|
||||||
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:viewBindingIgnore="true"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter">
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/boost_1"
|
android:id="@+id/boost_1"
|
||||||
|
@ -130,4 +130,18 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/boost_4" />
|
app:layout_constraintTop_toBottomOf="@id/boost_4" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/boost_custom_too_small"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@style/Signal.Text.BodySmall"
|
||||||
|
android:textColor="@color/signal_colorError"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/boost_custom"
|
||||||
|
tools:text="@string/Boost__the_minimum_amount_you_can_donate_is_s" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4341,6 +4341,9 @@
|
||||||
<string name="ManageDonationsFragment__gift_a_badge">Gift a badge</string>
|
<string name="ManageDonationsFragment__gift_a_badge">Gift a badge</string>
|
||||||
|
|
||||||
<string name="Boost__enter_custom_amount">Enter Custom Amount</string>
|
<string name="Boost__enter_custom_amount">Enter Custom Amount</string>
|
||||||
|
<string name="Boost__one_time_contribution">One-time contribution</string>
|
||||||
|
<!-- Error label when the amount is smaller than what we can accept -->
|
||||||
|
<string name="Boost__the_minimum_amount_you_can_donate_is_s">The minimum amount you can donate is %s</string>
|
||||||
|
|
||||||
<string name="MySupportPreference__s_per_month">%1$s/month</string>
|
<string name="MySupportPreference__s_per_month">%1$s/month</string>
|
||||||
<string name="MySupportPreference__renews_s">Renews %1$s</string>
|
<string name="MySupportPreference__renews_s">Renews %1$s</string>
|
||||||
|
|
|
@ -131,6 +131,30 @@ class DonationsConfigurationExtensionsKtTest {
|
||||||
assertTrue(boostAmounts.map { it.key.currencyCode }.containsAll(setOf("USD", "BIF")))
|
assertTrue(boostAmounts.map { it.key.currencyCode }.containsAll(setOf("USD", "BIF")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given all methods are available, when I getMinimumDonationAmounts, then I expect BIF`() {
|
||||||
|
val minimumDonationAmounts = testSubject.getMinimumDonationAmounts(AllPaymentMethodsAvailability)
|
||||||
|
|
||||||
|
assertEquals(1, minimumDonationAmounts.size)
|
||||||
|
assertNotNull(minimumDonationAmounts[Currency.getInstance("BIF")])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given only PayPal available, when I getMinimumDonationAmounts, then I expect BIF and JPY`() {
|
||||||
|
val minimumDonationAmounts = testSubject.getMinimumDonationAmounts(PayPalOnly)
|
||||||
|
|
||||||
|
assertEquals(2, minimumDonationAmounts.size)
|
||||||
|
assertTrue(minimumDonationAmounts.map { it.key.currencyCode }.containsAll(setOf("JPY", "BIF")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given only Card available, when I getMinimumDonationAmounts, then I expect BIF and USD`() {
|
||||||
|
val minimumDonationAmounts = testSubject.getMinimumDonationAmounts(CardOnly)
|
||||||
|
|
||||||
|
assertEquals(2, minimumDonationAmounts.size)
|
||||||
|
assertTrue(minimumDonationAmounts.map { it.key.currencyCode }.containsAll(setOf("USD", "BIF")))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Given GIFT_LEVEL, When I getBadge, then I expect the gift badge`() {
|
fun `Given GIFT_LEVEL, When I getBadge, then I expect the gift badge`() {
|
||||||
mockkStatic(ApplicationDependencies::class) {
|
mockkStatic(ApplicationDependencies::class) {
|
||||||
|
|
Ładowanie…
Reference in New Issue