Implement Stripe Failure Code support.

This commit is contained in:
Alex Hart
2023-10-18 09:16:50 -04:00
committed by Cody Henthorne
parent 9da5f47623
commit 280da481ee
14 changed files with 219 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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