Add minimum amount error for boosts.
This commit is contained in:
parent
1618141342
commit
0bef37bfc1
9 changed files with 99 additions and 5 deletions
|
@ -85,6 +85,16 @@ fun DonationsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration>
|
|||
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> {
|
||||
val userPaymentMethods = paymentMethodAvailability.toSet()
|
||||
val availableCurrencyCodes = PlatformCurrencyUtil.getAvailableCurrencyCodes()
|
||||
|
|
|
@ -64,6 +64,13 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
|||
.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(
|
||||
price: FiatMoney,
|
||||
paymentIntentId: String,
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.text.Spanned
|
|||
import android.text.TextWatcher
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
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.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.lang.Integer.min
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
@ -102,7 +104,9 @@ data class Boost(
|
|||
val currency: Currency,
|
||||
override val isEnabled: Boolean,
|
||||
val onBoostClick: (View, Boost) -> Unit,
|
||||
val minimumAmount: FiatMoney,
|
||||
val isCustomAmountFocused: Boolean,
|
||||
val isCustomAmountTooSmall: Boolean,
|
||||
val onCustomAmountChanged: (String) -> Unit,
|
||||
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
||||
) : PreferenceModel<SelectionModel>(isEnabled = isEnabled) {
|
||||
|
@ -113,7 +117,10 @@ data class Boost(
|
|||
newItem.boosts == boosts &&
|
||||
newItem.selectedBoost == selectedBoost &&
|
||||
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 boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
||||
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>
|
||||
get() {
|
||||
|
@ -145,6 +153,16 @@ data class Boost(
|
|||
override fun bind(model: SelectionModel) {
|
||||
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) ->
|
||||
val isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||
button.isSelected = isSelected
|
||||
|
|
|
@ -242,6 +242,7 @@ class DonateToSignalFragment :
|
|||
when (state.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||
DonateToSignalType.GIFT -> error("This fragment does not support gifts.")
|
||||
}
|
||||
|
||||
space(20.dp)
|
||||
|
@ -310,6 +311,8 @@ class DonateToSignalFragment :
|
|||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isCustomAmountTooSmall = state.shouldDisplayCustomAmountTooSmallError,
|
||||
minimumAmount = state.minimumDonationAmountOfSelectedCurrency,
|
||||
isEnabled = areFieldsEnabled,
|
||||
onBoostClick = { view, boost ->
|
||||
startAnimationAboveSelectedBoost(view)
|
||||
|
|
|
@ -81,9 +81,15 @@ data class DonateToSignalState(
|
|||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, selectedCurrency),
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
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(
|
||||
|
|
|
@ -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 oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
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
|
||||
android:id="@+id/boost_1"
|
||||
|
@ -130,4 +130,18 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
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>
|
|
@ -4341,6 +4341,9 @@
|
|||
<string name="ManageDonationsFragment__gift_a_badge">Gift a badge</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__renews_s">Renews %1$s</string>
|
||||
|
|
|
@ -131,6 +131,30 @@ class DonationsConfigurationExtensionsKtTest {
|
|||
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
|
||||
fun `Given GIFT_LEVEL, When I getBadge, then I expect the gift badge`() {
|
||||
mockkStatic(ApplicationDependencies::class) {
|
||||
|
|
Loading…
Add table
Reference in a new issue