From eee4ff3f873a2095f5e127cbd9862e9202e36b26 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 29 Nov 2022 11:01:07 -0400 Subject: [PATCH] Add new error strings for credit cards. --- .../app/subscription/StripeRepository.kt | 20 +++-- .../donate/DonateToSignalFragment.kt | 17 +++- .../StripePaymentInProgressViewModel.kt | 36 +++++--- .../app/subscription/errors/DonationError.kt | 7 +- .../errors/DonationErrorDialogs.kt | 9 +- .../errors/DonationErrorNotifications.kt | 2 + .../errors/DonationErrorParams.kt | 88 +++++++++++++++++-- ...SubscriptionReceiptRequestResponseJob.java | 3 +- .../securesms/keyvalue/DonationsValues.kt | 18 +++- app/src/main/res/values/strings.xml | 18 ++++ .../donations/CreditCardPaymentSource.kt | 1 + .../donations/GooglePayPaymentSource.kt | 2 + .../java/org/signal/donations/StripeApi.kt | 1 + .../donations/StripePaymentSourceType.kt | 12 +++ 14 files changed, 200 insertions(+), 34 deletions(-) create mode 100644 donations/lib/src/main/java/org/signal/donations/StripePaymentSourceType.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt index c32c6345c8..5a28d37fc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/StripeRepository.kt @@ -11,6 +11,7 @@ import org.signal.core.util.money.FiatMoney import org.signal.donations.GooglePayApi import org.signal.donations.StripeApi import org.signal.donations.StripeIntentAccessor +import org.signal.donations.StripePaymentSourceType import org.signal.donations.json.StripeIntentStatus import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource @@ -86,12 +87,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str price: FiatMoney, badgeRecipient: RecipientId, badgeLevel: Long, + paymentSourceType: StripePaymentSourceType ): Single { Log.d(TAG, "Creating payment intent for $price...", true) return stripeApi.createPaymentIntent(price, badgeLevel) .onErrorResumeNext { - handleCreatePaymentIntentError(it, badgeRecipient) + handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType) } .flatMap { result -> val recipient = Recipient.resolved(badgeRecipient) @@ -127,7 +129,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str Log.d(TAG, "Confirming payment intent...", true) return stripeApi.confirmPaymentIntent(paymentSource, paymentIntent) .onErrorResumeNext { - Single.error(DonationError.getPaymentSetupError(donationErrorSource, it)) + Single.error(DonationError.getPaymentSetupError(donationErrorSource, it, paymentSource.type)) } } @@ -196,7 +198,10 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str } } - fun setDefaultPaymentMethod(paymentMethodId: String): Completable { + fun setDefaultPaymentMethod( + paymentMethodId: String, + paymentSourceType: StripePaymentSourceType + ): Completable { return Single.fromCallable { Log.d(TAG, "Getting the subscriber...") SignalStore.donationsValues().requireSubscriber() @@ -209,6 +214,9 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str } }.flatMap(ServiceResponse::flattenResult).ignoreElement().doOnComplete { Log.d(TAG, "Set default payment method via Signal service!") + }.andThen { + Log.d(TAG, "Storing the subscription payment source type locally.") + SignalStore.donationsValues().setSubscriptionPaymentSourceType(paymentSourceType) } } @@ -216,7 +224,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str Log.d(TAG, "Creating credit card payment source via Stripe api...") return stripeApi.createPaymentSourceFromCardData(cardData).map { when (it) { - is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason) + is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason, StripePaymentSourceType.CREDIT_CARD) is StripeApi.CreatePaymentSourceFromCardDataResult.Success -> it.paymentSource } } @@ -230,13 +238,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str companion object { private val TAG = Log.tag(StripeRepository::class.java) - fun handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId): Single { + private fun handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: StripePaymentSourceType): Single { return if (throwable is DonationError) { Single.error(throwable) } else { val recipient = Recipient.resolved(badgeRecipient) val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT - Single.error(DonationError.getPaymentSetupError(errorSource, throwable)) + Single.error(DonationError.getPaymentSetupError(errorSource, throwable, paymentSourceType)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt index 7f09ca4218..12be5c633b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate +import android.content.Context import android.content.DialogInterface import android.text.SpannableStringBuilder import android.view.View @@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boo import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure @@ -395,9 +397,22 @@ class DonateToSignalFragment : errorDialog = DonationErrorDialogs.show( requireContext(), throwable, object : DonationErrorDialogs.DialogCallback() { + var tryCCAgain = false + + override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction? { + return DonationErrorParams.ErrorAction( + label = R.string.DeclineCode__try, + action = { + tryCCAgain = true + } + ) + } + override fun onDialogDismissed() { errorDialog = null - findNavController().popBackStack() + if (!tryCCAgain) { + findNavController().popBackStack() + } } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt index ac46bb430e..d96786c966 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt @@ -14,6 +14,7 @@ import org.signal.core.util.logging.Log import org.signal.donations.GooglePayPaymentSource import org.signal.donations.StripeApi import org.signal.donations.StripeIntentAccessor +import org.signal.donations.StripePaymentSourceType import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository @@ -75,7 +76,7 @@ class StripePaymentInProgressViewModel( DonateToSignalType.GIFT -> DonationErrorSource.GIFT } - val paymentSourceProvider: Single = resolvePaymentSourceProvider(errorSource) + val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(errorSource) return when (request.donateToSignalType) { DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentSourceProvider, nextActionHandler) @@ -84,17 +85,23 @@ class StripePaymentInProgressViewModel( } } - private fun resolvePaymentSourceProvider(errorSource: DonationErrorSource): Single { + private fun resolvePaymentSourceProvider(errorSource: DonationErrorSource): PaymentSourceProvider { val paymentData = this.paymentData val cardData = this.cardData return when { paymentData == null && cardData == null -> error("No payment provider available.") paymentData != null && cardData != null -> error("Too many providers available") - paymentData != null -> Single.just(GooglePayPaymentSource(paymentData)) - cardData != null -> stripeRepository.createCreditCardPaymentSource(errorSource, cardData) + paymentData != null -> PaymentSourceProvider( + StripePaymentSourceType.GOOGLE_PAY, + Single.just(GooglePayPaymentSource(paymentData)).doAfterTerminate { clearPaymentInformation() } + ) + cardData != null -> PaymentSourceProvider( + StripePaymentSourceType.CREDIT_CARD, + stripeRepository.createCreditCardPaymentSource(errorSource, cardData).doAfterTerminate { clearPaymentInformation() } + ) else -> error("This should never happen.") - }.doAfterTerminate { clearPaymentInformation() } + } } fun providePaymentData(paymentData: PaymentData) { @@ -118,9 +125,9 @@ class StripePaymentInProgressViewModel( cardData = null } - private fun proceedMonthly(request: GatewayRequest, paymentSourceProvider: Single, nextActionHandler: (StripeApi.Secure3DSAction) -> Single) { + private fun proceedMonthly(request: GatewayRequest, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: (StripeApi.Secure3DSAction) -> Single) { val ensureSubscriberId: Completable = monthlyDonationRepository.ensureSubscriberId() - val createAndConfirmSetupIntent: Single = paymentSourceProvider.flatMap { stripeRepository.createAndConfirmSetupIntent(it) } + val createAndConfirmSetupIntent: Single = paymentSourceProvider.paymentSource.flatMap { stripeRepository.createAndConfirmSetupIntent(it) } val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString()) Log.d(TAG, "Starting subscription payment pipeline...", true) @@ -134,12 +141,12 @@ class StripePaymentInProgressViewModel( .flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult) } .map { (_, paymentMethod) -> paymentMethod ?: secure3DSAction.paymentMethodId!! } } - .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it) } + .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it, paymentSourceProvider.paymentSourceType) } .onErrorResumeNext { if (it is DonationError) { Completable.error(it) } else { - Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it)) + Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, paymentSourceProvider.paymentSourceType)) } } @@ -164,15 +171,15 @@ class StripePaymentInProgressViewModel( private fun proceedOneTime( request: GatewayRequest, - paymentSourceProvider: Single, + paymentSourceProvider: PaymentSourceProvider, nextActionHandler: (StripeApi.Secure3DSAction) -> Single ) { Log.w(TAG, "Beginning one-time payment pipeline...", true) val amount = request.fiat - val continuePayment: Single = stripeRepository.continuePayment(amount, request.recipientId, request.level) - val intentAndSource: Single> = Single.zip(continuePayment, paymentSourceProvider, ::Pair) + val continuePayment: Single = stripeRepository.continuePayment(amount, request.recipientId, request.level, paymentSourceProvider.paymentSourceType) + val intentAndSource: Single> = Single.zip(continuePayment, paymentSourceProvider.paymentSource, ::Pair) disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) -> stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId) @@ -249,6 +256,11 @@ class StripePaymentInProgressViewModel( ) } + private data class PaymentSourceProvider( + val paymentSourceType: StripePaymentSourceType, + val paymentSource: Single + ) + class Factory( private val stripeRepository: StripeRepository, private val monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()), 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 d5864c0216..8bdb8e9985 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 @@ -7,6 +7,7 @@ import io.reactivex.rxjava3.subjects.Subject import org.signal.core.util.logging.Log import org.signal.donations.StripeDeclineCode import org.signal.donations.StripeError +import org.signal.donations.StripePaymentSourceType sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) { @@ -55,7 +56,7 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : /** * Payment failed by the credit card processor, with a specific reason told to us by Stripe. */ - class DeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode) : PaymentSetupError(source, cause) + class DeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: StripePaymentSourceType) : PaymentSetupError(source, cause) } /** @@ -132,13 +133,13 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : * charge has occurred. */ @JvmStatic - fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable): DonationError { + fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: StripePaymentSourceType): DonationError { return if (throwable is StripeError.PostError) { val declineCode: StripeDeclineCode? = throwable.declineCode val errorCode: String? = throwable.errorCode when { - declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode) + declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode, method) errorCode != null -> PaymentSetupError.CodedError(source, throwable, errorCode) else -> PaymentSetupError.GenericError(source, 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 569700bcba..bbb8b30428 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 @@ -36,7 +36,7 @@ object DonationErrorDialogs { return builder.show() } - open class DialogCallback : DonationErrorParams.Callback { + abstract class DialogCallback : DonationErrorParams.Callback { override fun onCancel(context: Context): DonationErrorParams.ErrorAction? { return DonationErrorParams.ErrorAction( @@ -61,6 +61,13 @@ object DonationErrorDialogs { ) } + override fun onTryCreditCardAgain(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 ade8cefd33..5be4f9a487 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 @@ -63,6 +63,8 @@ object DonationErrorNotifications { ) } + override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction? = null + override fun onGoToGooglePay(context: Context): DonationErrorParams.ErrorAction { return createAction( context = context, 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 60964762aa..a26e47c6c5 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 @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors import android.content.Context import androidx.annotation.StringRes import org.signal.donations.StripeDeclineCode +import org.signal.donations.StripePaymentSourceType import org.thoughtcrime.securesms.R class DonationErrorParams private constructor( @@ -88,19 +89,78 @@ class DonationErrorParams private constructor( } private fun getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback): DonationErrorParams { + val getStripeDeclineCodePositiveActionParams: (Context, Callback, Int) -> DonationErrorParams = when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> this::getTryCreditCardAgainParams + StripePaymentSourceType.GOOGLE_PAY -> this::getGoToGooglePayParams + } + return when (declinedError.declineCode) { is StripeDeclineCode.Known -> when (declinedError.declineCode.code) { - StripeDeclineCode.Code.APPROVE_WITH_ID -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again) - StripeDeclineCode.Code.CALL_ISSUER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem) + StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again + } + ) + StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues + StripePaymentSourceType.GOOGLE_PAY -> 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 -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase) - StripeDeclineCode.Code.EXPIRED_CARD -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_has_expired) - StripeDeclineCode.Code.INCORRECT_NUMBER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_number_is_incorrect) - StripeDeclineCode.Code.INCORRECT_CVC -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_cards_cvc_number_is_incorrect) + StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_has_expired + } + ) + StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_number_is_incorrect + } + ) + StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect + } + ) StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_have_sufficient_funds) - StripeDeclineCode.Code.INVALID_CVC -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_cards_cvc_number_is_incorrect) - StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__the_expiration_month) - StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__the_expiration_year) - StripeDeclineCode.Code.INVALID_NUMBER -> getGoToGooglePayParams(context, callback, R.string.DeclineCode__your_card_number_is_incorrect) + StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect + } + ) + StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_month + } + ) + StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_year + } + ) + StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams( + context, callback, + when (declinedError.method) { + StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details + StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_number_is_incorrect + } + ) StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_completing_the_payment_again) StripeDeclineCode.Code.PROCESSING_ERROR -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_again) StripeDeclineCode.Code.REENTER_TRANSACTION -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_again) @@ -127,6 +187,15 @@ class DonationErrorParams private constructor( negativeAction = callback.onCancel(context) ) } + + private fun getTryCreditCardAgainParams(context: Context, callback: Callback, message: Int): DonationErrorParams { + return DonationErrorParams( + title = R.string.DonationsErrors__error_processing_payment, + message = message, + positiveAction = callback.onTryCreditCardAgain(context), + negativeAction = callback.onCancel(context) + ) + } } interface Callback { @@ -135,5 +204,6 @@ class DonationErrorParams private constructor( fun onLearnMore(context: Context): ErrorAction? fun onContactSupport(context: Context): ErrorAction? fun onGoToGooglePay(context: Context): ErrorAction? + fun onTryCreditCardAgain(context: Context): ErrorAction? } } 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 dfa9bee3fc..1e95776423 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -300,7 +300,8 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { paymentSetupError = new DonationError.PaymentSetupError.DeclinedError( getErrorSource(), new Exception(chargeFailure.getMessage()), - declineCode + declineCode, + SignalStore.donationsValues().getSubscriptionPaymentSourceType() ); } else { paymentSetupError = new DonationError.PaymentSetupError.CodedError( diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index 565a43e129..702b7683e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -6,6 +6,7 @@ import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.Subject import org.signal.core.util.logging.Log import org.signal.donations.StripeApi +import org.signal.donations.StripePaymentSourceType import org.signal.libsignal.zkgroup.InvalidInputException import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation @@ -95,6 +96,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign * assumed that there is no work to be done. */ private const val SUBSCRIPTION_EOP_REDEEMED = "subscription.eop.redeemed" + + /** + * Notes the type of payment the user utilized for the latest subscription. This is useful + * in determining which error messaging they should see if something goes wrong. + */ + private const val SUBSCRIPTION_PAYMENT_SOURCE_TYPE = "subscription.payment.source.type" } override fun onFirstEverAppLaunch() = Unit @@ -112,7 +119,8 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign SUBSCRIPTION_CREDENTIAL_RECEIPT, SUBSCRIPTION_EOP_STARTED_TO_CONVERT, SUBSCRIPTION_EOP_STARTED_TO_REDEEM, - SUBSCRIPTION_EOP_REDEEMED + SUBSCRIPTION_EOP_REDEEMED, + SUBSCRIPTION_PAYMENT_SOURCE_TYPE ) private val subscriptionCurrencyPublisher: Subject by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) } @@ -442,6 +450,14 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign remove(SUBSCRIPTION_CREDENTIAL_RECEIPT) } + fun setSubscriptionPaymentSourceType(stripePaymentSourceType: StripePaymentSourceType) { + putString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, stripePaymentSourceType.code) + } + + fun getSubscriptionPaymentSourceType(): StripePaymentSourceType { + return StripePaymentSourceType.fromCode(getString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, null)) + } + var subscriptionEndOfPeriodConversionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_CONVERT, 0L) var subscriptionEndOfPeriodRedemptionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_REDEEM, 0L) var subscriptionEndOfPeriodRedeemed by longValue(SUBSCRIPTION_EOP_REDEEMED, 0L) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ba41dcbf2..f50fde489a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4607,6 +4607,8 @@ Your card has expired. Update your payment method in Google Pay and try again. Go to Google Pay + + Try again Your card number is incorrect. Update it in Google Pay and try again. @@ -4622,6 +4624,22 @@ Try again or contact your bank for more information. + + + Verify your card details are correct and try again. + + Verify your card details are correct and try again. If the problem continues, contact your bank. + + Your card has expired. Verify your card details are correct and try again. + + Your card\'s CVC number is incorrect. Verify your card details are correct and try again. + + The expiration month on your card is incorrect. Verify your card details are correct and try again. + + The expiration year on your card is incorrect. Verify your card details are correct and try again. + + Your card number is incorrect. Verify your card details are correct and try again. + Name your profile diff --git a/donations/lib/src/main/java/org/signal/donations/CreditCardPaymentSource.kt b/donations/lib/src/main/java/org/signal/donations/CreditCardPaymentSource.kt index 8311f5c9c8..b8833b879e 100644 --- a/donations/lib/src/main/java/org/signal/donations/CreditCardPaymentSource.kt +++ b/donations/lib/src/main/java/org/signal/donations/CreditCardPaymentSource.kt @@ -8,6 +8,7 @@ import org.json.JSONObject class CreditCardPaymentSource( private val payload: JSONObject ) : StripeApi.PaymentSource { + override val type = StripePaymentSourceType.CREDIT_CARD override fun parameterize(): JSONObject = payload override fun getTokenId(): String = parameterize().getString("id") override fun email(): String? = null diff --git a/donations/lib/src/main/java/org/signal/donations/GooglePayPaymentSource.kt b/donations/lib/src/main/java/org/signal/donations/GooglePayPaymentSource.kt index e4ab2af28e..8014e4498d 100644 --- a/donations/lib/src/main/java/org/signal/donations/GooglePayPaymentSource.kt +++ b/donations/lib/src/main/java/org/signal/donations/GooglePayPaymentSource.kt @@ -4,6 +4,8 @@ import com.google.android.gms.wallet.PaymentData import org.json.JSONObject class GooglePayPaymentSource(private val paymentData: PaymentData) : StripeApi.PaymentSource { + override val type = StripePaymentSourceType.GOOGLE_PAY + override fun parameterize(): JSONObject { val jsonData = JSONObject(paymentData.toJson()) val paymentMethodJsonData = jsonData.getJSONObject("paymentMethodData") 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 b73a62eb43..e7377edf99 100644 --- a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt +++ b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt @@ -520,6 +520,7 @@ class StripeApi( ) : Parcelable interface PaymentSource { + val type: StripePaymentSourceType fun parameterize(): JSONObject fun getTokenId(): String fun email(): String? diff --git a/donations/lib/src/main/java/org/signal/donations/StripePaymentSourceType.kt b/donations/lib/src/main/java/org/signal/donations/StripePaymentSourceType.kt new file mode 100644 index 0000000000..c179d52b11 --- /dev/null +++ b/donations/lib/src/main/java/org/signal/donations/StripePaymentSourceType.kt @@ -0,0 +1,12 @@ +package org.signal.donations + +enum class StripePaymentSourceType(val code: String) { + CREDIT_CARD("credit_card"), + GOOGLE_PAY("google_pay"); + + companion object { + fun fromCode(code: String?): StripePaymentSourceType { + return values().firstOrNull { it.code == code } ?: GOOGLE_PAY + } + } +}