Add SEPA API endpoints.
This commit is contained in:
parent
f5c5a34798
commit
6279149cb8
13 changed files with 140 additions and 36 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue