kopia lustrzana https://github.com/ryukoposting/Signal-Android
361 wiersze
14 KiB
Kotlin
361 wiersze
14 KiB
Kotlin
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
import io.reactivex.rxjava3.core.Observable
|
|
import io.reactivex.rxjava3.core.Single
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
|
import org.signal.core.util.StringUtil
|
|
import org.signal.core.util.logging.Log
|
|
import org.signal.core.util.money.FiatMoney
|
|
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
|
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
|
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
|
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.SubscriptionRedemptionJobWatcher
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
|
import org.thoughtcrime.securesms.subscription.Subscription
|
|
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
|
import org.thoughtcrime.securesms.util.next
|
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
|
import java.math.BigDecimal
|
|
import java.text.DecimalFormat
|
|
import java.text.DecimalFormatSymbols
|
|
import java.util.Currency
|
|
|
|
/**
|
|
* Contains the logic to manage the UI of the unified donations screen.
|
|
* Does not directly deal with performing payments, this ViewModel is
|
|
* only in charge of rendering our "current view of the world."
|
|
*/
|
|
class DonateToSignalViewModel(
|
|
startType: DonateToSignalType,
|
|
private val subscriptionsRepository: MonthlyDonationRepository,
|
|
private val oneTimeDonationRepository: OneTimeDonationRepository
|
|
) : ViewModel() {
|
|
|
|
companion object {
|
|
private val TAG = Log.tag(DonateToSignalViewModel::class.java)
|
|
}
|
|
|
|
private val store = RxStore(DonateToSignalState(donateToSignalType = startType))
|
|
private val oneTimeDonationDisposables = CompositeDisposable()
|
|
private val monthlyDonationDisposables = CompositeDisposable()
|
|
private val networkDisposable = CompositeDisposable()
|
|
private val _actions = PublishSubject.create<DonateToSignalAction>()
|
|
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
|
|
|
|
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
|
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
init {
|
|
initializeOneTimeDonationState(oneTimeDonationRepository)
|
|
initializeMonthlyDonationState(subscriptionsRepository)
|
|
|
|
networkDisposable += InternetConnectionObserver
|
|
.observe()
|
|
.distinctUntilChanged()
|
|
.subscribe { isConnected ->
|
|
if (isConnected) {
|
|
retryMonthlyDonationState()
|
|
retryOneTimeDonationState()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun retryMonthlyDonationState() {
|
|
if (!monthlyDonationDisposables.isDisposed && store.state.monthlyDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
|
|
store.update { it.copy(monthlyDonationState = it.monthlyDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
|
|
initializeMonthlyDonationState(subscriptionsRepository)
|
|
}
|
|
}
|
|
|
|
fun retryOneTimeDonationState() {
|
|
if (!oneTimeDonationDisposables.isDisposed && store.state.oneTimeDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
|
|
initializeOneTimeDonationState(oneTimeDonationRepository)
|
|
}
|
|
}
|
|
|
|
fun requestChangeCurrency() {
|
|
val snapshot = store.state
|
|
if (snapshot.canSetCurrency) {
|
|
_actions.onNext(DonateToSignalAction.DisplayCurrencySelectionDialog(snapshot.donateToSignalType, snapshot.selectableCurrencyCodes))
|
|
}
|
|
}
|
|
|
|
fun requestSelectGateway() {
|
|
val snapshot = store.state
|
|
if (snapshot.areFieldsEnabled) {
|
|
_actions.onNext(DonateToSignalAction.DisplayGatewaySelectorDialog(createGatewayRequest(snapshot)))
|
|
}
|
|
}
|
|
|
|
fun updateSubscription() {
|
|
val snapshot = store.state
|
|
if (snapshot.areFieldsEnabled) {
|
|
_actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot)))
|
|
}
|
|
}
|
|
|
|
fun cancelSubscription() {
|
|
val snapshot = store.state
|
|
if (snapshot.areFieldsEnabled) {
|
|
_actions.onNext(DonateToSignalAction.CancelSubscription(createGatewayRequest(snapshot)))
|
|
}
|
|
}
|
|
|
|
fun toggleDonationType() {
|
|
store.update { it.copy(donateToSignalType = it.donateToSignalType.next()) }
|
|
}
|
|
|
|
fun setSelectedSubscription(subscription: Subscription) {
|
|
store.update { it.copy(monthlyDonationState = it.monthlyDonationState.copy(selectedSubscription = subscription)) }
|
|
}
|
|
|
|
fun setSelectedBoost(boost: Boost) {
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(selectedBoost = boost, isCustomAmountFocused = false)) }
|
|
}
|
|
|
|
fun setCustomAmountFocused() {
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(isCustomAmountFocused = true)) }
|
|
}
|
|
|
|
fun setCustomAmount(rawAmount: String) {
|
|
val amount = StringUtil.stripBidiIndicator(rawAmount)
|
|
val bigDecimalAmount: BigDecimal = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) {
|
|
BigDecimal.ZERO
|
|
} else {
|
|
val decimalFormat = DecimalFormat.getInstance() as DecimalFormat
|
|
decimalFormat.isParseBigDecimal = true
|
|
|
|
try {
|
|
decimalFormat.parse(amount) as BigDecimal
|
|
} catch (e: NumberFormatException) {
|
|
BigDecimal.ZERO
|
|
}
|
|
}
|
|
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(customAmount = FiatMoney(bigDecimalAmount, it.oneTimeDonationState.customAmount.currency))) }
|
|
}
|
|
|
|
fun getSelectedSubscriptionCost(): FiatMoney {
|
|
return store.state.monthlyDonationState.selectedSubscription!!.prices.first { it.currency == store.state.selectedCurrency }
|
|
}
|
|
|
|
fun refreshActiveSubscription() {
|
|
subscriptionsRepository
|
|
.getActiveSubscription()
|
|
.subscribeBy(
|
|
onSuccess = {
|
|
_activeSubscription.onNext(it)
|
|
},
|
|
onError = {
|
|
_activeSubscription.onNext(ActiveSubscription.EMPTY)
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun createGatewayRequest(snapshot: DonateToSignalState): GatewayRequest {
|
|
val amount = getAmount(snapshot)
|
|
return GatewayRequest(
|
|
donateToSignalType = snapshot.donateToSignalType,
|
|
badge = snapshot.badge!!,
|
|
label = snapshot.badge!!.description,
|
|
price = amount.amount,
|
|
currencyCode = amount.currency.currencyCode,
|
|
level = snapshot.level.toLong(),
|
|
recipientId = Recipient.self().id
|
|
)
|
|
}
|
|
|
|
private fun getAmount(snapshot: DonateToSignalState): FiatMoney {
|
|
return when (snapshot.donateToSignalType) {
|
|
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
|
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
|
|
DonateToSignalType.GIFT -> error("This ViewModel does not support gifts.")
|
|
}
|
|
}
|
|
|
|
private fun getOneTimeAmount(snapshot: DonateToSignalState.OneTimeDonationState): FiatMoney {
|
|
return if (snapshot.isCustomAmountFocused) {
|
|
snapshot.customAmount
|
|
} else {
|
|
snapshot.selectedBoost!!.price
|
|
}
|
|
}
|
|
|
|
private fun initializeOneTimeDonationState(oneTimeDonationRepository: OneTimeDonationRepository) {
|
|
oneTimeDonationDisposables += oneTimeDonationRepository.getBoostBadge().subscribeBy(
|
|
onSuccess = { badge ->
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) }
|
|
},
|
|
onError = {
|
|
Log.w(TAG, "Could not load boost badge", it)
|
|
}
|
|
)
|
|
|
|
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeDonationRepository.getBoosts().toObservable()
|
|
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
|
|
|
oneTimeDonationDisposables += Observable.combineLatest(boosts, oneTimeCurrency) { boostMap, currency ->
|
|
val boostList = if (currency in boostMap) {
|
|
boostMap[currency]!!
|
|
} else {
|
|
SignalStore.donationsValues().setOneTimeCurrency(PlatformCurrencyUtil.USD)
|
|
listOf()
|
|
}
|
|
|
|
Triple(boostList, currency, boostMap.keys)
|
|
}.subscribeBy(
|
|
onNext = { (boostList, currency, availableCurrencies) ->
|
|
store.update { state ->
|
|
state.copy(
|
|
oneTimeDonationState = state.oneTimeDonationState.copy(
|
|
boosts = boostList,
|
|
selectedCurrency = currency,
|
|
donationStage = DonateToSignalState.DonationStage.READY,
|
|
selectableCurrencyCodes = availableCurrencies.map(Currency::getCurrencyCode),
|
|
isCustomAmountFocused = false,
|
|
customAmount = FiatMoney(
|
|
BigDecimal.ZERO, currency
|
|
)
|
|
)
|
|
)
|
|
}
|
|
},
|
|
onError = {
|
|
Log.w(TAG, "Could not load boost information", it)
|
|
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.FAILURE)) }
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun initializeMonthlyDonationState(subscriptionsRepository: MonthlyDonationRepository) {
|
|
monitorLevelUpdateProcessing()
|
|
|
|
val allSubscriptions = subscriptionsRepository.getSubscriptions()
|
|
ensureValidSubscriptionCurrency(allSubscriptions)
|
|
monitorSubscriptionCurrency()
|
|
monitorSubscriptionState(allSubscriptions)
|
|
refreshActiveSubscription()
|
|
}
|
|
|
|
private fun monitorLevelUpdateProcessing() {
|
|
val isTransactionJobInProgress: Observable<Boolean> = SubscriptionRedemptionJobWatcher.watch().map {
|
|
it.map { jobState ->
|
|
when (jobState) {
|
|
JobTracker.JobState.PENDING -> true
|
|
JobTracker.JobState.RUNNING -> true
|
|
else -> false
|
|
}
|
|
}.orElse(false)
|
|
}
|
|
|
|
monthlyDonationDisposables += Observable
|
|
.combineLatest(isTransactionJobInProgress, LevelUpdate.isProcessing, DonateToSignalState::TransactionState)
|
|
.subscribeBy { transactionState ->
|
|
store.update { state ->
|
|
state.copy(
|
|
monthlyDonationState = state.monthlyDonationState.copy(
|
|
transactionState = transactionState
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun monitorSubscriptionState(allSubscriptions: Single<List<Subscription>>) {
|
|
monthlyDonationDisposables += Observable.combineLatest(allSubscriptions.toObservable(), _activeSubscription, ::Pair).subscribeBy(
|
|
onNext = { (subs, active) ->
|
|
store.update { state ->
|
|
state.copy(
|
|
monthlyDonationState = state.monthlyDonationState.copy(
|
|
subscriptions = subs,
|
|
selectedSubscription = state.monthlyDonationState.selectedSubscription ?: resolveSelectedSubscription(active, subs),
|
|
_activeSubscription = active,
|
|
donationStage = DonateToSignalState.DonationStage.READY,
|
|
selectableCurrencyCodes = subs.firstOrNull()?.prices?.map { it.currency.currencyCode } ?: emptyList()
|
|
)
|
|
)
|
|
}
|
|
},
|
|
onError = {
|
|
store.update { state ->
|
|
state.copy(
|
|
monthlyDonationState = state.monthlyDonationState.copy(
|
|
donationStage = DonateToSignalState.DonationStage.FAILURE
|
|
)
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun resolveSelectedSubscription(activeSubscription: ActiveSubscription, subscriptions: List<Subscription>): Subscription? {
|
|
return if (activeSubscription.isActive) {
|
|
subscriptions.firstOrNull { it.level == activeSubscription.activeSubscription.level }
|
|
} else {
|
|
subscriptions.firstOrNull()
|
|
}
|
|
}
|
|
|
|
private fun ensureValidSubscriptionCurrency(allSubscriptions: Single<List<Subscription>>) {
|
|
monthlyDonationDisposables += allSubscriptions.subscribeBy(
|
|
onSuccess = { subscriptions ->
|
|
if (subscriptions.isNotEmpty()) {
|
|
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
|
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
|
|
|
if (selectedCurrency !in priceCurrencies) {
|
|
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $selectedCurrency isn't supported.")
|
|
val usd = PlatformCurrencyUtil.USD
|
|
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
|
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
|
subscriptionsRepository.syncAccountRecord().subscribe()
|
|
}
|
|
}
|
|
},
|
|
onError = {}
|
|
)
|
|
}
|
|
|
|
private fun monitorSubscriptionCurrency() {
|
|
monthlyDonationDisposables += SignalStore.donationsValues().observableSubscriptionCurrency.subscribe {
|
|
store.update { state ->
|
|
state.copy(monthlyDonationState = state.monthlyDonationState.copy(selectedCurrency = it))
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCleared() {
|
|
super.onCleared()
|
|
oneTimeDonationDisposables.clear()
|
|
monthlyDonationDisposables.clear()
|
|
networkDisposable.clear()
|
|
store.dispose()
|
|
}
|
|
|
|
class Factory(
|
|
private val startType: DonateToSignalType,
|
|
private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
|
private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
|
) : ViewModelProvider.Factory {
|
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, oneTimeDonationRepository)) as T
|
|
}
|
|
}
|
|
}
|