Add more polish to Badges.

* Better network error handling
* Marking user cancellations so we don't annoy them
* Manage Profile screen treatment.
This commit is contained in:
Alex Hart
2021-10-29 14:05:22 -03:00
committed by Greyson Parrelli
parent 17517cfc88
commit 1af15842cc
19 changed files with 207 additions and 70 deletions

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -37,6 +38,9 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
subscriptionsRepository.getActiveSubscription().subscribeBy(
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.isActive) } },
onError = { throwable ->
Log.w(TAG, "Could not load active subscription", throwable)
}
)
}
@@ -45,4 +49,8 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
return modelClass.cast(AppSettingsViewModel(subscriptionsRepository)) as T
}
}
companion object {
private val TAG = Log.tag(AppSettingsViewModel::class.java)
}
}

View File

@@ -141,6 +141,7 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
subscriber.currencyCode,
levelUpdateOperation.idempotencyKey.serialize()
).flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().andThen {
SignalStore.donationsValues().clearUserManuallyCancelled()
SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation)
it.onComplete()
}.andThen {

View File

@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.subscription.Subscription
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
import org.whispersystems.signalservice.internal.ServiceResponse
import java.util.Currency
@@ -26,8 +27,9 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
}
}
fun getSubscriptions(currency: Currency): Single<List<Subscription>> = donationsService.subscriptionLevels.map { response ->
response.result.transform { subscriptionLevels ->
fun getSubscriptions(currency: Currency): Single<List<Subscription>> = donationsService.subscriptionLevels
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
.map { subscriptionLevels ->
subscriptionLevels.levels.map { (code, level) ->
Subscription(
id = code,
@@ -38,6 +40,5 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
}.sortedBy {
it.level
}
}.or(emptyList())
}
}
}

View File

@@ -58,6 +58,8 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel.refresh()
CurrencySelection.register(adapter)
BadgePreview.register(adapter)
Boost.register(adapter)

View File

@@ -21,5 +21,6 @@ data class BoostState(
READY,
TOKEN_REQUEST,
PAYMENT_PIPELINE,
FAILURE
}
}

View File

@@ -11,6 +11,7 @@ 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.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.badges.models.Badge
@@ -40,24 +41,33 @@ class BoostViewModel(
disposables.clear()
}
init {
fun refresh() {
disposables.clear()
val currencyObservable = SignalStore.donationsValues().observableBoostCurrency
val boosts = currencyObservable.flatMapSingle { boostRepository.getBoosts(it) }
val boostBadge = boostRepository.getBoostBadge()
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) {
boostList, badge ->
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) { boostList, badge ->
BoostInfo(boostList, boostList[2], badge)
}.subscribe { info ->
store.update {
it.copy(
boosts = info.boosts,
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
boostBadge = it.boostBadge ?: info.boostBadge,
stage = if (it.stage == BoostState.Stage.INIT) BoostState.Stage.READY else it.stage
)
}.subscribeBy(
onNext = { info ->
store.update {
it.copy(
boosts = info.boosts,
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
boostBadge = it.boostBadge ?: info.boostBadge,
stage = if (it.stage == BoostState.Stage.INIT || it.stage == BoostState.Stage.FAILURE) BoostState.Stage.READY else it.stage
)
}
},
onError = { throwable ->
Log.w(TAG, "Could not load boost information", throwable)
store.update {
it.copy(stage = BoostState.Stage.FAILURE)
}
}
}
)
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
@@ -170,4 +180,8 @@ class BoostViewModel(
return modelClass.cast(BoostViewModel(boostRepository, donationPaymentRepository, fetchTokenRequestCode))!!
}
}
companion object {
private val TAG = Log.tag(BoostViewModel::class.java)
}
}

View File

@@ -10,6 +10,7 @@ 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.logging.Log
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -66,9 +67,14 @@ class ManageDonationsViewModel(
}
)
disposables += subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency()).subscribeBy { subs ->
store.update { it.copy(availableSubscriptions = subs) }
}
disposables += subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency()).subscribeBy(
onSuccess = { subs ->
store.update { it.copy(availableSubscriptions = subs) }
},
onError = {
Log.w(TAG, "Error retrieving subscriptions data", it)
}
)
}
class Factory(
@@ -78,4 +84,8 @@ class ManageDonationsViewModel(
return modelClass.cast(ManageDonationsViewModel(subscriptionsRepository))!!
}
}
companion object {
private val TAG = Log.tag(ManageDonationsViewModel::class.java)
}
}

View File

@@ -39,9 +39,10 @@ data class CurrencySelection(
override fun bind(model: Model) {
spinner.text = model.currencySelection.selectedCurrencyCode
if (model.isEnabled) {
itemView.setOnClickListener { model.onClick() }
}
itemView.setOnClickListener { model.onClick() }
itemView.isEnabled = model.isEnabled
itemView.isClickable = model.isEnabled
}
}
}

View File

@@ -60,6 +60,8 @@ class SubscribeFragment : DSLSettingsFragment(
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
viewModel.refresh()
BadgePreview.register(adapter)
CurrencySelection.register(adapter)
Subscription.register(adapter)

View File

@@ -18,6 +18,7 @@ data class SubscribeState(
READY,
TOKEN_REQUEST,
PAYMENT_PIPELINE,
CANCELLING
CANCELLING,
FAILURE
}
}

View File

@@ -12,6 +12,7 @@ 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.logging.Log
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
@@ -43,7 +44,9 @@ class SubscribeViewModel(
disposables.clear()
}
init {
fun refresh() {
disposables.clear()
val currency: Observable<Currency> = SignalStore.donationsValues().observableSubscriptionCurrency
val allSubscriptions: Observable<List<Subscription>> = currency.switchMapSingle { subscriptionsRepository.getSubscriptions(it) }
refreshActiveSubscription()
@@ -56,16 +59,19 @@ class SubscribeViewModel(
}
}
disposables += Observable.combineLatest(allSubscriptions, activeSubscriptionSubject, ::Pair).subscribe { (subs, active) ->
store.update {
it.copy(
subscriptions = subs,
selectedSubscription = it.selectedSubscription ?: resolveSelectedSubscription(active, subs),
activeSubscription = active,
stage = if (it.stage == SubscribeState.Stage.INIT) SubscribeState.Stage.READY else it.stage,
)
}
}
disposables += Observable.combineLatest(allSubscriptions, activeSubscriptionSubject, ::Pair).subscribeBy(
onNext = { (subs, active) ->
store.update {
it.copy(
subscriptions = subs,
selectedSubscription = it.selectedSubscription ?: resolveSelectedSubscription(active, subs),
activeSubscription = active,
stage = if (it.stage == SubscribeState.Stage.INIT || it.stage == SubscribeState.Stage.FAILURE) SubscribeState.Stage.READY else it.stage,
)
}
},
onError = this::handleSubscriptionDataLoadFailure
)
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
@@ -77,10 +83,20 @@ class SubscribeViewModel(
}
}
private fun handleSubscriptionDataLoadFailure(throwable: Throwable) {
Log.w(TAG, "Could not load subscription data", throwable)
store.update {
it.copy(stage = SubscribeState.Stage.FAILURE)
}
}
fun refreshActiveSubscription() {
subscriptionsRepository
.getActiveSubscription()
.subscribeBy { activeSubscriptionSubject.onNext(it) }
.subscribeBy(
onSuccess = { activeSubscriptionSubject.onNext(it) },
onError = { activeSubscriptionSubject.onNext(ActiveSubscription(null)) }
)
}
private fun resolveSelectedSubscription(activeSubscription: ActiveSubscription, subscriptions: List<Subscription>): Subscription? {
@@ -97,6 +113,7 @@ class SubscribeViewModel(
onComplete = {
eventPublisher.onNext(DonationEvent.SubscriptionCancelled)
SignalStore.donationsValues().setLastEndOfPeriod(0L)
SignalStore.donationsValues().markUserManuallyCancelled()
refreshActiveSubscription()
store.update { it.copy(stage = SubscribeState.Stage.READY) }
},
@@ -196,4 +213,8 @@ class SubscribeViewModel(
return modelClass.cast(SubscribeViewModel(subscriptionsRepository, donationPaymentRepository, fetchTokenRequestCode))!!
}
}
companion object {
private val TAG = Log.tag(SubscribeViewModel::class.java)
}
}

View File

@@ -14,6 +14,8 @@ import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieDrawable
import com.google.android.material.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
import org.thoughtcrime.securesms.badges.BadgeImageView
@@ -131,9 +133,17 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
val args = ThanksForYourSupportBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
if (controlState == ControlState.DISPLAY) {
badgeRepository.setVisibilityForAllBadges(controlChecked).subscribe()
badgeRepository.setVisibilityForAllBadges(controlChecked).subscribeBy(
onError = {
Log.w(TAG, "Failure while updating badge visibility", it)
}
)
} else if (controlChecked) {
badgeRepository.setFeaturedBadge(args.badge).subscribe()
badgeRepository.setFeaturedBadge(args.badge).subscribeBy(
onError = {
Log.w(TAG, "Failure while updating featured badge", it)
}
)
}
if (args.isBoost) {
@@ -151,4 +161,8 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
private fun presentSubscriptionCopy() {
heading.setText(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support)
}
companion object {
private val TAG = Log.tag(ThanksForYourSupportBottomSheetDialogFragment::class.java)
}
}