Add currency selection logic update.

This commit is contained in:
Alex Hart 2022-12-21 14:14:29 -04:00 committed by Greyson Parrelli
parent 055b4691d7
commit 1e2f7f0775
8 changed files with 96 additions and 30 deletions

View file

@ -95,11 +95,15 @@ fun DonationsConfiguration.getMinimumDonationAmounts(paymentMethodAvailability:
.mapValues { FiatMoney(it.value.minimum, it.key) } .mapValues { FiatMoney(it.value.minimum, it.key) }
} }
fun DonationsConfiguration.getAvailablePaymentMethods(currencyCode: String): Set<String> {
return currencies[currencyCode.lowercase()]?.supportedPaymentMethods ?: emptySet()
}
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()
return currencies.filter { (code, config) -> return currencies.filter { (code, config) ->
val areAllMethodsAvailable = config.supportedPaymentMethods.containsAll(userPaymentMethods) val areAllMethodsAvailable = config.supportedPaymentMethods.any { it in userPaymentMethods }
availableCurrencyCodes.contains(code.uppercase()) && areAllMethodsAvailable availableCurrencyCodes.contains(code.uppercase()) && areAllMethodsAvailable
} }
} }

View file

@ -241,6 +241,7 @@ class DonateToSignalViewModel(
state.copy( state.copy(
oneTimeDonationState = state.oneTimeDonationState.copy( oneTimeDonationState = state.oneTimeDonationState.copy(
boosts = boostList, boosts = boostList,
selectedBoost = null,
selectedCurrency = currency, selectedCurrency = currency,
donationStage = DonateToSignalState.DonationStage.READY, donationStage = DonateToSignalState.DonationStage.READY,
selectableCurrencyCodes = availableCurrencies.map(Currency::getCurrencyCode), selectableCurrencyCodes = availableCurrencies.map(Currency::getCurrencyCode),

View file

@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
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.payments.FiatMoneyUtil import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
@ -42,6 +43,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
BadgeDisplay112.register(adapter) BadgeDisplay112.register(adapter)
GooglePayButton.register(adapter) GooglePayButton.register(adapter)
PayPalButton.register(adapter) PayPalButton.register(adapter)
IndeterminateLoadingCircle.register(adapter)
lifecycleDisposable.bindTo(viewLifecycleOwner) lifecycleDisposable.bindTo(viewLifecycleOwner)
@ -65,6 +67,12 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
space(66.dp) space(66.dp)
if (state.loading) {
customPref(IndeterminateLoadingCircle)
space(16.dp)
return@configure
}
if (state.isGooglePayAvailable) { if (state.isGooglePayAvailable) {
customPref( customPref(
GooglePayButton.Model( GooglePayButton.Model(

View file

@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
import io.reactivex.rxjava3.core.Single
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
import org.whispersystems.signalservice.api.services.DonationsService
import java.util.Locale
class GatewaySelectorRepository(
private val donationsService: DonationsService
) {
fun getAvailableGateways(currencyCode: String): Single<Set<GatewayResponse.Gateway>> {
return Single.fromCallable {
donationsService.getDonationsConfiguration(Locale.getDefault())
}.flatMap { it.flattenResult() }
.map { configuration ->
configuration.getAvailablePaymentMethods(currencyCode).map {
when (it) {
"PAYPAL" -> listOf(GatewayResponse.Gateway.PAYPAL)
"CARD" -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
else -> listOf()
}
}.flatten().toSet()
}
}
}

View file

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
data class GatewaySelectorState( data class GatewaySelectorState(
val loading: Boolean = true,
val badge: Badge, val badge: Badge,
val isGooglePayAvailable: Boolean = false, val isGooglePayAvailable: Boolean = false,
val isPayPalAvailable: Boolean = false, val isPayPalAvailable: Boolean = false,

View file

@ -2,18 +2,21 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.donations.PaymentSourceType import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.rx.RxStore
class GatewaySelectorViewModel( class GatewaySelectorViewModel(
args: GatewaySelectorBottomSheetArgs, args: GatewaySelectorBottomSheetArgs,
private val repository: StripeRepository repository: StripeRepository,
gatewaySelectorRepository: GatewaySelectorRepository
) : ViewModel() { ) : ViewModel() {
private val store = RxStore( private val store = RxStore(
@ -29,7 +32,19 @@ class GatewaySelectorViewModel(
val state = store.stateFlowable val state = store.stateFlowable
init { init {
checkIfGooglePayIsAvailable() val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false)
val availabilitySet = gatewaySelectorRepository.getAvailableGateways(currencyCode = args.request.currencyCode)
disposables += Single.zip(isGooglePayAvailable, availabilitySet, ::Pair).subscribeBy { (googlePayAvailable, gatewaysAvailable) ->
SignalStore.donationsValues().isGooglePayReady = googlePayAvailable
store.update {
it.copy(
loading = false,
isCreditCardAvailable = it.isCreditCardAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.CREDIT_CARD),
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.GOOGLE_PAY),
isPayPalAvailable = it.isPayPalAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.PAYPAL)
)
}
}
} }
override fun onCleared() { override fun onCleared() {
@ -37,25 +52,13 @@ class GatewaySelectorViewModel(
disposables.clear() disposables.clear()
} }
private fun checkIfGooglePayIsAvailable() {
disposables += repository.isGooglePayAvailable().subscribeBy(
onComplete = {
SignalStore.donationsValues().isGooglePayReady = true
store.update { it.copy(isGooglePayAvailable = true) }
},
onError = {
SignalStore.donationsValues().isGooglePayReady = false
store.update { it.copy(isGooglePayAvailable = false) }
}
)
}
class Factory( class Factory(
private val args: GatewaySelectorBottomSheetArgs, private val args: GatewaySelectorBottomSheetArgs,
private val repository: StripeRepository private val repository: StripeRepository,
private val gatewaySelectorRepository: GatewaySelectorRepository = GatewaySelectorRepository(ApplicationDependencies.getDonationsService())
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(GatewaySelectorViewModel(args, repository)) as T return modelClass.cast(GatewaySelectorViewModel(args, repository, gatewaySelectorRepository)) as T
} }
} }
} }

View file

@ -24,11 +24,11 @@ class DonationsConfigurationExtensionsKtTest {
private val testSubject = JsonUtil.fromJson(testData, DonationsConfiguration::class.java) private val testSubject = JsonUtil.fromJson(testData, DonationsConfiguration::class.java)
@Test @Test
fun `Given all methods are available, when I getSubscriptionAmounts, then I expect BIF`() { fun `Given all methods are available, when I getSubscriptionAmounts, then I expect all currencies`() {
val subscriptionPrices = testSubject.getSubscriptionAmounts(DonationsConfiguration.SUBSCRIPTION_LEVELS.first(), AllPaymentMethodsAvailability) val subscriptionPrices = testSubject.getSubscriptionAmounts(DonationsConfiguration.SUBSCRIPTION_LEVELS.first(), AllPaymentMethodsAvailability)
assertEquals(1, subscriptionPrices.size) assertEquals(3, subscriptionPrices.size)
assertEquals("BIF", subscriptionPrices.first().currency.currencyCode) assertTrue(subscriptionPrices.map { it.currency.currencyCode }.containsAll(setOf("JPY", "BIF", "USD")))
} }
@Test @Test
@ -84,11 +84,13 @@ class DonationsConfigurationExtensionsKtTest {
} }
@Test @Test
fun `Given all methods are available, when I getGiftAmounts, then I expect BIF`() { fun `Given all methods are available, when I getGiftAmounts, then I expect BIF and JPY and USD`() {
val giftAmounts = testSubject.getGiftBadgeAmounts(AllPaymentMethodsAvailability) val giftAmounts = testSubject.getGiftBadgeAmounts(AllPaymentMethodsAvailability)
assertEquals(1, giftAmounts.size) assertEquals(3, giftAmounts.size)
assertNotNull(giftAmounts[Currency.getInstance("BIF")]) assertNotNull(giftAmounts[Currency.getInstance("BIF")])
assertNotNull(giftAmounts[Currency.getInstance("JPY")])
assertNotNull(giftAmounts[Currency.getInstance("USD")])
} }
@Test @Test
@ -108,11 +110,13 @@ class DonationsConfigurationExtensionsKtTest {
} }
@Test @Test
fun `Given all methods are available, when I getBoostAmounts, then I expect BIF`() { fun `Given all methods are available, when I getBoostAmounts, then I expect BIF and JPY and USD`() {
val boostAmounts = testSubject.getBoostAmounts(AllPaymentMethodsAvailability) val boostAmounts = testSubject.getBoostAmounts(AllPaymentMethodsAvailability)
assertEquals(1, boostAmounts.size) assertEquals(3, boostAmounts.size)
assertNotNull(boostAmounts[Currency.getInstance("BIF")]) assertNotNull(boostAmounts[Currency.getInstance("BIF")])
assertNotNull(boostAmounts[Currency.getInstance("JPY")])
assertNotNull(boostAmounts[Currency.getInstance("USD")])
} }
@Test @Test
@ -132,11 +136,13 @@ class DonationsConfigurationExtensionsKtTest {
} }
@Test @Test
fun `Given all methods are available, when I getMinimumDonationAmounts, then I expect BIF`() { fun `Given all methods are available, when I getMinimumDonationAmounts, then I expect BIF and JPY and USD`() {
val minimumDonationAmounts = testSubject.getMinimumDonationAmounts(AllPaymentMethodsAvailability) val minimumDonationAmounts = testSubject.getMinimumDonationAmounts(AllPaymentMethodsAvailability)
assertEquals(1, minimumDonationAmounts.size) assertEquals(3, minimumDonationAmounts.size)
assertNotNull(minimumDonationAmounts[Currency.getInstance("BIF")]) assertNotNull(minimumDonationAmounts[Currency.getInstance("BIF")])
assertNotNull(minimumDonationAmounts[Currency.getInstance("JPY")])
assertNotNull(minimumDonationAmounts[Currency.getInstance("USD")])
} }
@Test @Test
@ -185,6 +191,24 @@ class DonationsConfigurationExtensionsKtTest {
} }
} }
@Test
fun `Given I want to pay in USD, when I getAvailablePaymentMethods, then I expect CARD`() {
val availablePaymentMethods = testSubject.getAvailablePaymentMethods("UsD")
assertEquals(1, availablePaymentMethods.size)
assertTrue("CARD" in availablePaymentMethods)
}
@Test
fun `Given I want to pay in BIF, when I getAvailablePaymentMethods, then I expect CARD and PAYPAL`() {
val availablePaymentMethods = testSubject.getAvailablePaymentMethods("bIF")
println(testSubject.currencies)
assertEquals(2, availablePaymentMethods.size)
assertTrue("CARD" in availablePaymentMethods)
assertTrue("PAYPAL" in availablePaymentMethods)
}
private object AllPaymentMethodsAvailability : PaymentMethodAvailability { private object AllPaymentMethodsAvailability : PaymentMethodAvailability {
override fun isPayPalAvailable(): Boolean = true override fun isPayPalAvailable(): Boolean = true
override fun isGooglePayOrCreditCardAvailable(): Boolean = true override fun isGooglePayOrCreditCardAvailable(): Boolean = true

View file

@ -1,6 +1,6 @@
{ {
"currencies": { "currencies": {
"JPY": { "jpy": {
"minimum": 300, "minimum": 300,
"oneTime": { "oneTime": {
"1": [ "1": [
@ -24,7 +24,7 @@
"PAYPAL" "PAYPAL"
] ]
}, },
"USD": { "usd": {
"minimum": 2.5, "minimum": 2.5,
"oneTime": { "oneTime": {
"1": [ "1": [
@ -48,7 +48,7 @@
"CARD" "CARD"
] ]
}, },
"BIF": { "bif": {
"minimum": 3000, "minimum": 3000,
"oneTime": { "oneTime": {
"1": [ "1": [