Surface retryable failure for slow donations config load.

This commit is contained in:
Alex Hart 2026-06-10 12:28:29 -03:00 committed by Cody Henthorne
parent 41260f37c9
commit d447af36ba
4 changed files with 30 additions and 14 deletions

View File

@ -70,6 +70,12 @@ object InAppPaymentsRepository {
private const val JOB_PREFIX = "InAppPayments__"
private val TAG = Log.tag(InAppPaymentsRepository::class.java)
/**
* Upper bound on how long we'll wait for the donations configuration before surfacing a retryable
* failure rather than leaving the user on an indefinite loading spinner (e.g. on a slow VPN).
*/
const val DONATIONS_CONFIGURATION_TIMEOUT_SECONDS = 30L
private val backupExpirationTimeout = 30.days
private val backupExpirationDeletion = 60.days

View File

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.Currency
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* Shared one-time payment methods that apply to both Stripe and PayPal payments.
@ -77,6 +78,7 @@ object OneTimeInAppPaymentRepository {
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
return Single.fromCallable { AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault()) }
.subscribeOn(Schedulers.io())
.timeout(InAppPaymentsRepository.DONATIONS_CONFIGURATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.flatMap { it.flattenResult() }
.map { config ->
config.getBoostAmounts().mapValues { (_, value) ->
@ -97,6 +99,7 @@ object OneTimeInAppPaymentRepository {
.getDonationsConfiguration(Locale.getDefault())
}
.subscribeOn(Schedulers.io())
.timeout(InAppPaymentsRepository.DONATIONS_CONFIGURATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.flatMap { it.flattenResult() }
.map { it.getBoostBadges().first() }
}
@ -107,8 +110,9 @@ object OneTimeInAppPaymentRepository {
*/
fun getMinimumDonationAmounts(): Single<Map<Currency, FiatMoney>> {
return Single.fromCallable { AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault()) }
.flatMap { it.flattenResult() }
.subscribeOn(Schedulers.io())
.timeout(InAppPaymentsRepository.DONATIONS_CONFIGURATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.flatMap { it.flattenResult() }
.map { it.getMinimumDonationAmounts() }
}

View File

@ -39,6 +39,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds
/**
@ -109,6 +110,7 @@ object RecurringInAppPaymentRepository {
return Single
.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
.subscribeOn(Schedulers.io())
.timeout(InAppPaymentsRepository.DONATIONS_CONFIGURATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.flatMap { it.flattenResult() }
.map { config ->
config.getSubscriptionLevels().map { (level, levelConfig) ->

View File

@ -18,6 +18,7 @@ import org.signal.core.util.money.PlatformCurrencyUtil
import org.signal.core.util.orNull
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
@ -265,15 +266,6 @@ class DonateToSignalViewModel(
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(pendingOneTimeDonation = pendingOneTimeDonation.orNull())) }
}
oneTimeDonationDisposables += oneTimeInAppPaymentRepository.getBoostBadge().subscribeBy(
onSuccess = { badge ->
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) }
},
onError = {
Log.w(TAG, "Could not load boost badge", it)
}
)
oneTimeDonationDisposables += oneTimeInAppPaymentRepository.getMinimumDonationAmounts().subscribeBy(
onSuccess = { amountMap ->
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(minimumDonationAmounts = amountMap)) }
@ -283,10 +275,14 @@ class DonateToSignalViewModel(
}
)
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeInAppPaymentRepository.getBoosts().toObservable()
val boostsAndBadge: Observable<Pair<Map<Currency, List<Boost>>, Badge>> = Single.zip(
oneTimeInAppPaymentRepository.getBoosts(),
oneTimeInAppPaymentRepository.getBoostBadge()
) { boosts, badge -> boosts to badge }.toObservable()
val oneTimeCurrency: Observable<Currency> = SignalStore.inAppPayments.observableOneTimeCurrency
oneTimeDonationDisposables += Observable.combineLatest(boosts, oneTimeCurrency) { boostMap, currency ->
oneTimeDonationDisposables += Observable.combineLatest(boostsAndBadge, oneTimeCurrency) { (boostMap, badge), currency ->
val boostList = if (currency in boostMap) {
boostMap[currency]!!
} else {
@ -294,12 +290,13 @@ class DonateToSignalViewModel(
listOf()
}
Triple(boostList, currency, boostMap.keys)
OneTimeConfiguration(boostList, badge, currency, boostMap.keys)
}.subscribeBy(
onNext = { (boostList, currency, availableCurrencies) ->
onNext = { (boostList, badge, currency, availableCurrencies) ->
store.update { state ->
state.copy(
oneTimeDonationState = state.oneTimeDonationState.copy(
badge = badge,
boosts = boostList,
selectedBoost = null,
selectedCurrency = currency,
@ -321,6 +318,13 @@ class DonateToSignalViewModel(
)
}
private data class OneTimeConfiguration(
val boosts: List<Boost>,
val badge: Badge,
val currency: Currency,
val availableCurrencies: Set<Currency>
)
private fun initializeMonthlyDonationState(subscriptionsRepository: RecurringInAppPaymentRepository) {
monitorLevelUpdateProcessing()