Add decline code messages into expiration sheet.

fork-5.53.8
Alex Hart 2022-05-20 13:05:33 -03:00
rodzic 4d8faffb75
commit 6dec6cef27
14 zmienionych plików z 405 dodań i 30 usunięć

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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()
}
)
}
}
}

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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"
)
}
)
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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"),

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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());
}
}
}

Wyświetl plik

@ -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)
));
}
}

Wyświetl plik

@ -542,7 +542,16 @@
<fragment
android:id="@+id/internalSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.internal.InternalSettingsFragment"
android:label="internal_settings_fragment" />
android:label="internal_settings_fragment" >
<action
android:id="@+id/action_internalSettingsFragment_to_donorErrorConfigurationFragment"
app:destination="@id/donorErrorConfigurationFragment" />
</fragment>
<fragment
android:id="@+id/donorErrorConfigurationFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.internal.donor.DonorErrorConfigurationFragment"
android:label="donor_error_configuration_fragment" />
<!-- endregion -->

Wyświetl plik

@ -41,5 +41,10 @@
app:argType="string"
app:nullable="true" />
<argument
android:name="charge_failure"
app:argType="string"
app:nullable="true" />
</dialog>
</navigation>

Wyświetl plik

@ -2739,6 +2739,7 @@
<string name="preferences__internal_badges" translatable="false">Badges</string>
<string name="preferences__internal_badges_enqueue_redemption" translatable="false">Enqueue redemption.</string>
<string name="preferences__internal_badges_enqueue_keep_alive" translatable="false">Enqueue keep-alive.</string>
<string name="preferences__internal_badges_set_error_state" translatable="false">Set error state.</string>
<string name="preferences__internal_release_channel" translatable="false">Release channel</string>
<string name="preferences__internal_fetch_release_channel" translatable="false">Fetch release channel</string>
<string name="preferences__internal_release_channel_set_last_version" translatable="false">Set last version seen back 10 versions</string>
@ -2751,6 +2752,11 @@
<string name="preferences__internal_clear_all_service_ids_description" translatable="false">Clears all known service IDs (except your own). Do not use on your personal device!</string>
<string name="preferences__internal_clear_all_profile_keys" translatable="false">Clear all profile keys</string>
<string name="preferences__internal_clear_all_profile_keys_description" translatable="false">Clears all known profile keys (except your own). Do not use on your personal device!</string>
<string name="preferences__internal_donor_error_expired_badge" translatable="false">Expired Badge</string>
<string name="preferences__internal_donor_error_charge_failure" translatable="false">Charge Failure</string>
<string name="preferences__internal_donor_error_cancelation_reason" translatable="false">Cancelation Reason</string>
<string name="preferences__internal_donor_error_save_and_finish" translatable="false">Save and Finish</string>
<string name="preferences__internal_donor_error_clear" translatable="false">Clear</string>
<!-- Payments -->
@ -4226,8 +4232,12 @@
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically">Your recurring monthly donation was automatically cancelled because you were inactive for too long. Your %1$s badge is no longer visible on your profile.</string>
<!-- Copy displayed when badge expires after payment failure -->
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled">Your recurring monthly donation was cancelled because we couldn\'t process your payment. Your badge is no longer visible on your profile.</string>
<!-- Copy displayed when badge expires after a payment failure and we have a displayable charge failure reason -->
<string name="ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s">Your recurring monthly donation was cancelled. %1$s Your %2$s badge is no longer visible on your profile.</string>
<string name="ExpiredBadgeBottomSheetDialogFragment__you_can">You can keep using Signal but to support the app and reactivate your badge, renew now.</string>
<string name="ExpiredBadgeBottomSheetDialogFragment__renew_subscription">Renew subscription</string>
<!-- Button label to send user to Google Pay website -->
<string name="ExpiredBadgeBottomSheetDialogFragment__go_to_google_pay">Go to Google Pay</string>
<string name="CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment">Can\'t process subscription payment</string>
<string name="CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble">We\'re having trouble collecting your Signal Sustainer payment. Make sure your payment method is up to date. If it isn\'t, update it in Google Pay. Signal will try to process the payment again in a few days.</string>