diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt index b29ba136a5..48b0d4c29e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt index 2e54924666..5b642d67d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt @@ -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 { return DonationErrorParams.ErrorAction( label = R.string.DeclineCode__try, action = { - tryCCAgain = true + tryAgain = true + } + ) + } + + override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction { + 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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt index df1bf5018b..764e320dba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt index bbb8b30428..a57b49aa29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorDialogs.kt @@ -68,6 +68,13 @@ object DonationErrorDialogs { ) } + override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction? { + return DonationErrorParams.ErrorAction( + label = R.string.DeclineCode__try, + action = {} + ) + } + override fun onLearnMore(context: Context): DonationErrorParams.ErrorAction? { return DonationErrorParams.ErrorAction( label = R.string.DeclineCode__learn_more, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt index f4bc8ad609..5366f394e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorNotifications.kt @@ -64,6 +64,7 @@ object DonationErrorNotifications { } override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction? = null + override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction? = null override fun onGoToGooglePay(context: Context): DonationErrorParams.ErrorAction { return createAction( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt index 3e67ac3a87..0d21767290 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt @@ -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 private constructor( @@ -26,15 +27,10 @@ class DonationErrorParams 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 private constructor( } private fun getStripeDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback): DonationErrorParams { + 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 private constructor( } } + private fun getStripeFailureCodeErrorParams(context: Context, failureCodeError: DonationError.PaymentSetupError.StripeFailureCodeError, callback: Callback): DonationErrorParams { + 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 getGenericPaymentSetupErrorParams(context: Context, callback: Callback): DonationErrorParams { + return DonationErrorParams( + title = R.string.DonationsErrors__error_processing_payment, + message = R.string.DonationsErrors__your_payment, + positiveAction = callback.onOk(context), + negativeAction = null + ) + } + private fun getLearnMoreParams(context: Context, callback: Callback, message: Int): DonationErrorParams { return DonationErrorParams( title = R.string.DonationsErrors__error_processing_payment, @@ -250,6 +287,15 @@ class DonationErrorParams private constructor( negativeAction = callback.onCancel(context) ) } + + private fun getTryBankTransferAgainParams(context: Context, callback: Callback, message: Int): DonationErrorParams { + return DonationErrorParams( + title = R.string.DonationsErrors__error_processing_payment, + message = message, + positiveAction = callback.onTryBankTransferAgain(context), + negativeAction = callback.onCancel(context) + ) + } } interface Callback { @@ -259,5 +305,6 @@ class DonationErrorParams private constructor( fun onContactSupport(context: Context): ErrorAction? fun onGoToGooglePay(context: Context): ErrorAction? fun onTryCreditCardAgain(context: Context): ErrorAction? + fun onTryBankTransferAgain(context: Context): ErrorAction? } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt index a57526d773..42d71fd34c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrors.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationProcessorErrorExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationProcessorErrorExtensions.kt index 55b7406741..3a0ae18141 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationProcessorErrorExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationProcessorErrorExtensions.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java index 91756b0b66..c072cc9fba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -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(), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5d10ac05c..54a47af326 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4917,6 +4917,18 @@ Your card number is incorrect. Verify your card details are correct and try again. + + + The bank account provided has insufficient funds to complete this purchase, try again or contact your bank for more information. + + This payment was revoked by the account holder and could not be processed. You haven\'t been charged. + + An error occurred while processing this payment, please try again. + + The bank details provided could not be processed, contact your bank for more information. + + Verify your bank details are correct and try again. If the problem continues, contact your bank. + Name your profile diff --git a/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt b/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt index f64c52449c..7ec2a968c3 100644 --- a/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt +++ b/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt @@ -16,6 +16,9 @@ sealed class PaymentSourceType { object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD", false) object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD", false) object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT", true) + + fun hasDeclineCodeSupport(): Boolean = this !is SEPADebit + fun hasFailureCodeSupport(): Boolean = this is SEPADebit } private enum class Codes(val code: String) { diff --git a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt index c06c444777..fe06bae03c 100644 --- a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt +++ b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt @@ -305,12 +305,15 @@ class StripeApi( val body = response.body()?.string() val errorCode = parseErrorCode(body) val declineCode = parseDeclineCode(body) ?: StripeDeclineCode.getFromCode(errorCode) + val failureCode = parseFailureCode(body) ?: StripeFailureCode.getFromCode(errorCode) - throw StripeError.PostError( - response.code(), - errorCode, - declineCode - ) + if (failureCode is StripeFailureCode.Known) { + throw StripeError.PostError.Failed(response.code(), failureCode) + } else if (declineCode is StripeDeclineCode.Known) { + throw StripeError.PostError.Declined(response.code(), declineCode) + } else { + throw StripeError.PostError.Generic(response.code(), errorCode) + } } } @@ -342,6 +345,20 @@ class StripeApi( } } + private fun parseFailureCode(body: String?): StripeFailureCode? { + if (body == null) { + Log.d(TAG, "parseFailureCode: No body.", true) + return null + } + + return try { + StripeFailureCode.getFromCode(JSONObject(body).getJSONObject("error").getString("failure_code")) + } catch (e: Exception) { + Log.d(TAG, "parseFailureCode: Failed to parse failure code.", e, true) + null + } + } + object Validation { private val MAX_AMOUNT = BigDecimal(99_999_999) diff --git a/donations/lib/src/main/java/org/signal/donations/StripeError.kt b/donations/lib/src/main/java/org/signal/donations/StripeError.kt index d1313d8280..e19b8cff9d 100644 --- a/donations/lib/src/main/java/org/signal/donations/StripeError.kt +++ b/donations/lib/src/main/java/org/signal/donations/StripeError.kt @@ -7,5 +7,11 @@ sealed class StripeError(message: String) : Exception(message) { class FailedToParseSetupIntentResponseError(invalidDefCause: InvalidDefinitionException?) : StripeError("Failed to parse setup intent response: ${invalidDefCause?.type} ${invalidDefCause?.property} ${invalidDefCause?.beanDescription}") object FailedToParsePaymentMethodResponseError : StripeError("Failed to parse payment method response") object FailedToCreatePaymentSourceFromCardData : StripeError("Failed to create payment source from card data") - class PostError(val statusCode: Int, val errorCode: String?, val declineCode: StripeDeclineCode?) : StripeError("postForm failed with code: $statusCode. errorCode: $errorCode. declineCode: $declineCode") + sealed class PostError( + override val message: String + ) : StripeError(message) { + class Generic(statusCode: Int, val errorCode: String?) : PostError("postForm failed with code: $statusCode errorCode: $errorCode") + class Declined(statusCode: Int, val declineCode: StripeDeclineCode) : PostError("postForm failed with code: $statusCode declineCode: $declineCode") + class Failed(statusCode: Int, val failureCode: StripeFailureCode) : PostError("postForm failed with code: $statusCode failureCode: $failureCode") + } } diff --git a/donations/lib/src/main/java/org/signal/donations/StripeFailureCode.kt b/donations/lib/src/main/java/org/signal/donations/StripeFailureCode.kt new file mode 100644 index 0000000000..4fd5d326b6 --- /dev/null +++ b/donations/lib/src/main/java/org/signal/donations/StripeFailureCode.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.donations + +/** + * Bank Transfer failure codes, as detailed here: + * https://stripe.com/docs/payments/sepa-debit#failed-payments + */ +sealed interface StripeFailureCode { + data class Known(val code: Code) : StripeFailureCode + data class Unknown(val code: String) : StripeFailureCode + + val isKnown get() = this is Known + enum class Code(val code: String) { + REFER_TO_CUSTOMER("refer_to_customer"), + INSUFFICIENT_FUNDS("insufficient_funds"), + DEBIT_DISPUTED("debit_disputed"), + AUTHORIZATION_REVOKED("authorization_revoked"), + DEBIT_NOT_AUTHORIZED("debit_not_authorized"), + ACCOUNT_CLOSED("account_closed"), + BANK_ACCOUNT_RESTRICTED("bank_account_restricted"), + DEBIT_AUTHORIZATION_NOT_MATCH("debit_authorization_not_match"), + RECIPIENT_DECEASED("recipient_deceased"), + BRANCH_DOES_NOT_EXIST("branch_does_not_exist"), + INCORRECT_ACCOUNT_HOLDER_NAME("incorrect_account_holder_name"), + INVALID_ACCOUNT_NUMBER("invalid_account_number"), + GENERIC_COULD_NOT_PROCESS("generic_could_not_process") + } + + companion object { + fun getFromCode(code: String?): StripeFailureCode { + if (code == null) { + return Unknown("null") + } + + val typedCode: Code? = Code.values().firstOrNull { it.code == code } + return typedCode?.let { Known(typedCode) } ?: Unknown(code) + } + } +}