mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 21:24:42 +00:00
Implement Stripe Failure Code support.
This commit is contained in:
committed by
Cody Henthorne
parent
9da5f47623
commit
280da481ee
@@ -4,6 +4,7 @@ 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.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
||||
@@ -38,6 +39,7 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
val badge: Badge = args.badge
|
||||
val cancellationReason = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
|
||||
val declineCode: StripeDeclineCode? = args.chargeFailure?.let { StripeDeclineCode.getFromCode(it) }
|
||||
val failureCode: StripeFailureCode? = args.chargeFailure?.let { StripeFailureCode.getFromCode(it) }
|
||||
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
|
||||
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
|
||||
|
||||
@@ -69,6 +71,12 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
getString(declineCode.mapToErrorStringResource()),
|
||||
badge.name
|
||||
)
|
||||
} else if (failureCode != null) {
|
||||
getString(
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_canceled_s,
|
||||
getString(failureCode.mapToErrorStringResource()),
|
||||
badge.name
|
||||
)
|
||||
} else if (inactive) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_recurring_monthly_donation_was_automatically, badge.name)
|
||||
} else {
|
||||
|
||||
@@ -284,20 +284,30 @@ class DonationCheckoutDelegate(
|
||||
fragment!!.requireContext(),
|
||||
throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
var tryCCAgain = false
|
||||
var tryAgain = false
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryCCAgain = true
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogDismissed() {
|
||||
errorDialog = null
|
||||
if (!tryCCAgain) {
|
||||
if (!tryAgain) {
|
||||
tryAgain = false
|
||||
fragment!!.findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeError
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
|
||||
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
|
||||
@@ -64,6 +65,11 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
*/
|
||||
class StripeDeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: PaymentSourceType.Stripe) : PaymentSetupError(source, cause)
|
||||
|
||||
/**
|
||||
* Bank Transfer failed, with a specific reason told to us by Stripe
|
||||
*/
|
||||
class StripeFailureCodeError(source: DonationErrorSource, cause: Throwable, val failureCode: StripeFailureCode, val method: PaymentSourceType.Stripe) : PaymentSetupError(source, cause)
|
||||
|
||||
/**
|
||||
* Payment setup failed in some way, which we are told about by PayPal.
|
||||
*/
|
||||
@@ -186,16 +192,16 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
@JvmStatic
|
||||
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: PaymentSourceType): DonationError {
|
||||
return when (throwable) {
|
||||
is StripeError.PostError -> {
|
||||
val declineCode: StripeDeclineCode? = throwable.declineCode
|
||||
is StripeError.PostError.Generic -> {
|
||||
val errorCode: String? = throwable.errorCode
|
||||
|
||||
when {
|
||||
declineCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeDeclinedError(source, throwable, declineCode, method)
|
||||
errorCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeCodedError(source, throwable, errorCode)
|
||||
else -> PaymentSetupError.GenericError(source, throwable)
|
||||
if (errorCode != null) {
|
||||
PaymentSetupError.StripeCodedError(source, throwable, errorCode)
|
||||
} else {
|
||||
PaymentSetupError.GenericError(source, throwable)
|
||||
}
|
||||
}
|
||||
is StripeError.PostError.Declined -> PaymentSetupError.StripeDeclinedError(source, throwable, throwable.declineCode, method as PaymentSourceType.Stripe)
|
||||
is StripeError.PostError.Failed -> PaymentSetupError.StripeFailureCodeError(source, throwable, throwable.failureCode, method as PaymentSourceType.Stripe)
|
||||
|
||||
is UserCancelledPaymentError -> {
|
||||
return throwable
|
||||
|
||||
@@ -68,6 +68,13 @@ object DonationErrorDialogs {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction<Unit>? {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLearnMore(context: Context): DonationErrorParams.ErrorAction<Unit>? {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__learn_more,
|
||||
|
||||
@@ -64,6 +64,7 @@ object DonationErrorNotifications {
|
||||
}
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<PendingIntent>? = null
|
||||
override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction<PendingIntent>? = null
|
||||
|
||||
override fun onGoToGooglePay(context: Context): DonationErrorParams.ErrorAction<PendingIntent> {
|
||||
return createAction(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class DonationErrorParams<V> private constructor(
|
||||
@@ -26,15 +27,10 @@ class DonationErrorParams<V> private constructor(
|
||||
return when (throwable) {
|
||||
is DonationError.GiftRecipientVerificationError -> getVerificationErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.StripeDeclinedError -> getStripeDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.StripeFailureCodeError -> getStripeFailureCodeErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.PayPalDeclinedError -> getPayPalDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
message = R.string.DonationsErrors__your_payment,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
is DonationError.PaymentSetupError -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
|
||||
// TODO [sepa] -- This is only used for the notification, and will be rare, but we should probably have better copy here.
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
@@ -110,6 +106,10 @@ class DonationErrorParams<V> private constructor(
|
||||
}
|
||||
|
||||
private fun <V> getStripeDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
if (!declinedError.method.hasDeclineCodeSupport()) {
|
||||
return getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
|
||||
fun unexpectedDeclinedError(declinedError: DonationError.PaymentSetupError.StripeDeclinedError): Nothing {
|
||||
error("Unexpected declined error: ${declinedError.declineCode} during ${declinedError.method} processing.")
|
||||
}
|
||||
@@ -224,6 +224,43 @@ class DonationErrorParams<V> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getStripeFailureCodeErrorParams(context: Context, failureCodeError: DonationError.PaymentSetupError.StripeFailureCodeError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
if (!failureCodeError.method.hasFailureCodeSupport()) {
|
||||
return getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
|
||||
return when (failureCodeError.failureCode) {
|
||||
is StripeFailureCode.Known -> {
|
||||
val errorText = failureCodeError.failureCode.mapToErrorStringResource()
|
||||
when (failureCodeError.failureCode.code) {
|
||||
StripeFailureCode.Code.REFER_TO_CUSTOMER -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.DEBIT_DISPUTED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.AUTHORIZATION_REVOKED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.DEBIT_NOT_AUTHORIZED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.ACCOUNT_CLOSED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.BANK_ACCOUNT_RESTRICTED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.DEBIT_AUTHORIZATION_NOT_MATCH -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.RECIPIENT_DECEASED -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.BRANCH_DOES_NOT_EXIST -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.INCORRECT_ACCOUNT_HOLDER_NAME -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.INVALID_ACCOUNT_NUMBER -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.GENERIC_COULD_NOT_PROCESS -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
}
|
||||
}
|
||||
is StripeFailureCode.Unknown -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getGenericPaymentSetupErrorParams(context: Context, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
message = R.string.DonationsErrors__your_payment,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun <V> getLearnMoreParams(context: Context, callback: Callback<V>, message: Int): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
@@ -250,6 +287,15 @@ class DonationErrorParams<V> private constructor(
|
||||
negativeAction = callback.onCancel(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun <V> getTryBankTransferAgainParams(context: Context, callback: Callback<V>, message: Int): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
message = message,
|
||||
positiveAction = callback.onTryBankTransferAgain(context),
|
||||
negativeAction = callback.onCancel(context)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<V> {
|
||||
@@ -259,5 +305,6 @@ class DonationErrorParams<V> private constructor(
|
||||
fun onContactSupport(context: Context): ErrorAction<V>?
|
||||
fun onGoToGooglePay(context: Context): ErrorAction<V>?
|
||||
fun onTryCreditCardAgain(context: Context): ErrorAction<V>?
|
||||
fun onTryBankTransferAgain(context: Context): ErrorAction<V>?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,31 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@StringRes
|
||||
fun StripeFailureCode.mapToErrorStringResource(): Int {
|
||||
return when (this) {
|
||||
is StripeFailureCode.Known -> when (this.code) {
|
||||
StripeFailureCode.Code.REFER_TO_CUSTOMER -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
StripeFailureCode.Code.INSUFFICIENT_FUNDS -> R.string.StripeFailureCode__the_bank_account_provided
|
||||
StripeFailureCode.Code.DEBIT_DISPUTED -> R.string.StripeFailureCode__verify_your_bank_details_are_correct // TODO [sepa] -- verify
|
||||
StripeFailureCode.Code.AUTHORIZATION_REVOKED -> R.string.StripeFailureCode__this_payment_was_revoked
|
||||
StripeFailureCode.Code.DEBIT_NOT_AUTHORIZED -> R.string.StripeFailureCode__this_payment_was_revoked
|
||||
StripeFailureCode.Code.ACCOUNT_CLOSED -> R.string.StripeFailureCode__the_bank_details_provided_could_not_be_processed
|
||||
StripeFailureCode.Code.BANK_ACCOUNT_RESTRICTED -> R.string.StripeFailureCode__the_bank_details_provided_could_not_be_processed
|
||||
StripeFailureCode.Code.DEBIT_AUTHORIZATION_NOT_MATCH -> R.string.StripeFailureCode__an_error_occurred_while_processing_this_payment
|
||||
StripeFailureCode.Code.RECIPIENT_DECEASED -> R.string.StripeFailureCode__the_bank_details_provided_could_not_be_processed
|
||||
StripeFailureCode.Code.BRANCH_DOES_NOT_EXIST -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
StripeFailureCode.Code.INCORRECT_ACCOUNT_HOLDER_NAME -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
StripeFailureCode.Code.INVALID_ACCOUNT_NUMBER -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
StripeFailureCode.Code.GENERIC_COULD_NOT_PROCESS -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
}
|
||||
is StripeFailureCode.Unknown -> R.string.StripeFailureCode__verify_your_bank_details_are_correct
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
fun StripeDeclineCode.mapToErrorStringResource(): Int {
|
||||
return when (this) {
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
||||
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
@@ -18,8 +19,11 @@ fun DonationProcessorError.toDonationError(
|
||||
ActiveSubscription.Processor.STRIPE -> {
|
||||
check(method is PaymentSourceType.Stripe)
|
||||
val declineCode = StripeDeclineCode.getFromCode(chargeFailure.code)
|
||||
val failureCode = StripeFailureCode.getFromCode(chargeFailure.code)
|
||||
if (declineCode.isKnown()) {
|
||||
DonationError.PaymentSetupError.StripeDeclinedError(source, this, declineCode, method)
|
||||
} else if (failureCode.isKnown) {
|
||||
DonationError.PaymentSetupError.StripeFailureCodeError(source, this, failureCode, method)
|
||||
} else if (chargeFailure.code != null) {
|
||||
DonationError.PaymentSetupError.StripeCodedError(source, this, chargeFailure.code)
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.donations.PaymentSourceType;
|
||||
import org.signal.donations.StripeDeclineCode;
|
||||
import org.signal.donations.StripeFailureCode;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
@@ -308,6 +309,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
Log.d(TAG, "Stripe charge failure detected: " + chargeFailure, true);
|
||||
|
||||
StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason());
|
||||
StripeFailureCode failureCode = StripeFailureCode.Companion.getFromCode(chargeFailure.getCode());
|
||||
DonationError.PaymentSetupError paymentSetupError;
|
||||
PaymentSourceType paymentSourceType = SignalStore.donationsValues().getSubscriptionPaymentSourceType();
|
||||
boolean isStripeSource = paymentSourceType instanceof PaymentSourceType.Stripe;
|
||||
@@ -319,6 +321,13 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||
declineCode,
|
||||
(PaymentSourceType.Stripe) paymentSourceType
|
||||
);
|
||||
} else if (failureCode.isKnown() && isStripeSource) {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.StripeFailureCodeError(
|
||||
getErrorSource(),
|
||||
new Exception(chargeFailure.getMessage()),
|
||||
failureCode,
|
||||
(PaymentSourceType.Stripe) paymentSourceType
|
||||
);
|
||||
} else if (isStripeSource) {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.StripeCodedError(
|
||||
getErrorSource(),
|
||||
|
||||
Reference in New Issue
Block a user