kopia lustrzana https://github.com/ryukoposting/Signal-Android
Respect server currency lists for subscriptions and badges.
rodzic
c06fb81490
commit
2a9eb1bae0
|
@ -13,7 +13,6 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
import org.whispersystems.libsignal.util.guava.Optional
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
@ -44,7 +43,7 @@ class BadgesOverviewViewModel(
|
||||||
|
|
||||||
disposables += Single.zip(
|
disposables += Single.zip(
|
||||||
subscriptionsRepository.getActiveSubscription(),
|
subscriptionsRepository.getActiveSubscription(),
|
||||||
subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency())
|
subscriptionsRepository.getSubscriptions()
|
||||||
) { active, all ->
|
) { active, all ->
|
||||||
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
|
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
|
||||||
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.badges.Badges
|
import org.thoughtcrime.securesms.badges.Badges
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
||||||
|
@ -28,7 +29,7 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriptions(currency: Currency): Single<List<Subscription>> = donationsService.getSubscriptionLevels(Locale.getDefault())
|
fun getSubscriptions(): Single<List<Subscription>> = donationsService.getSubscriptionLevels(Locale.getDefault())
|
||||||
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
|
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
|
||||||
.map { subscriptionLevels ->
|
.map { subscriptionLevels ->
|
||||||
subscriptionLevels.levels.map { (code, level) ->
|
subscriptionLevels.levels.map { (code, level) ->
|
||||||
|
@ -36,7 +37,13 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
|
||||||
id = code,
|
id = code,
|
||||||
name = level.name,
|
name = level.name,
|
||||||
badge = Badges.fromServiceBadge(level.badge),
|
badge = Badges.fromServiceBadge(level.badge),
|
||||||
price = FiatMoney(level.currencies[currency.currencyCode]!!, currency),
|
prices = level.currencies.filter {
|
||||||
|
PlatformCurrencyUtil
|
||||||
|
.getAvailableCurrencyCodes()
|
||||||
|
.contains(it.key)
|
||||||
|
}.map { (currencyCode, price) ->
|
||||||
|
FiatMoney(price, Currency.getInstance(currencyCode))
|
||||||
|
}.toSet(),
|
||||||
level = code.toInt()
|
level = code.toInt()
|
||||||
)
|
)
|
||||||
}.sortedBy {
|
}.sortedBy {
|
||||||
|
|
|
@ -141,10 +141,10 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||||
|
|
||||||
customPref(
|
customPref(
|
||||||
CurrencySelection.Model(
|
CurrencySelection.Model(
|
||||||
currencySelection = state.currencySelection,
|
selectedCurrency = state.currencySelection,
|
||||||
isEnabled = state.stage == BoostState.Stage.READY,
|
isEnabled = state.stage == BoostState.Stage.READY,
|
||||||
onClick = {
|
onClick = {
|
||||||
findNavController().navigate(BoostFragmentDirections.actionBoostFragmentToSetDonationCurrencyFragment(true))
|
findNavController().navigate(BoostFragmentDirections.actionBoostFragmentToSetDonationCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray()))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import io.reactivex.rxjava3.core.Single
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.badges.Badges
|
import org.thoughtcrime.securesms.badges.Badges
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||||
import org.whispersystems.signalservice.api.services.DonationsService
|
import org.whispersystems.signalservice.api.services.DonationsService
|
||||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||||
|
@ -13,12 +14,14 @@ import java.util.Locale
|
||||||
|
|
||||||
class BoostRepository(private val donationsService: DonationsService) {
|
class BoostRepository(private val donationsService: DonationsService) {
|
||||||
|
|
||||||
fun getBoosts(currency: Currency): Single<List<Boost>> {
|
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
|
||||||
return donationsService.boostAmounts
|
return donationsService.boostAmounts
|
||||||
.flatMap(ServiceResponse<Map<String, List<BigDecimal>>>::flattenResult)
|
.flatMap(ServiceResponse<Map<String, List<BigDecimal>>>::flattenResult)
|
||||||
.map { result ->
|
.map { result ->
|
||||||
val boosts = result[currency.currencyCode] ?: throw Exception("Unsupported currency! ${currency.currencyCode}")
|
result
|
||||||
boosts.map { Boost(FiatMoney(it, currency)) }
|
.filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) }
|
||||||
|
.mapKeys { (code, _) -> Currency.getInstance(code) }
|
||||||
|
.mapValues { (currency, prices) -> prices.map { Boost(FiatMoney(it, currency)) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||||
|
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
|
|
||||||
data class BoostState(
|
data class BoostState(
|
||||||
val boostBadge: Badge? = null,
|
val boostBadge: Badge? = null,
|
||||||
val currencySelection: CurrencySelection = CurrencySelection("USD"),
|
val currencySelection: Currency,
|
||||||
val isGooglePayAvailable: Boolean = false,
|
val isGooglePayAvailable: Boolean = false,
|
||||||
val boosts: List<Boost> = listOf(),
|
val boosts: List<Boost> = listOf(),
|
||||||
val selectedBoost: Boost? = null,
|
val selectedBoost: Boost? = null,
|
||||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, Currency.getInstance(currencySelection.selectedCurrencyCode)),
|
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, currencySelection),
|
||||||
val isCustomAmountFocused: Boolean = false,
|
val isCustomAmountFocused: Boolean = false,
|
||||||
val stage: Stage = Stage.INIT,
|
val stage: Stage = Stage.INIT,
|
||||||
|
val supportedCurrencyCodes: List<String> = emptyList()
|
||||||
) {
|
) {
|
||||||
enum class Stage {
|
enum class Stage {
|
||||||
INIT,
|
INIT,
|
||||||
|
|
|
@ -17,10 +17,11 @@ import org.signal.donations.GooglePayApi
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
class BoostViewModel(
|
class BoostViewModel(
|
||||||
private val boostRepository: BoostRepository,
|
private val boostRepository: BoostRepository,
|
||||||
|
@ -28,7 +29,7 @@ class BoostViewModel(
|
||||||
private val fetchTokenRequestCode: Int
|
private val fetchTokenRequestCode: Int
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(BoostState())
|
private val store = Store(BoostState(currencySelection = SignalStore.donationsValues().getBoostCurrency()))
|
||||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
@ -41,15 +42,26 @@ class BoostViewModel(
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSupportedCurrencyCodes(): List<String> {
|
||||||
|
return store.state.supportedCurrencyCodes
|
||||||
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
|
|
||||||
val currencyObservable = SignalStore.donationsValues().observableBoostCurrency
|
val currencyObservable = SignalStore.donationsValues().observableBoostCurrency
|
||||||
val boosts = currencyObservable.flatMapSingle { boostRepository.getBoosts(it) }
|
val allBoosts = boostRepository.getBoosts()
|
||||||
val boostBadge = boostRepository.getBoostBadge()
|
val boostBadge = boostRepository.getBoostBadge()
|
||||||
|
|
||||||
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) { boostList, badge ->
|
disposables += Observable.combineLatest(currencyObservable, allBoosts.toObservable(), boostBadge.toObservable()) { currency, boostMap, badge ->
|
||||||
BoostInfo(boostList, boostList[2], badge)
|
val boostList = if (currency in boostMap) {
|
||||||
|
boostMap[currency]!!
|
||||||
|
} else {
|
||||||
|
SignalStore.donationsValues().setBoostCurrency(PlatformCurrencyUtil.USD)
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
BoostInfo(boostList, boostList[2], badge, boostMap.keys)
|
||||||
}.subscribeBy(
|
}.subscribeBy(
|
||||||
onNext = { info ->
|
onNext = { info ->
|
||||||
store.update {
|
store.update {
|
||||||
|
@ -57,7 +69,8 @@ class BoostViewModel(
|
||||||
boosts = info.boosts,
|
boosts = info.boosts,
|
||||||
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
|
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
|
||||||
boostBadge = it.boostBadge ?: info.boostBadge,
|
boostBadge = it.boostBadge ?: info.boostBadge,
|
||||||
stage = if (it.stage == BoostState.Stage.INIT || it.stage == BoostState.Stage.FAILURE) BoostState.Stage.READY else it.stage
|
stage = if (it.stage == BoostState.Stage.INIT || it.stage == BoostState.Stage.FAILURE) BoostState.Stage.READY else it.stage,
|
||||||
|
supportedCurrencyCodes = info.supportedCurrencies.map(Currency::getCurrencyCode)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -77,7 +90,7 @@ class BoostViewModel(
|
||||||
disposables += currencyObservable.subscribeBy { currency ->
|
disposables += currencyObservable.subscribeBy { currency ->
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
currencySelection = CurrencySelection(currency.currencyCode),
|
currencySelection = currency,
|
||||||
isCustomAmountFocused = false,
|
isCustomAmountFocused = false,
|
||||||
customAmount = FiatMoney(
|
customAmount = FiatMoney(
|
||||||
BigDecimal.ZERO, currency
|
BigDecimal.ZERO, currency
|
||||||
|
@ -169,7 +182,7 @@ class BoostViewModel(
|
||||||
store.update { it.copy(isCustomAmountFocused = isFocused) }
|
store.update { it.copy(isCustomAmountFocused = isFocused) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class BoostInfo(val boosts: List<Boost>, val defaultBoost: Boost?, val boostBadge: Badge)
|
private data class BoostInfo(val boosts: List<Boost>, val defaultBoost: Boost?, val boostBadge: Badge, val supportedCurrencies: Set<Currency>)
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val boostRepository: BoostRepository,
|
private val boostRepository: BoostRepository,
|
||||||
|
|
|
@ -15,7 +15,8 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
|
||||||
|
|
||||||
private val viewModel: SetCurrencyViewModel by viewModels(
|
private val viewModel: SetCurrencyViewModel by viewModels(
|
||||||
factoryProducer = {
|
factoryProducer = {
|
||||||
SetCurrencyViewModel.Factory(SetCurrencyFragmentArgs.fromBundle(requireArguments()).isBoost)
|
val args = SetCurrencyFragmentArgs.fromBundle(requireArguments())
|
||||||
|
SetCurrencyViewModel.Factory(args.isBoost, args.supportedCurrencyCodes.toList())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.VisibleForTesting
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.signal.donations.StripeApi
|
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
@ -14,32 +13,26 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class SetCurrencyViewModel(private val isBoost: Boolean) : ViewModel() {
|
class SetCurrencyViewModel(
|
||||||
|
private val isBoost: Boolean,
|
||||||
|
supportedCurrencyCodes: List<String>
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(SetCurrencyState())
|
private val store = Store(
|
||||||
|
SetCurrencyState(
|
||||||
|
selectedCurrencyCode = if (isBoost) {
|
||||||
|
SignalStore.donationsValues().getBoostCurrency().currencyCode
|
||||||
|
} else {
|
||||||
|
SignalStore.donationsValues().getSubscriptionCurrency().currencyCode
|
||||||
|
},
|
||||||
|
currencies = supportedCurrencyCodes
|
||||||
|
.map(Currency::getInstance)
|
||||||
|
.sortedWith(CurrencyComparator(BuildConfig.DEFAULT_CURRENCIES.split(",")))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val state: LiveData<SetCurrencyState> = store.stateLiveData
|
val state: LiveData<SetCurrencyState> = store.stateLiveData
|
||||||
|
|
||||||
init {
|
|
||||||
val defaultCurrency = if (isBoost) {
|
|
||||||
SignalStore.donationsValues().getBoostCurrency()
|
|
||||||
} else {
|
|
||||||
SignalStore.donationsValues().getSubscriptionCurrency()
|
|
||||||
}
|
|
||||||
|
|
||||||
store.update { state ->
|
|
||||||
val platformCurrencies = Currency.getAvailableCurrencies()
|
|
||||||
val stripeCurrencies = platformCurrencies
|
|
||||||
.filter { StripeApi.Validation.supportedCurrencyCodes.contains(it.currencyCode) }
|
|
||||||
.sortedWith(CurrencyComparator(BuildConfig.DEFAULT_CURRENCIES.split(",")))
|
|
||||||
|
|
||||||
state.copy(
|
|
||||||
selectedCurrencyCode = defaultCurrency.currencyCode,
|
|
||||||
currencies = stripeCurrencies
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedCurrency(selectedCurrencyCode: String) {
|
fun setSelectedCurrency(selectedCurrencyCode: String) {
|
||||||
store.update { it.copy(selectedCurrencyCode = selectedCurrencyCode) }
|
store.update { it.copy(selectedCurrencyCode = selectedCurrencyCode) }
|
||||||
|
|
||||||
|
@ -93,9 +86,9 @@ class SetCurrencyViewModel(private val isBoost: Boolean) : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(private val isBoost: Boolean) : ViewModelProvider.Factory {
|
class Factory(private val isBoost: Boolean, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
return modelClass.cast(SetCurrencyViewModel(isBoost))!!
|
return modelClass.cast(SetCurrencyViewModel(isBoost, supportedCurrencyCodes))!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.google.android.material.button.MaterialButton
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
|
@ -51,7 +52,7 @@ object ActiveSubscriptionPreference {
|
||||||
R.string.MySupportPreference__s_per_month,
|
R.string.MySupportPreference__s_per_month,
|
||||||
FiatMoneyUtil.format(
|
FiatMoneyUtil.format(
|
||||||
context.resources,
|
context.resources,
|
||||||
model.subscription.price,
|
model.subscription.prices.first { it.currency == SignalStore.donationsValues().getSubscriptionCurrency() },
|
||||||
FiatMoneyUtil.formatOptions()
|
FiatMoneyUtil.formatOptions()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
@ -66,7 +65,7 @@ class ManageDonationsViewModel(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
disposables += subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency()).subscribeBy(
|
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
|
||||||
onSuccess = { subs ->
|
onSuccess = { subs ->
|
||||||
store.update { it.copy(availableSubscriptions = subs) }
|
store.update { it.copy(availableSubscriptions = subs) }
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,19 +6,16 @@ import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
data class CurrencySelection(
|
object CurrencySelection {
|
||||||
val selectedCurrencyCode: String,
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun register(adapter: MappingAdapter) {
|
fun register(adapter: MappingAdapter) {
|
||||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_currency_selection))
|
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_currency_selection))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class Model(
|
class Model(
|
||||||
val currencySelection: CurrencySelection,
|
val selectedCurrency: Currency,
|
||||||
override val isEnabled: Boolean,
|
override val isEnabled: Boolean,
|
||||||
val onClick: () -> Unit
|
val onClick: () -> Unit
|
||||||
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
||||||
|
@ -28,7 +25,7 @@ data class CurrencySelection(
|
||||||
|
|
||||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
return super.areContentsTheSame(newItem) &&
|
return super.areContentsTheSame(newItem) &&
|
||||||
newItem.currencySelection.selectedCurrencyCode == currencySelection.selectedCurrencyCode
|
newItem.selectedCurrency == selectedCurrency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +34,7 @@ data class CurrencySelection(
|
||||||
private val spinner: TextView = itemView.findViewById(R.id.subscription_currency_selection_spinner)
|
private val spinner: TextView = itemView.findViewById(R.id.subscription_currency_selection_spinner)
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
spinner.text = model.currencySelection.selectedCurrencyCode
|
spinner.text = model.selectedCurrency.currencyCode
|
||||||
|
|
||||||
itemView.setOnClickListener { model.onClick() }
|
itemView.setOnClickListener { model.onClick() }
|
||||||
|
|
||||||
|
|
|
@ -118,10 +118,13 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||||
|
|
||||||
customPref(
|
customPref(
|
||||||
CurrencySelection.Model(
|
CurrencySelection.Model(
|
||||||
currencySelection = state.currencySelection,
|
selectedCurrency = state.currencySelection,
|
||||||
isEnabled = areFieldsEnabled && state.activeSubscription?.isActive != true,
|
isEnabled = areFieldsEnabled && state.activeSubscription?.isActive != true,
|
||||||
onClick = {
|
onClick = {
|
||||||
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSetDonationCurrencyFragment(false))
|
val selectableCurrencies = viewModel.getSelectableCurrencyCodes()
|
||||||
|
if (selectableCurrencies != null) {
|
||||||
|
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSetDonationCurrencyFragment(false, selectableCurrencies.toTypedArray()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -138,7 +141,8 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
willRenew = isActive && state.activeSubscription?.activeSubscription?.willCancelAtPeriodEnd() ?: false,
|
willRenew = isActive && state.activeSubscription?.activeSubscription?.willCancelAtPeriodEnd() ?: false,
|
||||||
onClick = { viewModel.setSelectedSubscription(it) },
|
onClick = { viewModel.setSelectedSubscription(it) },
|
||||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(state.activeSubscription?.activeSubscription?.endOfCurrentPeriod ?: 0L)
|
renewalTimestamp = TimeUnit.SECONDS.toMillis(state.activeSubscription?.activeSubscription?.endOfCurrentPeriod ?: 0L),
|
||||||
|
selectedCurrency = state.currencySelection
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -154,7 +158,7 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||||
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
||||||
isEnabled = areFieldsEnabled && (!activeAndSameLevel || isExpiring),
|
isEnabled = areFieldsEnabled && (!activeAndSameLevel || isExpiring),
|
||||||
onClick = {
|
onClick = {
|
||||||
val price = viewModel.state.value?.selectedSubscription?.price ?: return@primaryButton
|
val price = viewModel.getPriceOfSelectedSubscription() ?: return@primaryButton
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
|
|
||||||
calendar.add(Calendar.MONTH, 1)
|
calendar.add(Calendar.MONTH, 1)
|
||||||
|
@ -222,7 +226,7 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onGooglePayButtonClicked() {
|
private fun onGooglePayButtonClicked() {
|
||||||
viewModel.requestTokenFromGooglePay(requireContext())
|
viewModel.requestTokenFromGooglePay()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPaymentConfirmed(badge: Badge) {
|
private fun onPaymentConfirmed(badge: Badge) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
data class SubscribeState(
|
data class SubscribeState(
|
||||||
val currencySelection: CurrencySelection = CurrencySelection("USD"),
|
val currencySelection: Currency,
|
||||||
val subscriptions: List<Subscription> = listOf(),
|
val subscriptions: List<Subscription> = listOf(),
|
||||||
val selectedSubscription: Subscription? = null,
|
val selectedSubscription: Subscription? = null,
|
||||||
val activeSubscription: ActiveSubscription? = null,
|
val activeSubscription: ActiveSubscription? = null,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -8,21 +7,26 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.gms.wallet.PaymentData
|
import com.google.android.gms.wallet.PaymentData
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
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 io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.signal.donations.GooglePayApi
|
import org.signal.donations.GooglePayApi
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||||
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
import org.thoughtcrime.securesms.subscription.Subscription
|
||||||
|
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
|
|
||||||
class SubscribeViewModel(
|
class SubscribeViewModel(
|
||||||
|
@ -31,7 +35,7 @@ class SubscribeViewModel(
|
||||||
private val fetchTokenRequestCode: Int
|
private val fetchTokenRequestCode: Int
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val store = Store(SubscribeState())
|
private val store = Store(SubscribeState(currencySelection = SignalStore.donationsValues().getSubscriptionCurrency()))
|
||||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||||
private val disposables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
@ -45,11 +49,20 @@ class SubscribeViewModel(
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPriceOfSelectedSubscription(): FiatMoney? {
|
||||||
|
return store.state.selectedSubscription?.prices?.first { it.currency == store.state.currencySelection }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelectableCurrencyCodes(): List<String>? {
|
||||||
|
return store.state.subscriptions.firstOrNull()?.prices?.map { it.currency.currencyCode }
|
||||||
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
|
|
||||||
val currency: Observable<Currency> = SignalStore.donationsValues().observableSubscriptionCurrency
|
val currency: Observable<Currency> = SignalStore.donationsValues().observableSubscriptionCurrency
|
||||||
val allSubscriptions: Observable<List<Subscription>> = currency.switchMapSingle { subscriptionsRepository.getSubscriptions(it) }
|
val allSubscriptions: Single<List<Subscription>> = subscriptionsRepository.getSubscriptions()
|
||||||
|
|
||||||
refreshActiveSubscription()
|
refreshActiveSubscription()
|
||||||
|
|
||||||
disposables += LevelUpdate.isProcessing.subscribeBy {
|
disposables += LevelUpdate.isProcessing.subscribeBy {
|
||||||
|
@ -60,7 +73,25 @@ class SubscribeViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disposables += Observable.combineLatest(allSubscriptions, activeSubscriptionSubject, ::Pair).subscribeBy(
|
disposables += 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. $currency isn't supported.")
|
||||||
|
val usd = PlatformCurrencyUtil.USD
|
||||||
|
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
||||||
|
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
disposables += Observable.combineLatest(allSubscriptions.toObservable(), activeSubscriptionSubject, ::Pair).subscribeBy(
|
||||||
onNext = { (subs, active) ->
|
onNext = { (subs, active) ->
|
||||||
store.update {
|
store.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -79,7 +110,7 @@ class SubscribeViewModel(
|
||||||
onError = { eventPublisher.onNext(DonationEvent.GooglePayUnavailableError(it)) }
|
onError = { eventPublisher.onNext(DonationEvent.GooglePayUnavailableError(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
disposables += currency.map { CurrencySelection(it.currencyCode) }.subscribe { selection ->
|
disposables += currency.subscribe { selection ->
|
||||||
store.update { it.copy(currencySelection = selection) }
|
store.update { it.copy(currencySelection = selection) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +221,7 @@ class SubscribeViewModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestTokenFromGooglePay(context: Context) {
|
fun requestTokenFromGooglePay() {
|
||||||
val snapshot = store.state
|
val snapshot = store.state
|
||||||
if (snapshot.selectedSubscription == null) {
|
if (snapshot.selectedSubscription == null) {
|
||||||
return
|
return
|
||||||
|
@ -198,8 +229,10 @@ class SubscribeViewModel(
|
||||||
|
|
||||||
store.update { it.copy(stage = SubscribeState.Stage.TOKEN_REQUEST) }
|
store.update { it.copy(stage = SubscribeState.Stage.TOKEN_REQUEST) }
|
||||||
|
|
||||||
|
val selectedCurrency = snapshot.currencySelection
|
||||||
|
|
||||||
subscriptionToPurchase = snapshot.selectedSubscription
|
subscriptionToPurchase = snapshot.selectedSubscription
|
||||||
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedSubscription.price, snapshot.selectedSubscription.name, fetchTokenRequestCode)
|
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedSubscription.prices.first { it.currency == selectedCurrency }, snapshot.selectedSubscription.name, fetchTokenRequestCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedSubscription(subscription: Subscription) {
|
fun setSelectedSubscription(subscription: Subscription) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +23,7 @@ data class Subscription(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val badge: Badge,
|
val badge: Badge,
|
||||||
val price: FiatMoney,
|
val prices: Set<FiatMoney>,
|
||||||
val level: Int,
|
val level: Int,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -39,7 +40,8 @@ data class Subscription(
|
||||||
val willRenew: Boolean,
|
val willRenew: Boolean,
|
||||||
override val isEnabled: Boolean,
|
override val isEnabled: Boolean,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
val renewalTimestamp: Long
|
val renewalTimestamp: Long,
|
||||||
|
val selectedCurrency: Currency
|
||||||
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
||||||
|
|
||||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
@ -52,7 +54,8 @@ data class Subscription(
|
||||||
newItem.isSelected == isSelected &&
|
newItem.isSelected == isSelected &&
|
||||||
newItem.isActive == isActive &&
|
newItem.isActive == isActive &&
|
||||||
newItem.renewalTimestamp == renewalTimestamp &&
|
newItem.renewalTimestamp == renewalTimestamp &&
|
||||||
newItem.willRenew == willRenew
|
newItem.willRenew == willRenew &&
|
||||||
|
newItem.selectedCurrency == selectedCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChangePayload(newItem: Model): Any? {
|
override fun getChangePayload(newItem: Model): Any? {
|
||||||
|
@ -86,7 +89,7 @@ data class Subscription(
|
||||||
|
|
||||||
val formattedPrice = FiatMoneyUtil.format(
|
val formattedPrice = FiatMoneyUtil.format(
|
||||||
context.resources,
|
context.resources,
|
||||||
model.subscription.price,
|
model.subscription.prices.first { it.currency == model.selectedCurrency },
|
||||||
FiatMoneyUtil.formatOptions()
|
FiatMoneyUtil.formatOptions()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import java.util.Currency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for java.util.Currency
|
||||||
|
*
|
||||||
|
* This is prefixed with "Platform" as there are several different Currency classes
|
||||||
|
* available in the app, and this utility class is specifically for dealing with
|
||||||
|
* java.util.Currency
|
||||||
|
*/
|
||||||
|
object PlatformCurrencyUtil {
|
||||||
|
|
||||||
|
val USD: Currency = Currency.getInstance("USD")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: Adding this as an extension method of Currency causes some confusion in
|
||||||
|
* AndroidStudio due to a separate Currency class from the AndroidSDK having
|
||||||
|
* an extension method of the same signature.
|
||||||
|
*/
|
||||||
|
fun getAvailableCurrencyCodes(): Set<String> {
|
||||||
|
return Currency.getAvailableCurrencies().map { it.currencyCode }.toSet()
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,10 @@
|
||||||
<argument
|
<argument
|
||||||
android:name="isBoost"
|
android:name="isBoost"
|
||||||
app:argType="boolean" />
|
app:argType="boolean" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="supportedCurrencyCodes"
|
||||||
|
app:argType="string[]" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
|
|
@ -73,6 +73,9 @@
|
||||||
android:name="isBoost"
|
android:name="isBoost"
|
||||||
app:argType="boolean" />
|
app:argType="boolean" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="supportedCurrencyCodes"
|
||||||
|
app:argType="string[]" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
|
|
Ładowanie…
Reference in New Issue