Add decline code messages into expiration sheet.

This commit is contained in:
Alex Hart
2022-05-20 13:05:33 -03:00
parent 4d8faffb75
commit 6dec6cef27
14 changed files with 405 additions and 30 deletions

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.badges.self.expired
import androidx.fragment.app.FragmentManager
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
@@ -11,9 +13,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.mapToErrorStringResource
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.shouldRouteToGooglePay
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
/**
@@ -32,11 +37,12 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
val badge: Badge = args.badge
val cancellationReason = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
val chargeFailure: ActiveSubscription.ChargeFailure? = SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure()
val declineCode: StripeDeclineCode? = args.chargeFailure?.let { StripeDeclineCode.getFromCode(it) }
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
Log.d(TAG, "Displaying Expired Badge Fragment with bundle: ${requireArguments()}", true)
return configure {
customPref(ExpiredBadge.Model(badge))
@@ -57,6 +63,12 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
DSLSettingsText.from(
if (badge.isBoost()) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired_and)
} else if (declineCode != null) {
getString(
R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s,
getString(declineCode.mapToErrorStringResource()),
badge.name
)
} else if (inactive) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically, badge.name)
} else {
@@ -68,22 +80,33 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
space(DimensionUnit.DP.toPixels(16f).toInt())
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_keep
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
},
DSLSettingsText.CenterModifier
)
)
if (badge.isSubscription() && declineCode?.shouldRouteToGooglePay() == true) {
space(DimensionUnit.DP.toPixels(68f).toInt())
space(DimensionUnit.DP.toPixels(92f).toInt())
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__go_to_google_pay),
onClick = {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.google_pay_url))
}
)
} else {
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_keep
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(92f).toInt())
}
primaryButton(
text = DSLSettingsText.from(
@@ -117,9 +140,16 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
}
companion object {
private val TAG = Log.tag(ExpiredBadgeBottomSheetDialogFragment::class.java)
@JvmStatic
fun show(badge: Badge, cancellationReason: UnexpectedSubscriptionCancellation?, fragmentManager: FragmentManager) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status).build()
fun show(
badge: Badge,
cancellationReason: UnexpectedSubscriptionCancellation?,
chargeFailure: ActiveSubscription.ChargeFailure?,
fragmentManager: FragmentManager
) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status, chargeFailure?.code).build()
val fragment = ExpiredBadgeBottomSheetDialogFragment()
fragment.arguments = args.toBundle()

View File

@@ -38,7 +38,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
override fun bindAdapter(adapter: DSLSettingsAdapter) {
Badge.register(adapter) { badge, _, isFaded ->
if (badge.isExpired() || isFaded) {
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null))
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null, null))
} else {
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
}

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.DialogInterface
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.AppUtil
import org.signal.core.util.concurrent.SignalExecutors
@@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.payments.DataExportUtil
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Optional
import java.util.concurrent.TimeUnit
import kotlin.math.max
@@ -409,6 +411,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
enqueueSubscriptionKeepAlive()
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_badges_set_error_state),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToDonorErrorConfigurationFragment())
}
)
}
dividerPref()

View File

@@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.components.settings.app.internal.donor
import androidx.fragment.app.viewModels
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable
class DonorErrorConfigurationFragment : DSLSettingsFragment() {
private val viewModel: DonorErrorConfigurationViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
override fun bindAdapter(adapter: DSLSettingsAdapter) {
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: DonorErrorConfigurationState): DSLConfiguration {
return configure {
radioListPref(
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_expired_badge),
selected = state.badges.indexOf(state.selectedBadge),
listItems = state.badges.map { it.name }.toTypedArray(),
onSelected = { viewModel.setSelectedBadge(it) }
)
radioListPref(
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_cancelation_reason),
selected = UnexpectedSubscriptionCancellation.values().indexOf(state.selectedUnexpectedSubscriptionCancellation),
listItems = UnexpectedSubscriptionCancellation.values().map { it.status }.toTypedArray(),
onSelected = { viewModel.setSelectedUnexpectedSubscriptionCancellation(it) },
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
)
radioListPref(
title = DSLSettingsText.from(R.string.preferences__internal_donor_error_charge_failure),
selected = StripeDeclineCode.Code.values().indexOf(state.selectedStripeDeclineCode),
listItems = StripeDeclineCode.Code.values().map { it.code }.toTypedArray(),
onSelected = { viewModel.setStripeDeclineCode(it) },
isEnabled = state.selectedBadge == null || state.selectedBadge.isSubscription()
)
primaryButton(
text = DSLSettingsText.from(R.string.preferences__internal_donor_error_save_and_finish),
onClick = {
lifecycleDisposable += viewModel.save().subscribe { requireActivity().finish() }
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.preferences__internal_donor_error_clear),
onClick = {
lifecycleDisposable += viewModel.clear().subscribe()
}
)
}
}
}

View File

@@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.internal.donor
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
data class DonorErrorConfigurationState(
val badges: List<Badge> = emptyList(),
val selectedBadge: Badge? = null,
val selectedUnexpectedSubscriptionCancellation: UnexpectedSubscriptionCancellation? = null,
val selectedStripeDeclineCode: StripeDeclineCode.Code? = null
)

View File

@@ -0,0 +1,152 @@
package org.thoughtcrime.securesms.components.settings.app.internal.donor
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import java.util.Locale
class DonorErrorConfigurationViewModel : ViewModel() {
private val store = RxStore(DonorErrorConfigurationState())
private val disposables = CompositeDisposable()
val state: Flowable<DonorErrorConfigurationState> = store.stateFlowable
init {
val giftBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
.getGiftBadges(Locale.getDefault())
.flatMap { it.flattenResult() }
.map { results -> results.values.map { Badges.fromServiceBadge(it) } }
.subscribeOn(Schedulers.io())
val boostBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
.getBoostBadge(Locale.getDefault())
.flatMap { it.flattenResult() }
.map { listOf(Badges.fromServiceBadge(it)) }
.subscribeOn(Schedulers.io())
val subscriptionBadges: Single<List<Badge>> = ApplicationDependencies.getDonationsService()
.getSubscriptionLevels(Locale.getDefault())
.flatMap { it.flattenResult() }
.map { levels -> levels.levels.values.map { Badges.fromServiceBadge(it.badge) } }
.subscribeOn(Schedulers.io())
disposables += Single.zip(giftBadges, boostBadges, subscriptionBadges) { g, b, s ->
g + b + s
}.subscribe { badges ->
store.update { it.copy(badges = badges) }
}
}
override fun onCleared() {
disposables.clear()
}
fun setSelectedBadge(badgeIndex: Int) {
store.update {
it.copy(selectedBadge = if (badgeIndex in it.badges.indices) it.badges[badgeIndex] else null)
}
}
fun setSelectedUnexpectedSubscriptionCancellation(unexpectedSubscriptionCancellationIndex: Int) {
store.update {
it.copy(
selectedUnexpectedSubscriptionCancellation = if (unexpectedSubscriptionCancellationIndex in UnexpectedSubscriptionCancellation.values().indices) {
UnexpectedSubscriptionCancellation.values()[unexpectedSubscriptionCancellationIndex]
} else {
null
}
)
}
}
fun setStripeDeclineCode(stripeDeclineCodeIndex: Int) {
store.update {
it.copy(
selectedStripeDeclineCode = if (stripeDeclineCodeIndex in StripeDeclineCode.Code.values().indices) {
StripeDeclineCode.Code.values()[stripeDeclineCodeIndex]
} else {
null
}
)
}
}
fun save(): Completable {
val snapshot = store.state
val saveState = Completable.fromAction {
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
when {
snapshot.selectedBadge?.isGift() == true -> handleGiftExpiration(snapshot)
snapshot.selectedBadge?.isBoost() == true -> handleBoostExpiration(snapshot)
snapshot.selectedBadge?.isSubscription() == true -> handleSubscriptionExpiration(snapshot)
else -> handleSubscriptionPaymentFailure(snapshot)
}
}
}.subscribeOn(Schedulers.io())
return clear().andThen(saveState)
}
fun clear(): Completable {
return Completable.fromAction {
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
SignalStore.donationsValues().setExpiredBadge(null)
SignalStore.donationsValues().setExpiredGiftBadge(null)
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null
SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = 0L
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(null)
}
store.update {
it.copy(
selectedStripeDeclineCode = null,
selectedUnexpectedSubscriptionCancellation = null,
selectedBadge = null
)
}
}
}
private fun handleBoostExpiration(state: DonorErrorConfigurationState) {
SignalStore.donationsValues().setExpiredBadge(state.selectedBadge)
}
private fun handleGiftExpiration(state: DonorErrorConfigurationState) {
SignalStore.donationsValues().setExpiredGiftBadge(state.selectedBadge)
}
private fun handleSubscriptionExpiration(state: DonorErrorConfigurationState) {
SignalStore.donationsValues().setExpiredBadge(state.selectedBadge)
handleSubscriptionPaymentFailure(state)
}
private fun handleSubscriptionPaymentFailure(state: DonorErrorConfigurationState) {
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = state.selectedUnexpectedSubscriptionCancellation?.status
SignalStore.donationsValues().unexpectedSubscriptionCancelationTimestamp = System.currentTimeMillis()
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(
state.selectedStripeDeclineCode?.let {
ActiveSubscription.ChargeFailure(
it.code,
"Test Charge Failure",
"Test Network Status",
"Test Network Reason",
"Test"
)
}
)
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import androidx.annotation.StringRes
import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.R
@StringRes
fun StripeDeclineCode.mapToErrorStringResource(): Int {
return when (this) {
is StripeDeclineCode.Known -> when (this.code) {
StripeDeclineCode.Code.APPROVE_WITH_ID -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
StripeDeclineCode.Code.CALL_ISSUER -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase
StripeDeclineCode.Code.EXPIRED_CARD -> R.string.DeclineCode__your_card_has_expired
StripeDeclineCode.Code.INCORRECT_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect
StripeDeclineCode.Code.INCORRECT_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> R.string.DeclineCode__your_card_does_not_have_sufficient_funds
StripeDeclineCode.Code.INVALID_CVC -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> R.string.DeclineCode__the_expiration_month
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> R.string.DeclineCode__the_expiration_year
StripeDeclineCode.Code.INVALID_NUMBER -> R.string.DeclineCode__your_card_number_is_incorrect
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> R.string.DeclineCode__try_completing_the_payment_again
StripeDeclineCode.Code.PROCESSING_ERROR -> R.string.DeclineCode__try_again
StripeDeclineCode.Code.REENTER_TRANSACTION -> R.string.DeclineCode__try_again
else -> R.string.DeclineCode__try_another_payment_method_or_contact_your_bank
}
else -> R.string.DeclineCode__try_another_payment_method_or_contact_your_bank
}
}
fun StripeDeclineCode.shouldRouteToGooglePay(): Boolean {
return when (this) {
is StripeDeclineCode.Known -> when (this.code) {
StripeDeclineCode.Code.APPROVE_WITH_ID -> true
StripeDeclineCode.Code.CALL_ISSUER -> true
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> false
StripeDeclineCode.Code.EXPIRED_CARD -> true
StripeDeclineCode.Code.INCORRECT_NUMBER -> true
StripeDeclineCode.Code.INCORRECT_CVC -> true
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> false
StripeDeclineCode.Code.INVALID_CVC -> true
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> true
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> true
StripeDeclineCode.Code.INVALID_NUMBER -> true
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> false
StripeDeclineCode.Code.PROCESSING_ERROR -> false
StripeDeclineCode.Code.REENTER_TRANSACTION -> false
else -> false
}
else -> false
}
}

View File

@@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
/**
* Error states that can occur if we detect that a user's subscription has been cancelled and the manual
* cancellation flag is not set.
*
* This status is taken directly from the ActiveSubscription object, and is set in the Subscription's
* keep-alive and subscription receipt redemption jobs.
*/
enum class UnexpectedSubscriptionCancellation(val status: String) {
PAST_DUE("past_due"),

View File

@@ -68,7 +68,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
val expiredGiftBadge = SignalStore.donationsValues().getExpiredGiftBadge()
if (expiredGiftBadge != null) {
SignalStore.donationsValues().setExpiredBadge(null)
SignalStore.donationsValues().setExpiredGiftBadge(null)
ExpiredGiftSheet.show(childFragmentManager, expiredGiftBadge)
}

View File

@@ -396,7 +396,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
if (expiredBadge.isBoost() || !SignalStore.donationsValues().isUserManuallyCancelled()) {
Log.w(TAG, "Displaying bottom sheet for an expired badge", true);
ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, unexpectedSubscriptionCancellation, getParentFragmentManager());
ExpiredBadgeBottomSheetDialogFragment.show(
expiredBadge,
unexpectedSubscriptionCancellation,
SignalStore.donationsValues().getUnexpectedSubscriptionCancelationChargeFailure(),
getParentFragmentManager());
}
}
}

View File

@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.signal.core.util.logging.Log;
import org.signal.donations.StripeDeclineCode;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
@@ -152,9 +153,15 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
Log.w(TAG, "Subscription payment charge failure code: " + chargeFailure.getCode() + ", message: " + chargeFailure.getMessage(), true);
}
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true);
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod());
throw new Exception("Subscription has a payment failure: " + subscription.getStatus());
if (isForKeepAlive) {
Log.w(TAG, "Subscription payment failure in active subscription response (status = " + subscription.getStatus() + ").", true);
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), true);
throw new Exception("Active subscription hit a payment failure: " + subscription.getStatus());
} else {
Log.w(TAG, "New subscription has hit a payment failure. (status = " + subscription.getStatus() + ").", true);
onPaymentFailure(subscription.getStatus(), chargeFailure, subscription.getEndOfCurrentPeriod(), false);
throw new Exception("New subscription has hit a payment failure: " + subscription.getStatus());
}
} else if (!subscription.isActive()) {
ActiveSubscription.ChargeFailure chargeFailure = activeSubscription.getChargeFailure();
if (chargeFailure != null) {
@@ -269,15 +276,31 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
}
}
private void onPaymentFailure(@Nullable String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp) {
/**
* Handles state updates and error routing for a payment failure.
*
* There are two ways this could go, depending on whether the job was created for a keep-alive chain.
*
* 1. In the case of a normal chain (new subscription) We simply route the error out to the user. The payment failure would have occurred while trying to
* charge for the first month of their subscription, and are likely still on the "Subscribe" screen, so we can just display a dialog.
* 1. In the case of a keep-alive event, we want to book-keep the error to show the user on a subsequent launch, and we want to sync our failure state to
* linked devices.
*/
private void onPaymentFailure(@NonNull String status, @Nullable ActiveSubscription.ChargeFailure chargeFailure, long timestamp, boolean isForKeepAlive) {
SignalStore.donationsValues().setShouldCancelSubscriptionBeforeNextSubscribeAttempt(true);
if (status == null) {
DonationError.routeDonationError(context, DonationError.genericPaymentFailure(getErrorSource()));
} else {
if (isForKeepAlive){
Log.d(TAG, "Is for a keep-alive and we have a status. Setting UnexpectedSubscriptionCancelation state...", true);
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationChargeFailure(chargeFailure);
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationReason(status);
SignalStore.donationsValues().setUnexpectedSubscriptionCancelationTimestamp(timestamp);
MultiDeviceSubscriptionSyncRequestJob.enqueue();
} else {
Log.d(TAG, "Not for a keep-alive and we have a status. Routing a payment setup error...", true);
DonationError.routeDonationError(context, new DonationError.PaymentSetupError.DeclinedError(
getErrorSource(),
new Exception("Got a failure status from the subscription object."),
StripeDeclineCode.Companion.getFromCode(status)
));
}
}