mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Implement checks for badge redemption progress for subscriptions.
This commit is contained in:
@@ -241,8 +241,8 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
|
||||
Log.w(TAG, "Timed out while redeeming token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__redemption_still_pending)
|
||||
.setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away)
|
||||
.setTitle(R.string.DonationsErrors__still_processing)
|
||||
.setMessage(R.string.DonationsErrors__your_payment_is_still)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -12,6 +16,8 @@ import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -23,7 +29,9 @@ object ActiveSubscriptionPreference {
|
||||
class Model(
|
||||
val subscription: Subscription,
|
||||
val onAddBoostClick: () -> Unit,
|
||||
val renewalTimestamp: Long = -1L
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
val onContactSupport: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return subscription.id == newItem.subscription.id
|
||||
@@ -32,7 +40,8 @@ object ActiveSubscriptionPreference {
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
subscription == newItem.subscription &&
|
||||
renewalTimestamp == newItem.renewalTimestamp
|
||||
renewalTimestamp == newItem.renewalTimestamp &&
|
||||
redemptionState == newItem.redemptionState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +52,7 @@ object ActiveSubscriptionPreference {
|
||||
val price: TextView = itemView.findViewById(R.id.my_support_price)
|
||||
val expiry: TextView = itemView.findViewById(R.id.my_support_expiry)
|
||||
val boost: MaterialButton = itemView.findViewById(R.id.my_support_boost)
|
||||
val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadge(model.subscription.badge)
|
||||
@@ -56,7 +66,20 @@ object ActiveSubscriptionPreference {
|
||||
FiatMoneyUtil.formatOptions()
|
||||
)
|
||||
)
|
||||
expiry.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
when (model.redemptionState) {
|
||||
ManageDonationsState.SubscriptionRedemptionState.NONE -> presentRenewalState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS -> presentInProgressState()
|
||||
ManageDonationsState.SubscriptionRedemptionState.FAILED -> presentFailureState(model)
|
||||
}
|
||||
|
||||
boost.setOnClickListener {
|
||||
model.onAddBoostClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRenewalState(model: Model) {
|
||||
expiry.text = context.getString(
|
||||
R.string.MySupportPreference__renews_s,
|
||||
DateUtils.formatDateWithYear(
|
||||
@@ -64,10 +87,27 @@ object ActiveSubscriptionPreference {
|
||||
model.renewalTimestamp
|
||||
)
|
||||
)
|
||||
badge.alpha = 1f
|
||||
progress.visible = false
|
||||
}
|
||||
|
||||
boost.setOnClickListener {
|
||||
model.onAddBoostClick()
|
||||
}
|
||||
private fun presentInProgressState() {
|
||||
expiry.text = context.getString(R.string.MySupportPreference__processing_transaction)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = true
|
||||
}
|
||||
|
||||
private fun presentFailureState(model: Model) {
|
||||
expiry.text = SpannableStringBuilder(context.getString(R.string.MySupportPreference__couldnt_add_badge))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.clickable(
|
||||
context.getString(R.string.MySupportPreference__please_contact_support),
|
||||
ContextCompat.getColor(context, R.color.signal_accent_primary)
|
||||
) { model.onContactSupport() }
|
||||
)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -114,7 +116,12 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
onAddBoostClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToBoosts())
|
||||
},
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.activeSubscription.endOfCurrentPeriod)
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = state.subscriptionRedemptionState,
|
||||
onContactSupport = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -132,6 +139,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__manage_subscription),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_person_white_24dp),
|
||||
isEnabled = state.subscriptionRedemptionState != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
|
||||
}
|
||||
|
||||
@@ -7,11 +7,18 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
data class ManageDonationsState(
|
||||
val featuredBadge: Badge? = null,
|
||||
val transactionState: TransactionState = TransactionState.Init,
|
||||
val availableSubscriptions: List<Subscription> = emptyList()
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
) {
|
||||
sealed class TransactionState {
|
||||
object Init : TransactionState()
|
||||
object InTransaction : TransactionState()
|
||||
class NotInTransaction(val activeSubscription: ActiveSubscription) : TransactionState()
|
||||
}
|
||||
|
||||
enum class SubscriptionRedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
@@ -44,6 +45,22 @@ class ManageDonationsViewModel(
|
||||
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
|
||||
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription()
|
||||
|
||||
disposables += SubscriptionRedemptionJobWatcher.watch().subscribeBy { jobStateOptional ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
subscriptionRedemptionState = jobStateOptional.transform { jobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.RUNNING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.SUCCESS -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
JobTracker.JobState.FAILURE -> ManageDonationsState.SubscriptionRedemptionState.FAILED
|
||||
JobTracker.JobState.IGNORED -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
}
|
||||
}.or(ManageDonationsState.SubscriptionRedemptionState.NONE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += levelUpdateOperationEdges.flatMapSingle { isProcessing ->
|
||||
if (isProcessing) {
|
||||
Single.just(ManageDonationsState.TransactionState.InTransaction)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Allows observer to poll for the status of the latest pending, running, or completed redemption job for subscriptions.
|
||||
*/
|
||||
object SubscriptionRedemptionJobWatcher {
|
||||
fun watch(): Observable<Optional<JobTracker.JobState>> = Observable.interval(0, 5, TimeUnit.SECONDS).map {
|
||||
val redemptionJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == DonationReceiptRedemptionJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val receiptJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == SubscriptionReceiptRequestResponseJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val jobState: JobTracker.JobState? = redemptionJobState ?: receiptJobState
|
||||
|
||||
if (jobState == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) {
|
||||
Optional.of(JobTracker.JobState.FAILURE)
|
||||
} else {
|
||||
Optional.fromNullable(jobState)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@@ -275,12 +275,12 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
|
||||
Log.w(TAG, "Timeout occurred while redeeming token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__redemption_still_pending)
|
||||
.setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away)
|
||||
.setTitle(R.string.DonationsErrors__still_processing)
|
||||
.setMessage(R.string.DonationsErrors__your_payment)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
requireActivity().startActivity(AppSettingsActivity.manageSubscriptions(requireContext()))
|
||||
}
|
||||
.show()
|
||||
} else if (throwable is DonationExceptions.SetupFailed) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.whispersystems.signalservice.internal.EmptyResponse;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ import java.util.concurrent.TimeUnit;
|
||||
public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
private static final String TAG = Log.tag(DonationReceiptRedemptionJob.class);
|
||||
|
||||
public static final String SUBSCRIPTION_QUEUE = "ReceiptRedemption";
|
||||
public static final String KEY = "DonationReceiptRedemptionJob";
|
||||
public static final String INPUT_RECEIPT_CREDENTIAL_PRESENTATION = "data.receipt.credential.presentation";
|
||||
|
||||
@@ -31,7 +33,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("ReceiptRedemption")
|
||||
.setQueue(SUBSCRIPTION_QUEUE)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setMaxInstancesForQueue(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(7))
|
||||
@@ -66,6 +68,9 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
@Override
|
||||
public void onFailure() {
|
||||
SubscriptionNotification.RedemptionFailed.INSTANCE.show(context);
|
||||
if (isForSubscription()) {
|
||||
SignalStore.donationsValues().markSubscriptionRedemptionFailed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,6 +108,14 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
Log.w(TAG, "Encountered a retryable exception", response.getExecutionError().get(), true);
|
||||
throw new RetryableException();
|
||||
}
|
||||
|
||||
if (isForSubscription()) {
|
||||
SignalStore.donationsValues().clearSubscriptionRedemptionFailed();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isForSubscription() {
|
||||
return Objects.equals(getParameters().getQueue(), SUBSCRIPTION_QUEUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
@@ -103,6 +104,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
@Override
|
||||
public void onFailure() {
|
||||
SubscriptionNotification.VerificationFailed.INSTANCE.show(context);
|
||||
SignalStore.donationsValues().markSubscriptionRedemptionFailed();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,6 +142,10 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
|
||||
if (response.getApplicationError().isPresent()) {
|
||||
handleApplicationError(response);
|
||||
|
||||
if (response.getStatus() == 204) {
|
||||
SignalStore.donationsValues().clearSubscriptionRedemptionFailed();
|
||||
}
|
||||
} else if (response.getResult().isPresent()) {
|
||||
ReceiptCredential receiptCredential = getReceiptCredential(response.getResult().get());
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
private const val KEY_LEVEL_OPERATION_PREFIX = "donation.level.operation."
|
||||
private const val KEY_LEVEL_HISTORY = "donation.level.history"
|
||||
private const val DISPLAY_BADGES_ON_PROFILE = "donation.display.badges.on.profile"
|
||||
private const val SUBSCRIPTION_REDEMPTION_FAILED = "donation.subscription.redemption.failed"
|
||||
}
|
||||
|
||||
override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -197,4 +198,16 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
fun getDisplayBadgesOnProfile(): Boolean {
|
||||
return getBoolean(DISPLAY_BADGES_ON_PROFILE, false)
|
||||
}
|
||||
|
||||
fun getSubscriptionRedemptionFailed(): Boolean {
|
||||
return getBoolean(SUBSCRIPTION_REDEMPTION_FAILED, false)
|
||||
}
|
||||
|
||||
fun markSubscriptionRedemptionFailed() {
|
||||
putBoolean(SUBSCRIPTION_REDEMPTION_FAILED, true)
|
||||
}
|
||||
|
||||
fun clearSubscriptionRedemptionFailed() {
|
||||
putBoolean(SUBSCRIPTION_REDEMPTION_FAILED, false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user