Add SEPA max amount exceeded dialog.

This commit is contained in:
Cody Henthorne 2023-11-10 15:05:52 -05:00
parent 43a13964bd
commit 95d7d26f11
10 changed files with 74 additions and 15 deletions

View file

@ -14,6 +14,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.InputAwareLayout import org.thoughtcrime.securesms.components.InputAwareLayout
@ -286,6 +287,8 @@ class GiftFlowConfirmationFragment :
override fun onProcessorActionProcessed() = Unit override fun onProcessorActionProcessed() = Unit
override fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) = error("Unsupported operation")
override fun onUserLaunchedAnExternalApplication() = Unit override fun onUserLaunchedAnExternalApplication() = Unit
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) = error("Unsupported operation") override fun navigateToDonationPending(gatewayRequest: GatewayRequest) = error("Unsupported operation")

View file

@ -470,6 +470,15 @@ class DonateToSignalFragment :
viewModel.refreshActiveSubscription() viewModel.refreshActiveSubscription()
} }
override fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney) {
val max = FiatMoneyUtil.format(resources, sepaEuroMaximum, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonateToSignal__donation_amount_too_high)
.setMessage(getString(R.string.DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer, max))
.setPositiveButton(android.R.string.ok, null)
.show()
}
override fun onUserLaunchedAnExternalApplication() = Unit override fun onUserLaunchedAnExternalApplication() = Unit
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) { override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {

View file

@ -36,7 +36,9 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
import java.math.BigDecimal
import java.util.Currency import java.util.Currency
/** /**
@ -77,8 +79,12 @@ class DonationCheckoutDelegate(
registerGooglePayCallback() registerGooglePayCallback()
fragment.setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle -> fragment.setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
val response: GatewayResponse = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, GatewayResponse::class.java)!! if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
handleGatewaySelectionResponse(response) callback.showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
} else {
val response: GatewayResponse = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, GatewayResponse::class.java)!!
handleGatewaySelectionResponse(response)
}
} }
fragment.setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle -> fragment.setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
@ -342,5 +348,6 @@ class DonationCheckoutDelegate(
fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse) fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse)
fun onPaymentComplete(gatewayRequest: GatewayRequest) fun onPaymentComplete(gatewayRequest: GatewayRequest)
fun onProcessorActionProcessed() fun onProcessorActionProcessed()
fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney)
} }
} }

View file

@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.models.Pa
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
/** /**
@ -140,9 +141,18 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__bank_transfer), text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__bank_transfer),
icon = DSLSettingsIcon.from(R.drawable.bank_transfer), icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
onClick = { onClick = {
findNavController().popBackStack() if (state.sepaEuroMaximum != null &&
val response = GatewayResponse(GatewayResponse.Gateway.SEPA_DEBIT, args.request) args.request.fiat.currency == CurrencyUtil.EURO &&
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response)) args.request.fiat.amount > state.sepaEuroMaximum.amount
) {
findNavController().popBackStack()
setFragmentResult(REQUEST_KEY, bundleOf(FAILURE_KEY to true, SEPA_EURO_MAX to state.sepaEuroMaximum.amount))
} else {
findNavController().popBackStack()
val response = GatewayResponse(GatewayResponse.Gateway.SEPA_DEBIT, args.request)
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
}
} }
) )
} }
@ -164,6 +174,8 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
companion object { companion object {
const val REQUEST_KEY = "payment_checkout_mode" const val REQUEST_KEY = "payment_checkout_mode"
const val FAILURE_KEY = "gateway_failure"
const val SEPA_EURO_MAX = "sepa_euro_max"
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, request: GatewayRequest) { fun DSLConfiguration.presentTitleAndSubtitle(context: Context, request: GatewayRequest) {
when (request.donateToSignalType) { when (request.donateToSignalType) {

View file

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.internal.push.DonationsConfiguration import org.whispersystems.signalservice.internal.push.DonationsConfiguration
import java.util.Locale import java.util.Locale
@ -9,12 +11,12 @@ import java.util.Locale
class GatewaySelectorRepository( class GatewaySelectorRepository(
private val donationsService: DonationsService private val donationsService: DonationsService
) { ) {
fun getAvailableGateways(currencyCode: String): Single<Set<GatewayResponse.Gateway>> { fun getAvailableGatewayConfiguration(currencyCode: String): Single<GatewayConfiguration> {
return Single.fromCallable { return Single.fromCallable {
donationsService.getDonationsConfiguration(Locale.getDefault()) donationsService.getDonationsConfiguration(Locale.getDefault())
}.flatMap { it.flattenResult() } }.flatMap { it.flattenResult() }
.map { configuration -> .map { configuration ->
configuration.getAvailablePaymentMethods(currencyCode).map { val available = configuration.getAvailablePaymentMethods(currencyCode).map {
when (it) { when (it) {
DonationsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL) DonationsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL)
DonationsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY) DonationsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
@ -23,6 +25,16 @@ class GatewaySelectorRepository(
else -> listOf() else -> listOf()
} }
}.flatten().toSet() }.flatten().toSet()
GatewayConfiguration(
availableGateways = available,
sepaEuroMaximum = if (configuration.sepaMaximumEuros != null) FiatMoney(configuration.sepaMaximumEuros, CurrencyUtil.EURO) else null
)
} }
} }
data class GatewayConfiguration(
val availableGateways: Set<GatewayResponse.Gateway>,
val sepaEuroMaximum: FiatMoney?
)
} }

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
data class GatewaySelectorState( data class GatewaySelectorState(
@ -10,5 +11,6 @@ data class GatewaySelectorState(
val isPayPalAvailable: Boolean = false, val isPayPalAvailable: Boolean = false,
val isCreditCardAvailable: Boolean = false, val isCreditCardAvailable: Boolean = false,
val isSEPADebitAvailable: Boolean = false, val isSEPADebitAvailable: Boolean = false,
val isIDEALAvailable: Boolean = false val isIDEALAvailable: Boolean = false,
val sepaEuroMaximum: FiatMoney? = null
) )

View file

@ -36,17 +36,18 @@ class GatewaySelectorViewModel(
init { init {
val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false) val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false)
val availabilitySet = gatewaySelectorRepository.getAvailableGateways(currencyCode = args.request.currencyCode) val gatewayConfiguration = gatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = args.request.currencyCode)
disposables += Single.zip(isGooglePayAvailable, availabilitySet, ::Pair).subscribeBy { (googlePayAvailable, gatewaysAvailable) -> disposables += Single.zip(isGooglePayAvailable, gatewayConfiguration, ::Pair).subscribeBy { (googlePayAvailable, gatewayConfiguration) ->
SignalStore.donationsValues().isGooglePayReady = googlePayAvailable SignalStore.donationsValues().isGooglePayReady = googlePayAvailable
store.update { store.update {
it.copy( it.copy(
loading = false, loading = false,
isCreditCardAvailable = it.isCreditCardAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.CREDIT_CARD), isCreditCardAvailable = it.isCreditCardAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.CREDIT_CARD),
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.GOOGLE_PAY), isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.GOOGLE_PAY),
isPayPalAvailable = it.isPayPalAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.PAYPAL), isPayPalAvailable = it.isPayPalAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.PAYPAL),
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.SEPA_DEBIT), isSEPADebitAvailable = it.isSEPADebitAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.SEPA_DEBIT),
isIDEALAvailable = it.isIDEALAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.IDEAL) isIDEALAvailable = it.isIDEALAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.IDEAL),
sepaEuroMaximum = gatewayConfiguration.sepaEuroMaximum
) )
} }
} }

View file

@ -15,6 +15,8 @@ import java.util.Locale;
*/ */
public final class CurrencyUtil { public final class CurrencyUtil {
public static Currency EURO = Currency.getInstance("EUR");
public static @Nullable Currency getCurrencyByCurrencyCode(@NonNull String currencyCode) { public static @Nullable Currency getCurrencyByCurrencyCode(@NonNull String currencyCode) {
try { try {
return Currency.getInstance(currencyCode); return Currency.getInstance(currencyCode);

View file

@ -5855,6 +5855,10 @@
<string name="DonateToSignalFragment__your_payment_is_still_being_processed_onetime">Your donation is still being processed. This can take a few minutes depending on your connection. Please wait until this payment completes before making another donation.</string> <string name="DonateToSignalFragment__your_payment_is_still_being_processed_onetime">Your donation is still being processed. This can take a few minutes depending on your connection. Please wait until this payment completes before making another donation.</string>
<!-- Dialog body when a user opens the manage donations main screen and they have a pending iDEAL donation --> <!-- Dialog body when a user opens the manage donations main screen and they have a pending iDEAL donation -->
<string name="DonateToSignalFragment__your_ideal_payment_is_still_processing">Your iDEAL donation is still processing. Check your banking app to approve your payment before making another donation.</string> <string name="DonateToSignalFragment__your_ideal_payment_is_still_processing">Your iDEAL donation is still processing. Check your banking app to approve your payment before making another donation.</string>
<!-- Dialog title shown when a user tries to donate an amount higher than is allowed for a given payment method. -->
<string name="DonateToSignal__donation_amount_too_high">Donation amount too high</string>
<!-- Dialog body shown when a user tries to donate an amount higher than is allowed for a given payment method, place holder is the maximum -->
<string name="DonateToSignalFragment__you_can_send_up_to_s_via_bank_transfer">You can send up to %1$s via bank transfer. Try a different amount or a different payment method.</string>
<!-- Donation pill toggle monthly text --> <!-- Donation pill toggle monthly text -->
<string name="DonationPillToggle__monthly">Monthly</string> <string name="DonationPillToggle__monthly">Monthly</string>

View file

@ -31,6 +31,9 @@ public class DonationsConfiguration {
@JsonProperty("levels") @JsonProperty("levels")
private Map<Integer, LevelConfiguration> levels; private Map<Integer, LevelConfiguration> levels;
@JsonProperty("sepaMaximumEuros")
private BigDecimal sepaMaximumEuros;
public static class CurrencyConfiguration { public static class CurrencyConfiguration {
@JsonProperty("minimum") @JsonProperty("minimum")
private BigDecimal minimum; private BigDecimal minimum;
@ -84,4 +87,8 @@ public class DonationsConfiguration {
public Map<Integer, LevelConfiguration> getLevels() { public Map<Integer, LevelConfiguration> getLevels() {
return levels; return levels;
} }
public BigDecimal getSepaMaximumEuros() {
return sepaMaximumEuros;
}
} }