mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 13:13:43 +00:00
Add SEPA API endpoints.
This commit is contained in:
committed by
Cody Henthorne
parent
f5c5a34798
commit
6279149cb8
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user