Add SEPA API endpoints.

This commit is contained in:
Alex Hart 2023-10-03 09:57:34 -04:00 committed by Cody Henthorne
parent f5c5a34798
commit 6279149cb8
13 changed files with 140 additions and 36 deletions

View file

@ -14,6 +14,7 @@ import java.util.Currency
private const val CARD = "CARD"
private const val PAYPAL = "PAYPAL"
private const val SEPA_DEBIT = "SEPA_DEBIT"
/**
* Transforms the DonationsConfiguration into a Set<FiatMoney> which has been properly filtered
@ -116,6 +117,7 @@ private fun DonationsConfiguration.getFilteredCurrencies(paymentMethodAvailabili
interface PaymentMethodAvailability {
fun isPayPalAvailable(): Boolean
fun isGooglePayOrCreditCardAvailable(): Boolean
fun isSEPADebitAvailable(): Boolean
fun toSet(): Set<String> {
val set = mutableSetOf<String>()
@ -127,6 +129,10 @@ interface PaymentMethodAvailability {
set.add(CARD)
}
if (isSEPADebitAvailable()) {
set.add(SEPA_DEBIT)
}
return set
}
}
@ -134,4 +140,5 @@ interface PaymentMethodAvailability {
private object DefaultPaymentMethodAvailability : PaymentMethodAvailability {
override fun isPayPalAvailable(): Boolean = InAppDonations.isPayPalAvailable()
override fun isGooglePayOrCreditCardAvailable(): Boolean = InAppDonations.isCreditCardAvailable() || InAppDonations.isGooglePayAvailable()
override fun isSEPADebitAvailable(): Boolean = InAppDonations.isSEPADebitAvailable()
}

View file

@ -16,10 +16,11 @@ object InAppDonations {
*
* - Able to use Credit Cards and is in a region where they are able to be accepted.
* - Able to access Google Play services (and thus possibly able to use Google Pay).
* - Able to use SEPA Debit and is in a region where they are able to be accepted.
* - Able to use PayPal and is in a region where it is able to be accepted.
*/
fun hasAtLeastOnePaymentMethodAvailable(): Boolean {
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable()
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable()
}
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, donateToSignalType: DonateToSignalType): Boolean {
@ -27,6 +28,7 @@ object InAppDonations {
PaymentSourceType.PayPal -> isPayPalAvailableForDonateToSignalType(donateToSignalType)
PaymentSourceType.Stripe.CreditCard -> isCreditCardAvailable()
PaymentSourceType.Stripe.GooglePay -> isGooglePayAvailable()
PaymentSourceType.Stripe.SEPADebit -> isSEPADebitAvailable()
PaymentSourceType.Unknown -> false
}
}
@ -58,4 +60,11 @@ object InAppDonations {
fun isGooglePayAvailable(): Boolean {
return SignalStore.donationsValues().isGooglePayReady && !LocaleFeatureFlags.isGooglePayDisabled()
}
/**
* Whether the user is in a region which supports SEPA Debit transfers, based off local phone number.
*/
fun isSEPADebitAvailable(): Boolean {
return FeatureFlags.sepaDebitDonations()
}
}

View file

@ -90,9 +90,11 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
badgeLevel: Long,
paymentSourceType: PaymentSourceType
): Single<StripeIntentAccessor> {
check(paymentSourceType is PaymentSourceType.Stripe)
Log.d(TAG, "Creating payment intent for $price...", true)
return stripeApi.createPaymentIntent(price, badgeLevel)
return stripeApi.createPaymentIntent(price, badgeLevel, paymentSourceType)
.onErrorResumeNext {
OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
}
@ -110,9 +112,12 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
}.subscribeOn(Schedulers.io())
}
fun createAndConfirmSetupIntent(paymentSource: StripeApi.PaymentSource): Single<StripeApi.Secure3DSAction> {
fun createAndConfirmSetupIntent(
paymentSource: StripeApi.PaymentSource,
paymentSourceType: PaymentSourceType.Stripe
): Single<StripeApi.Secure3DSAction> {
Log.d(TAG, "Continuing subscription setup...", true)
return stripeApi.createSetupIntent()
return stripeApi.createSetupIntent(paymentSourceType)
.flatMap { result ->
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
stripeApi.confirmSetupIntent(paymentSource, result.setupIntent)
@ -134,13 +139,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
}
}
override fun fetchPaymentIntent(price: FiatMoney, level: Long): Single<StripeIntentAccessor> {
override fun fetchPaymentIntent(price: FiatMoney, level: Long, sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
Log.d(TAG, "Fetching payment intent from Signal service for $price... (Locale.US minimum precision: ${price.minimumUnitPrecisionString})")
return Single
.fromCallable {
ApplicationDependencies
.getDonationsService()
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, level)
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, level, sourceType.paymentMethod)
}
.flatMap(ServiceResponse<StripeClientSecret>::flattenResult)
.map {
@ -159,27 +164,27 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
* it means that the PaymentMethod is already tied to a PayPal account. We can retry in this
* situation by simply deleting the old subscriber id on the service and replacing it.
*/
private fun createPaymentMethod(retryOn409: Boolean = true): Single<StripeClientSecret> {
private fun createPaymentMethod(paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single<StripeClientSecret> {
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
.flatMap {
Single.fromCallable {
ApplicationDependencies
.getDonationsService()
.createStripeSubscriptionPaymentMethod(it.subscriberId)
.createStripeSubscriptionPaymentMethod(it.subscriberId, paymentSourceType.paymentMethod)
}
}
.flatMap { serviceResponse ->
if (retryOn409 && serviceResponse.status == 409) {
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(retryOn409 = false))
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(paymentSourceType, retryOn409 = false))
} else {
serviceResponse.flattenResult()
}
}
}
override fun fetchSetupIntent(): Single<StripeIntentAccessor> {
override fun fetchSetupIntent(sourceType: PaymentSourceType.Stripe): Single<StripeIntentAccessor> {
Log.d(TAG, "Fetching setup intent from Signal service...")
return createPaymentMethod()
return createPaymentMethod(sourceType)
.map {
StripeIntentAccessor(
objectType = StripeIntentAccessor.ObjectType.SETUP_INTENT,

View file

@ -128,7 +128,10 @@ class StripePaymentInProgressViewModel(
private fun proceedMonthly(request: GatewayRequest, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: (StripeApi.Secure3DSAction) -> Single<StripeIntentAccessor>) {
val ensureSubscriberId: Completable = monthlyDonationRepository.ensureSubscriberId()
val createAndConfirmSetupIntent: Single<StripeApi.Secure3DSAction> = paymentSourceProvider.paymentSource.flatMap { stripeRepository.createAndConfirmSetupIntent(it) }
val createAndConfirmSetupIntent: Single<StripeApi.Secure3DSAction> = paymentSourceProvider.paymentSource.flatMap {
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
}
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString())
Log.d(TAG, "Starting subscription payment pipeline...", true)

View file

@ -33,18 +33,21 @@ class DonationErrorParams<V> private constructor(
positiveAction = callback.onOk(context),
negativeAction = null
)
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> DonationErrorParams(
title = R.string.DonationsErrors__still_processing,
message = R.string.DonationsErrors__your_payment_is_still,
positiveAction = callback.onOk(context),
negativeAction = null
)
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> DonationErrorParams(
title = R.string.DonationsErrors__failed_to_validate_badge,
message = R.string.DonationsErrors__could_not_validate,
positiveAction = callback.onContactSupport(context),
negativeAction = null
)
is DonationError.BadgeRedemptionError.GenericError -> getGenericRedemptionError(context, throwable, callback)
else -> DonationErrorParams(
title = R.string.DonationsErrors__couldnt_add_badge,
@ -63,6 +66,7 @@ class DonationErrorParams<V> private constructor(
positiveAction = callback.onContactSupport(context),
negativeAction = null
)
else -> DonationErrorParams(
title = R.string.DonationsErrors__couldnt_add_badge,
message = R.string.DonationsErrors__your_badge_could_not,
@ -80,6 +84,7 @@ class DonationErrorParams<V> private constructor(
positiveAction = callback.onOk(context),
negativeAction = null
)
else -> DonationErrorParams(
title = R.string.DonationsErrors__cannot_send_donation,
message = R.string.DonationsErrors__this_user_cant_receive_donations_until,
@ -97,9 +102,14 @@ class DonationErrorParams<V> private constructor(
}
private fun <V> getStripeDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
fun unexpectedDeclinedError(declinedError: DonationError.PaymentSetupError.StripeDeclinedError): Nothing {
error("Unexpected declined error: ${declinedError.declineCode} during ${declinedError.method} processing.")
}
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> this::getTryCreditCardAgainParams
PaymentSourceType.Stripe.GooglePay -> this::getGoToGooglePayParams
PaymentSourceType.Stripe.SEPADebit -> error("Not implemented.")
}
return when (declinedError.declineCode) {
@ -110,16 +120,20 @@ class DonationErrorParams<V> private constructor(
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase)
StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams(
context,
@ -127,24 +141,30 @@ class DonationErrorParams<V> private constructor(
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_has_expired
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_have_sufficient_funds)
StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams(
context,
@ -152,37 +172,46 @@ class DonationErrorParams<V> private constructor(
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_month
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_year
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams(
context,
callback,
when (declinedError.method) {
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
PaymentSourceType.Stripe.SEPADebit -> unexpectedDeclinedError(declinedError)
}
)
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)
else -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank)
}
else -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank)
}
}

View file

@ -117,6 +117,7 @@ public final class FeatureFlags {
public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback";
private static final String CONVERSATION_ITEM_V2_TEXT = "android.conversationItemV2.text.4";
public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
private static final String SEPA_DEBIT_DONATIONS = "android.sepa.debit.donations";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -190,7 +191,8 @@ public final class FeatureFlags {
@VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
PHONE_NUMBER_PRIVACY
PHONE_NUMBER_PRIVACY,
SEPA_DEBIT_DONATIONS
);
/**
@ -680,6 +682,13 @@ public final class FeatureFlags {
return getString(CRASH_PROMPT_CONFIG, "");
}
/**
* Whether or not SEPA debit payments for donations are enabled.
* WARNING: This feature is under heavy development and is *not* ready for wider use.
*/
public static boolean sepaDebitDonations() {
return getBoolean(SEPA_DEBIT_DONATIONS, Environment.IS_STAGING);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {

View file

@ -212,15 +212,18 @@ class DonationsConfigurationExtensionsKtTest {
private object AllPaymentMethodsAvailability : PaymentMethodAvailability {
override fun isPayPalAvailable(): Boolean = true
override fun isGooglePayOrCreditCardAvailable(): Boolean = true
override fun isSEPADebitAvailable(): Boolean = false
}
private object PayPalOnly : PaymentMethodAvailability {
override fun isPayPalAvailable(): Boolean = true
override fun isGooglePayOrCreditCardAvailable(): Boolean = false
override fun isSEPADebitAvailable(): Boolean = false
}
private object CardOnly : PaymentMethodAvailability {
override fun isPayPalAvailable(): Boolean = false
override fun isGooglePayOrCreditCardAvailable(): Boolean = true
override fun isSEPADebitAvailable(): Boolean = false
}
}

View file

@ -11,16 +11,18 @@ sealed class PaymentSourceType {
override val code: String = Codes.PAY_PAL.code
}
sealed class Stripe(override val code: String) : PaymentSourceType() {
object CreditCard : Stripe(Codes.CREDIT_CARD.code)
object GooglePay : Stripe(Codes.GOOGLE_PAY.code)
sealed class Stripe(override val code: String, val paymentMethod: String) : PaymentSourceType() {
object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD")
object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD")
object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT")
}
private enum class Codes(val code: String) {
UNKNOWN("unknown"),
PAY_PAL("paypal"),
CREDIT_CARD("credit_card"),
GOOGLE_PAY("google_pay")
GOOGLE_PAY("google_pay"),
SEPA_DEBIT("sepa_debit")
}
companion object {
@ -30,6 +32,7 @@ sealed class PaymentSourceType {
Codes.PAY_PAL -> PayPal
Codes.CREDIT_CARD -> Stripe.CreditCard
Codes.GOOGLE_PAY -> Stripe.GooglePay
Codes.SEPA_DEBIT -> Stripe.SEPADebit
}
}
}

View file

@ -59,9 +59,9 @@ class StripeApi(
data class Failure(val reason: Throwable) : CreatePaymentSourceFromCardDataResult()
}
fun createSetupIntent(): Single<CreateSetupIntentResult> {
fun createSetupIntent(sourceType: PaymentSourceType.Stripe): Single<CreateSetupIntentResult> {
return setupIntentHelper
.fetchSetupIntent()
.fetchSetupIntent(sourceType)
.map { CreateSetupIntentResult(it) }
.subscribeOn(Schedulers.io())
}
@ -84,7 +84,7 @@ class StripeApi(
}
}
fun createPaymentIntent(price: FiatMoney, level: Long): Single<CreatePaymentIntentResult> {
fun createPaymentIntent(price: FiatMoney, level: Long, sourceType: PaymentSourceType.Stripe): Single<CreatePaymentIntentResult> {
@Suppress("CascadeIf")
return if (Validation.isAmountTooSmall(price)) {
Single.just(CreatePaymentIntentResult.AmountIsTooSmall(price))
@ -95,7 +95,7 @@ class StripeApi(
Single.just<CreatePaymentIntentResult>(CreatePaymentIntentResult.CurrencyIsNotSupported(price.currency.currencyCode))
} else {
paymentIntentFetcher
.fetchPaymentIntent(price, level)
.fetchPaymentIntent(price, level, sourceType)
.map<CreatePaymentIntentResult> { CreatePaymentIntentResult.Success(it) }
}.subscribeOn(Schedulers.io())
}
@ -513,12 +513,15 @@ class StripeApi(
interface PaymentIntentFetcher {
fun fetchPaymentIntent(
price: FiatMoney,
level: Long
level: Long,
sourceType: PaymentSourceType.Stripe
): Single<StripeIntentAccessor>
}
interface SetupIntentHelper {
fun fetchSetupIntent(): Single<StripeIntentAccessor>
fun fetchSetupIntent(
sourceType: PaymentSourceType.Stripe
): Single<StripeIntentAccessor>
}
@Parcelize

View file

@ -91,8 +91,8 @@ public class DonationsService {
* @param currencyCode The currency code for the amount
* @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway.
*/
public ServiceResponse<StripeClientSecret> createDonationIntentWithAmount(String amount, String currencyCode, long level) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.createStripeOneTimePaymentIntent(currencyCode, Long.parseLong(amount), level), 200));
public ServiceResponse<StripeClientSecret> createDonationIntentWithAmount(String amount, String currencyCode, long level, String paymentMethod) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.createStripeOneTimePaymentIntent(currencyCode, paymentMethod, Long.parseLong(amount), level), 200));
}
/**
@ -205,9 +205,9 @@ public class DonationsService {
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
* but instead with the SetupIntent stripe APIs.
*/
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) {
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId, String type) {
return wrapInServiceResponse(() -> {
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize());
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize(), type);
return new Pair<>(clientSecret, 200);
});
}

View file

@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.push
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
/**
* Localized bank transfer mandate.
*/
class BankMandate @JsonCreator constructor(@JsonProperty("mandate") mandate: String)

View file

@ -277,7 +277,7 @@ public class PushServiceSocket {
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
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_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/braintree/%s";
@ -287,6 +287,7 @@ public class PushServiceSocket {
private static final String CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/confirm";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String DONATIONS_CONFIGURATION = "/v1/subscription/configuration";
private static final String BANK_MANDATE = "/v1/subscription/bank_mandate/%s";
private static final String VERIFICATION_SESSION_PATH = "/v1/verification/session";
private static final String VERIFICATION_CODE_PATH = "/v1/verification/session/%s/code";
@ -1139,8 +1140,8 @@ public class PushServiceSocket {
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
}
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level));
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, String paymentMethod, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level, paymentMethod));
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, StripeClientSecret.class);
}
@ -1196,6 +1197,17 @@ public class PushServiceSocket {
return JsonUtil.fromJson(result, DonationsConfiguration.class);
}
/**
* @param bankTransferType Valid values for bankTransferType are {SEPA_DEBIT}.
* @return localized bank mandate text for the given bankTransferType.
*/
public BankMandate getBankMandate(Locale locale, String bankTransferType) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String result = makeServiceRequestWithoutAuthentication(String.format(BANK_MANDATE, bankTransferType), "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(result, BankMandate.class);
}
public void updateSubscriptionLevel(String subscriberId, String level, String currencyCode, String idempotencyKey) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(UPDATE_SUBSCRIPTION_LEVEL, subscriberId, level, currencyCode, idempotencyKey), "PUT", "");
}
@ -1213,8 +1225,11 @@ public class PushServiceSocket {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
}
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
/**
* @param type One of CARD or SEPA_DEBIT
*/
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId, String type) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, type), "POST", "");
return JsonUtil.fromJson(response, StripeClientSecret.class);
}

View file

@ -12,9 +12,13 @@ class StripeOneTimePaymentIntentPayload {
@JsonProperty
private long level;
public StripeOneTimePaymentIntentPayload(long amount, String currency, long level) {
this.amount = amount;
this.currency = currency;
this.level = level;
@JsonProperty
private String paymentMethod;
public StripeOneTimePaymentIntentPayload(long amount, String currency, long level, String paymentMethod) {
this.amount = amount;
this.currency = currency;
this.level = level;
this.paymentMethod = paymentMethod;
}
}