Implement checks for badge redemption progress for subscriptions.
This commit is contained in:
parent
16ae2c870f
commit
b0f43535c6
12 changed files with 166 additions and 16 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,16 @@
|
|||
app:layout_constraintTop_toTopOf="@id/my_support_title"
|
||||
tools:src="@drawable/test_gradient" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/my_support_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/my_support_badge"
|
||||
app:layout_constraintEnd_toEndOf="@id/my_support_badge"
|
||||
app:layout_constraintStart_toStartOf="@id/my_support_badge"
|
||||
app:layout_constraintTop_toTopOf="@id/my_support_badge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/my_support_title"
|
||||
android:layout_width="0dp"
|
||||
|
@ -75,10 +85,10 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/my_support_boost"
|
||||
android:background="@drawable/my_boost_gradient"
|
||||
style="@style/Signal.Widget.Button.Large.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/my_boost_gradient"
|
||||
android:text="@string/MySupportPreference__add_a_signal_boost"
|
||||
app:cornerRadius="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/my_support_heading_barrier" />
|
||||
|
|
|
@ -3986,6 +3986,9 @@
|
|||
<string name="MySupportPreference__add_a_signal_boost">Add a Signal Boost</string>
|
||||
<string name="MySupportPreference__s_per_month">%1$s/month</string>
|
||||
<string name="MySupportPreference__renews_s">Renews %1$s</string>
|
||||
<string name="MySupportPreference__processing_transaction">Processing transaction…</string>
|
||||
<string name="MySupportPreference__couldnt_add_badge">Couldn\'t add badge.</string>
|
||||
<string name="MySupportPreference__please_contact_support">Please contact support.</string>
|
||||
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired">Your Badge has Expired</string>
|
||||
<string name="ExpiredBadgeBottomSheetDialogFragment__badge_expired">Badge expired</string>
|
||||
|
@ -4007,10 +4010,10 @@
|
|||
<string name="SubscribeFragment__processing_payment">Processing payment…</string>
|
||||
<string name="DonationsErrors__payment_failed">Payment failed</string>
|
||||
<string name="DonationsErrors__your_payment">Your payment couldn\'t be processed and you have not been charged. Please try again.</string>
|
||||
<string name="DonationsErrors__redemption_still_pending">Redemption still pending</string>
|
||||
<string name="DonationsErrors__still_processing">Still processing</string>
|
||||
<string name="DonationsErrors__redemption_failed">Redemption failed</string>
|
||||
<string name="DonationsErrors__please_contact_support">Please contact support</string>
|
||||
<string name="DonationsErrors__you_might_not_see_your_badge_right_away">You may not see your badge right away, but we\'re working on it!</string>
|
||||
<string name="DonationsErrors__your_payment_is_still">Your payment is still being processed. This can take a few minutes depending on your connection.</string>
|
||||
<string name="DonationsErrors__google_pay_unavailable">Google Pay Unavailable</string>
|
||||
<string name="DonationsErrors__you_have_to_set_up_google_pay_to_donate_in_app">You have to set up Google Pay to donate in-app.</string>
|
||||
<string name="DonationsErrors__failed_to_cancel_subscription">Failed to cancel subscription</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue