Add proper endpoint for setting iDEAL default payment method.

This commit is contained in:
Alex Hart
2023-10-23 13:13:13 -04:00
committed by GitHub
parent 10eec025d2
commit a4df433d80
9 changed files with 47 additions and 44 deletions

View File

@@ -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<StatusAndPaymentMethodId> {
fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor): Single<StatusAndPaymentMethodId> {
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<EmptyResponse>::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?
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -228,6 +228,13 @@ public class DonationsService {
});
}
public ServiceResponse<EmptyResponse> 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

View File

@@ -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", "");
}