From a4df433d8014fe8a7c37b12fcb4926eafc91e5c1 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 23 Oct 2023 13:13:13 -0400 Subject: [PATCH] Add proper endpoint for setting iDEAL default payment method. --- .../app/subscription/StripeRepository.kt | 26 +++++++++++-------- .../donate/stripe/ExternalNavigationHelper.kt | 3 ++- .../StripePaymentInProgressViewModel.kt | 13 +++++----- .../jobs/DonationReceiptRedemptionJob.java | 2 +- .../jobs/ExternalLaunchDonationJob.kt | 9 +++++-- .../java/org/signal/donations/StripeApi.kt | 3 ++- .../donations/json/StripeSetupIntent.kt | 23 ++-------------- .../api/services/DonationsService.java | 7 +++++ .../internal/push/PushServiceSocket.java | 5 ++++ 9 files changed, 47 insertions(+), 44 deletions(-) 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 1228f4cfcf..4ae83b81f8 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 @@ -202,23 +202,19 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str * that we are successful and proceed as normal. If the payment didn't actually succeed, then we * expect an error later in the chain to inform us of this. */ - fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor, paymentSourceType: PaymentSourceType): Single { + fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor): Single { return Single.fromCallable { when (stripeIntentAccessor.objectType) { - StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(StripeIntentStatus.SUCCEEDED, null) + StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(stripeIntentAccessor.intentId, StripeIntentStatus.SUCCEEDED, null) StripeIntentAccessor.ObjectType.PAYMENT_INTENT -> stripeApi.getPaymentIntent(stripeIntentAccessor).let { if (it.status == null) { Log.d(TAG, "Returned payment intent had a null status.", true) } - StatusAndPaymentMethodId(it.status ?: StripeIntentStatus.SUCCEEDED, it.paymentMethod) + StatusAndPaymentMethodId(stripeIntentAccessor.intentId, it.status ?: StripeIntentStatus.SUCCEEDED, it.paymentMethod) } StripeIntentAccessor.ObjectType.SETUP_INTENT -> stripeApi.getSetupIntent(stripeIntentAccessor).let { - if (paymentSourceType == PaymentSourceType.Stripe.IDEAL) { - StatusAndPaymentMethodId(it.status, it.requireGeneratedSepaDebit()) - } else { - StatusAndPaymentMethodId(it.status, it.paymentMethod) - } + StatusAndPaymentMethodId(stripeIntentAccessor.intentId, it.status, it.paymentMethod) } } } @@ -226,6 +222,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str fun setDefaultPaymentMethod( paymentMethodId: String, + setupIntentId: String, paymentSourceType: PaymentSourceType ): Completable { return Single.fromCallable { @@ -235,9 +232,15 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str Log.d(TAG, "Setting default payment method via Signal service...") // TODO [sepa] -- iDEAL has its own call Single.fromCallable { - ApplicationDependencies - .getDonationsService() - .setDefaultStripePaymentMethod(it.subscriberId, paymentMethodId) + if (paymentSourceType == PaymentSourceType.Stripe.IDEAL) { + ApplicationDependencies + .getDonationsService() + .setDefaultIdealPaymentMethod(it.subscriberId, setupIntentId) + } else { + ApplicationDependencies + .getDonationsService() + .setDefaultStripePaymentMethod(it.subscriberId, paymentMethodId) + } } }.flatMap(ServiceResponse::flattenResult).ignoreElement().doOnComplete { Log.d(TAG, "Set default payment method via Signal service!") @@ -267,6 +270,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str } data class StatusAndPaymentMethodId( + val intentId: String, val status: StripeIntentStatus, val paymentMethod: String? ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/ExternalNavigationHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/ExternalNavigationHelper.kt index e8cb17d280..7de194f189 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/ExternalNavigationHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/ExternalNavigationHelper.kt @@ -11,6 +11,7 @@ import android.content.Intent import android.net.Uri import android.widget.Toast import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.donations.StripeApi import org.thoughtcrime.securesms.R /** @@ -21,7 +22,7 @@ object ExternalNavigationHelper { fun maybeLaunchExternalNavigationIntent(context: Context, webRequestUri: Uri?, launchIntent: (Intent) -> Unit): Boolean { val url = webRequestUri ?: return false - if (url.scheme?.startsWith("http") == true) { + if (url.scheme?.startsWith("http") == true || url.scheme == StripeApi.RETURN_URL_SCHEME) { return false } 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 a812b0d73d..d788131b2e 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 @@ -165,14 +165,13 @@ class StripePaymentInProgressViewModel( paymentSourceProvider.paymentSourceType.code ) ) - .flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, paymentSourceProvider.paymentSourceType) } - .map { (_, paymentMethod) -> paymentMethod ?: secure3DSAction.paymentMethodId!! } + .flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult) } } - .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it, paymentSourceProvider.paymentSourceType) } + .flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it.paymentMethod!!, it.intentId, paymentSourceProvider.paymentSourceType) } .onErrorResumeNext { - when { - it is DonationError -> Completable.error(it) - it is DonationProcessorError -> Completable.error(it.toDonationError(DonationErrorSource.MONTHLY, paymentSourceProvider.paymentSourceType)) + when (it) { + is DonationError -> Completable.error(it) + is DonationProcessorError -> Completable.error(it.toDonationError(DonationErrorSource.MONTHLY, paymentSourceProvider.paymentSourceType)) else -> Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.MONTHLY, it, paymentSourceProvider.paymentSourceType)) } } @@ -225,7 +224,7 @@ class StripePaymentInProgressViewModel( ) ) } - .flatMap { stripeRepository.getStatusAndPaymentMethodId(it, paymentSourceProvider.paymentSourceType) } + .flatMap { stripeRepository.getStatusAndPaymentMethodId(it) } .flatMapCompletable { oneTimeDonationRepository.waitForOneTimeRedemption( gatewayRequest = request, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java index 99b8777bae..b437202a72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -303,7 +303,7 @@ public class DonationReceiptRedemptionJob extends BaseJob { } private boolean isForSubscription() { - return Objects.equals(getParameters().getQueue(), SUBSCRIPTION_QUEUE); + return Objects.requireNonNull(getParameters().getQueue()).startsWith(SUBSCRIPTION_QUEUE); } private boolean isForOneTimeDonation() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt index 1201d77558..f0999fb174 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt @@ -139,8 +139,13 @@ class ExternalLaunchDonationJob private constructor( val subscriber = SignalStore.donationsValues().requireSubscriber() Log.i(TAG, "Setting default payment method...", true) - val setPaymentMethodResponse = ApplicationDependencies.getDonationsService() - .setDefaultStripePaymentMethod(subscriber.subscriberId, stripeSetupIntent.paymentMethod!!) + val setPaymentMethodResponse = if (stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.IDEAL) { + ApplicationDependencies.getDonationsService() + .setDefaultIdealPaymentMethod(subscriber.subscriberId, stripeSetupIntent.id) + } else { + ApplicationDependencies.getDonationsService() + .setDefaultStripePaymentMethod(subscriber.subscriberId, stripeSetupIntent.paymentMethod!!) + } getResultOrThrow(setPaymentMethodResponse) 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 f46db5d3be..4efbdbd252 100644 --- a/donations/lib/src/main/java/org/signal/donations/StripeApi.kt +++ b/donations/lib/src/main/java/org/signal/donations/StripeApi.kt @@ -42,7 +42,8 @@ class StripeApi( private val CARD_YEAR_KEY = "card[exp_year]" private val CARD_CVC_KEY = "card[cvc]" - private const val RETURN_URL_3DS = "sgnlpay://3DS" + const val RETURN_URL_SCHEME = "sgnlpay" + private const val RETURN_URL_3DS = "$RETURN_URL_SCHEME://3DS" } sealed class CreatePaymentIntentResult { diff --git a/donations/lib/src/main/java/org/signal/donations/json/StripeSetupIntent.kt b/donations/lib/src/main/java/org/signal/donations/json/StripeSetupIntent.kt index 0047adcc08..799d360dcc 100644 --- a/donations/lib/src/main/java/org/signal/donations/json/StripeSetupIntent.kt +++ b/donations/lib/src/main/java/org/signal/donations/json/StripeSetupIntent.kt @@ -15,24 +15,5 @@ data class StripeSetupIntent @JsonCreator constructor( @JsonProperty("client_secret") val clientSecret: String, @JsonProperty("status") val status: StripeIntentStatus, @JsonProperty("payment_method") val paymentMethod: String?, - @JsonProperty("customer") val customer: String?, - @JsonProperty("latest_attempt") val latestAttempt: LatestAttempt? -) { - - fun requireGeneratedSepaDebit(): String = latestAttempt!!.paymentMethodDetails!!.ideal!!.generatedSepaDebit!! - - @JsonIgnoreProperties - data class LatestAttempt @JsonCreator constructor( - @JsonProperty("payment_method_details") val paymentMethodDetails: PaymentMethodDetails? - ) - - @JsonIgnoreProperties - data class PaymentMethodDetails @JsonCreator constructor( - @JsonProperty("ideal") val ideal: Ideal? - ) - - @JsonIgnoreProperties - data class Ideal @JsonCreator constructor( - @JsonProperty("generated_sepa_debit") val generatedSepaDebit: String? - ) -} + @JsonProperty("customer") val customer: String? +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java index 220dd5e73d..ab10e6be67 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -228,6 +228,13 @@ public class DonationsService { }); } + public ServiceResponse setDefaultIdealPaymentMethod(SubscriberId subscriberId, String setupIntentId) { + return wrapInServiceResponse(() -> { + pushServiceSocket.setDefaultIdealSubscriptionPaymentMethod(subscriberId.serialize(), setupIntentId); + return new Pair<>(EmptyResponse.INSTANCE, 200); + }); + } + /** * @param subscriberId The subscriber ID to create a payment method for. * @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 926500c7cf..46ede03a3a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -281,6 +281,7 @@ public class PushServiceSocket { private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method?type=%s"; private static final String CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal"; private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/stripe/%s"; + private static final String DEFAULT_IDEAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method_for_ideal/%s"; private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/braintree/%s"; private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials"; private static final String CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/create"; @@ -1245,6 +1246,10 @@ public class PushServiceSocket { makeServiceRequestWithoutAuthentication(String.format(DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", ""); } + public void setDefaultIdealSubscriptionPaymentMethod(String subscriberId, String setupIntentId) throws IOException { + makeServiceRequestWithoutAuthentication(String.format(DEFAULT_IDEAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, setupIntentId), "POST", ""); + } + public void setDefaultPaypalSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException { makeServiceRequestWithoutAuthentication(String.format(DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", ""); }