mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Implement beginnings of support for iDEAL payments.
This commit is contained in:
committed by
Cody Henthorne
parent
280da481ee
commit
5e1025453a
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
@@ -263,8 +264,8 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest))
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToBankTransferMandateFragment(gatewayRequest))
|
||||
override fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse) {
|
||||
error("Unsupported operation")
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
|
||||
@@ -284,7 +285,5 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().popBackStack(R.id.giftFlowConfirmationFragment, false)
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToDonationPendingBottomSheet(gatewayRequest))
|
||||
}
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) = error("Unsupported operation")
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ object InAppDonations {
|
||||
* - Able to use PayPal and is in a region where it is able to be accepted.
|
||||
*/
|
||||
fun hasAtLeastOnePaymentMethodAvailable(): Boolean {
|
||||
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable()
|
||||
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable() || isIDEALAvailable()
|
||||
}
|
||||
|
||||
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, donateToSignalType: DonateToSignalType): Boolean {
|
||||
@@ -29,6 +29,7 @@ object InAppDonations {
|
||||
PaymentSourceType.Stripe.CreditCard -> isCreditCardAvailable()
|
||||
PaymentSourceType.Stripe.GooglePay -> isGooglePayAvailable()
|
||||
PaymentSourceType.Stripe.SEPADebit -> isSEPADebitAvailableForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.Stripe.IDEAL -> isIDEALAvailbleForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.Unknown -> false
|
||||
}
|
||||
}
|
||||
@@ -68,6 +69,13 @@ object InAppDonations {
|
||||
return FeatureFlags.sepaDebitDonations()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region which supports IDEAL transfers, based off local phone number.
|
||||
*/
|
||||
fun isIDEALAvailable(): Boolean {
|
||||
return FeatureFlags.idealDonations()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region which supports SEPA Debit transfers, based off local phone number
|
||||
* and donation type.
|
||||
@@ -75,4 +83,12 @@ object InAppDonations {
|
||||
fun isSEPADebitAvailableForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return donateToSignalType != DonateToSignalType.GIFT && FeatureFlags.sepaDebitDonations()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region which suports IDEAL transfers, based off local phone number and
|
||||
* donation type
|
||||
*/
|
||||
fun isIDEALAvailbleForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return donateToSignalType != DonateToSignalType.GIFT && FeatureFlags.idealDonations()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ object PendingOneTimeDonationSerializer {
|
||||
PaymentSourceType.PayPal -> PendingOneTimeDonation.PaymentMethodType.PAYPAL
|
||||
PaymentSourceType.Stripe.CreditCard, PaymentSourceType.Stripe.GooglePay, PaymentSourceType.Unknown -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
PaymentSourceType.Stripe.SEPADebit -> PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT
|
||||
PaymentSourceType.Stripe.IDEAL -> PendingOneTimeDonation.PaymentMethodType.IDEAL
|
||||
},
|
||||
amount = amount.toFiatValue(),
|
||||
timestamp = System.currentTimeMillis()
|
||||
|
||||
@@ -203,7 +203,7 @@ 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): Single<StatusAndPaymentMethodId> {
|
||||
fun getStatusAndPaymentMethodId(stripeIntentAccessor: StripeIntentAccessor, paymentSourceType: PaymentSourceType): Single<StatusAndPaymentMethodId> {
|
||||
return Single.fromCallable {
|
||||
when (stripeIntentAccessor.objectType) {
|
||||
StripeIntentAccessor.ObjectType.NONE -> StatusAndPaymentMethodId(StripeIntentStatus.SUCCEEDED, null)
|
||||
@@ -215,7 +215,11 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
}
|
||||
|
||||
StripeIntentAccessor.ObjectType.SETUP_INTENT -> stripeApi.getSetupIntent(stripeIntentAccessor).let {
|
||||
StatusAndPaymentMethodId(it.status, it.paymentMethod)
|
||||
if (paymentSourceType == PaymentSourceType.Stripe.IDEAL) {
|
||||
StatusAndPaymentMethodId(it.status, it.requireGeneratedSepaDebit())
|
||||
} else {
|
||||
StatusAndPaymentMethodId(it.status, it.paymentMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +261,11 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
return stripeApi.createPaymentSourceFromSEPADebitData(sepaDebitData)
|
||||
}
|
||||
|
||||
fun createIdealPaymentSource(idealData: StripeApi.IDEALData): Single<StripeApi.PaymentSource> {
|
||||
Log.d(TAG, "Creating iDEAL payment source via Stripe api...")
|
||||
return stripeApi.createPaymentSourceFromIDEALData(idealData)
|
||||
}
|
||||
|
||||
data class StatusAndPaymentMethodId(
|
||||
val status: StripeIntentStatus,
|
||||
val paymentMethod: String?
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
@@ -417,8 +418,8 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(gatewayRequest))
|
||||
override fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(gatewayResponse))
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.ga
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.details.BankTransferDetailsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
|
||||
@@ -90,13 +90,13 @@ class DonationCheckoutDelegate(
|
||||
handleDonationProcessorActionResult(result)
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(BankTransferDetailsFragment.REQUEST_KEY) { _, bundle ->
|
||||
fragment.setFragmentResultListener(BankTransferRequestKeys.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
handleDonationProcessorActionResult(result)
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(BankTransferDetailsFragment.PENDING_KEY) { _, bundle ->
|
||||
val request: GatewayRequest = bundle.getParcelableCompat(BankTransferDetailsFragment.PENDING_KEY, GatewayRequest::class.java)!!
|
||||
fragment.setFragmentResultListener(BankTransferRequestKeys.PENDING_KEY) { _, bundle ->
|
||||
val request: GatewayRequest = bundle.getParcelableCompat(BankTransferRequestKeys.PENDING_KEY, GatewayRequest::class.java)!!
|
||||
callback.navigateToDonationPending(gatewayRequest = request)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ class DonationCheckoutDelegate(
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> launchSEPADebit(gatewayResponse)
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> launchBankTransfer(gatewayResponse)
|
||||
GatewayResponse.Gateway.IDEAL -> launchBankTransfer(gatewayResponse)
|
||||
}
|
||||
} else {
|
||||
error("Unsupported combination! ${gatewayResponse.gateway} ${gatewayResponse.request.donateToSignalType}")
|
||||
@@ -167,8 +168,8 @@ class DonationCheckoutDelegate(
|
||||
callback.navigateToCreditCardForm(gatewayResponse.request)
|
||||
}
|
||||
|
||||
private fun launchSEPADebit(gatewayResponse: GatewayResponse) {
|
||||
callback.navigateToBankTransferMandate(gatewayResponse.request)
|
||||
private fun launchBankTransfer(gatewayResponse: GatewayResponse) {
|
||||
callback.navigateToBankTransferMandate(gatewayResponse)
|
||||
}
|
||||
|
||||
private fun registerGooglePayCallback() {
|
||||
@@ -325,7 +326,7 @@ class DonationCheckoutDelegate(
|
||||
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
|
||||
fun navigateToBankTransferMandate(gatewayRequest: GatewayRequest)
|
||||
fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse)
|
||||
fun onPaymentComplete(gatewayRequest: GatewayRequest)
|
||||
fun onProcessorActionProcessed()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ sealed interface GatewayOrderStrategy {
|
||||
GatewayResponse.Gateway.CREDIT_CARD,
|
||||
GatewayResponse.Gateway.PAYPAL,
|
||||
GatewayResponse.Gateway.GOOGLE_PAY,
|
||||
GatewayResponse.Gateway.SEPA_DEBIT
|
||||
GatewayResponse.Gateway.SEPA_DEBIT,
|
||||
GatewayResponse.Gateway.IDEAL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +29,8 @@ sealed interface GatewayOrderStrategy {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY,
|
||||
GatewayResponse.Gateway.PAYPAL,
|
||||
GatewayResponse.Gateway.CREDIT_CARD,
|
||||
GatewayResponse.Gateway.SEPA_DEBIT
|
||||
GatewayResponse.Gateway.SEPA_DEBIT,
|
||||
GatewayResponse.Gateway.IDEAL
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ data class GatewayResponse(val gateway: Gateway, val request: GatewayRequest) :
|
||||
GOOGLE_PAY,
|
||||
PAYPAL,
|
||||
CREDIT_CARD,
|
||||
SEPA_DEBIT;
|
||||
SEPA_DEBIT,
|
||||
IDEAL;
|
||||
|
||||
fun toPaymentSourceType(): PaymentSourceType {
|
||||
return when (this) {
|
||||
@@ -18,6 +19,7 @@ data class GatewayResponse(val gateway: Gateway, val request: GatewayRequest) :
|
||||
PAYPAL -> PaymentSourceType.PayPal
|
||||
CREDIT_CARD -> PaymentSourceType.Stripe.CreditCard
|
||||
SEPA_DEBIT -> PaymentSourceType.Stripe.SEPADebit
|
||||
IDEAL -> PaymentSourceType.Stripe.IDEAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
GatewayResponse.Gateway.PAYPAL -> renderPayPalButton(state, isFirst)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> renderCreditCardButton(state, isFirst)
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> renderSEPADebitButton(state, isFirst)
|
||||
GatewayResponse.Gateway.IDEAL -> renderIDEALButton(state, isFirst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +161,25 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState, isFirstButton: Boolean) {
|
||||
if (state.isIDEALAvailable) {
|
||||
if (!isFirstButton) {
|
||||
space(8.dp)
|
||||
}
|
||||
|
||||
// TODO [sepa] -- Final assets and copy
|
||||
tonalButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__ideal),
|
||||
icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.IDEAL, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "payment_checkout_mode"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
|
||||
import java.util.Locale
|
||||
|
||||
class GatewaySelectorRepository(
|
||||
@@ -15,9 +16,10 @@ class GatewaySelectorRepository(
|
||||
.map { configuration ->
|
||||
configuration.getAvailablePaymentMethods(currencyCode).map {
|
||||
when (it) {
|
||||
"PAYPAL" -> listOf(GatewayResponse.Gateway.PAYPAL)
|
||||
"CARD" -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
|
||||
"SEPA_DEBIT" -> listOf(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
DonationsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL)
|
||||
DonationsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
|
||||
DonationsConfiguration.SEPA_DEBIT -> listOf(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
DonationsConfiguration.IDEAL -> listOf(GatewayResponse.Gateway.IDEAL)
|
||||
else -> listOf()
|
||||
}
|
||||
}.flatten().toSet()
|
||||
|
||||
@@ -9,5 +9,6 @@ data class GatewaySelectorState(
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
val isSEPADebitAvailable: Boolean = false
|
||||
val isSEPADebitAvailable: Boolean = false,
|
||||
val isIDEALAvailable: Boolean = false
|
||||
)
|
||||
|
||||
@@ -26,7 +26,8 @@ class GatewaySelectorViewModel(
|
||||
isGooglePayAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.request.donateToSignalType),
|
||||
isCreditCardAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.request.donateToSignalType),
|
||||
isPayPalAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.PayPal, args.request.donateToSignalType),
|
||||
isSEPADebitAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.request.donateToSignalType)
|
||||
isSEPADebitAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.request.donateToSignalType),
|
||||
isIDEALAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.request.donateToSignalType)
|
||||
)
|
||||
)
|
||||
private val disposables = CompositeDisposable()
|
||||
@@ -44,7 +45,8 @@ class GatewaySelectorViewModel(
|
||||
isCreditCardAvailable = it.isCreditCardAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.CREDIT_CARD),
|
||||
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.GOOGLE_PAY),
|
||||
isPayPalAvailable = it.isPayPalAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.PAYPAL),
|
||||
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.SEPA_DEBIT),
|
||||
isIDEALAvailable = it.isIDEALAvailable && gatewaysAvailable.contains(GatewayResponse.Gateway.IDEAL)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ class StripePaymentInProgressViewModel(
|
||||
PaymentSourceType.Stripe.SEPADebit,
|
||||
stripeRepository.createSEPADebitPaymentSource(data.sepaDebitData).doAfterTerminate { clearPaymentInformation() }
|
||||
)
|
||||
is StripePaymentData.IDEAL -> PaymentSourceProvider(
|
||||
PaymentSourceType.Stripe.IDEAL,
|
||||
stripeRepository.createIdealPaymentSource(data.idealData).doAfterTerminate { clearPaymentInformation() }
|
||||
)
|
||||
else -> error("This should never happen.")
|
||||
}
|
||||
}
|
||||
@@ -120,6 +124,11 @@ class StripePaymentInProgressViewModel(
|
||||
this.stripePaymentData = StripePaymentData.SEPADebit(bankData)
|
||||
}
|
||||
|
||||
fun provideIDEALData(bankData: StripeApi.IDEALData) {
|
||||
requireNoPaymentInformation()
|
||||
this.stripePaymentData = StripePaymentData.IDEAL(bankData)
|
||||
}
|
||||
|
||||
private fun requireNoPaymentInformation() {
|
||||
require(stripePaymentData == null)
|
||||
}
|
||||
@@ -135,7 +144,7 @@ class StripePaymentInProgressViewModel(
|
||||
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
}
|
||||
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request, paymentSourceProvider.paymentSourceType.isLongRunning)
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request, paymentSourceProvider.paymentSourceType.isBankTransfer)
|
||||
|
||||
Log.d(TAG, "Starting subscription payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
@@ -145,7 +154,7 @@ class StripePaymentInProgressViewModel(
|
||||
.andThen(createAndConfirmSetupIntent)
|
||||
.flatMap { secure3DSAction ->
|
||||
nextActionHandler(secure3DSAction)
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult) }
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, paymentSourceProvider.paymentSourceType) }
|
||||
.map { (_, paymentMethod) -> paymentMethod ?: secure3DSAction.paymentMethodId!! }
|
||||
}
|
||||
.flatMapCompletable { stripeRepository.setDefaultPaymentMethod(it, paymentSourceProvider.paymentSourceType) }
|
||||
@@ -196,7 +205,7 @@ class StripePaymentInProgressViewModel(
|
||||
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
|
||||
stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId)
|
||||
.flatMap { nextActionHandler(it) }
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it, paymentSourceProvider.paymentSourceType) }
|
||||
.flatMapCompletable {
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
gatewayRequest = request,
|
||||
@@ -275,6 +284,7 @@ class StripePaymentInProgressViewModel(
|
||||
class GooglePay(val paymentData: PaymentData) : StripePaymentData
|
||||
class CreditCard(val cardData: StripeApi.CardData) : StripePaymentData
|
||||
class SEPADebit(val sepaDebitData: StripeApi.SEPADebitData) : StripePaymentData
|
||||
class IDEAL(val idealData: StripeApi.IDEALData) : StripePaymentData
|
||||
}
|
||||
|
||||
class Factory(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer
|
||||
|
||||
object BankTransferRequestKeys {
|
||||
const val REQUEST_KEY = "bank.transfer.result"
|
||||
const val PENDING_KEY = "bank.transfer.pending"
|
||||
}
|
||||
@@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
@@ -73,11 +74,6 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
*/
|
||||
class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.ErrorHandlerCallback {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "bank.transfer.result"
|
||||
const val PENDING_KEY = "bank.transfer.pending"
|
||||
}
|
||||
|
||||
private val args: BankTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModels()
|
||||
|
||||
@@ -103,7 +99,7 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
if (result.status == DonationProcessorActionResult.Status.SUCCESS) {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
setFragmentResult(REQUEST_KEY, bundle)
|
||||
setFragmentResult(BankTransferRequestKeys.REQUEST_KEY, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +162,7 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
findNavController().popBackStack()
|
||||
findNavController().popBackStack()
|
||||
|
||||
setFragmentResult(PENDING_KEY, bundleOf(PENDING_KEY to gatewayRequest))
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to gatewayRequest))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +226,7 @@ private fun BankTransferDetailsContent(
|
||||
val fullString = stringResource(id = R.string.BankTransferDetailsFragment__enter_your_bank_details, learnMore)
|
||||
|
||||
Texts.LinkifiedText(
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [alex] -- final URL
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [sepa] -- final URL
|
||||
onUrlClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.util.EnumMap
|
||||
|
||||
/**
|
||||
* Set of banks that are supported for iDEAL transfers, as listed here:
|
||||
* https://stripe.com/docs/api/payment_methods/object#payment_method_object-ideal-bank
|
||||
*/
|
||||
enum class IdealBank(
|
||||
val code: String
|
||||
) {
|
||||
ABN_AMRO("abn_amro"),
|
||||
ASN_BANK("asn_bank"),
|
||||
BUNQ("bunq"),
|
||||
ING("ing"),
|
||||
KNAB("knab"),
|
||||
N26("n26"),
|
||||
RABOBANK("rabobank"),
|
||||
REGIOBANK("regiobank"),
|
||||
REVOLUT("revolut"),
|
||||
SNS_BANK("sns_bank"),
|
||||
TRIODOS_BANK("triodos_bank"),
|
||||
VAN_LANCHOT("van_lanchot"),
|
||||
YOURSAFE("yoursafe");
|
||||
|
||||
fun getUIValues(): UIValues = bankToUIValues[this]!!
|
||||
|
||||
companion object {
|
||||
|
||||
private val bankToUIValues: Map<IdealBank, UIValues> by lazy {
|
||||
EnumMap<IdealBank, UIValues>(IdealBank::class.java).apply {
|
||||
putAll(
|
||||
arrayOf(
|
||||
ABN_AMRO to UIValues(
|
||||
name = R.string.IdealBank__abn_amro,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
ASN_BANK to UIValues(
|
||||
name = R.string.IdealBank__asn_bank,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
BUNQ to UIValues(
|
||||
name = R.string.IdealBank__bunq,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
ING to UIValues(
|
||||
name = R.string.IdealBank__ing,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
KNAB to UIValues(
|
||||
name = R.string.IdealBank__knab,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
N26 to UIValues(
|
||||
name = R.string.IdealBank__n26,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
RABOBANK to UIValues(
|
||||
name = R.string.IdealBank__rabobank,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
REGIOBANK to UIValues(
|
||||
name = R.string.IdealBank__regiobank,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
REVOLUT to UIValues(
|
||||
name = R.string.IdealBank__revolut,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
SNS_BANK to UIValues(
|
||||
name = R.string.IdealBank__sns_bank,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
TRIODOS_BANK to UIValues(
|
||||
name = R.string.IdealBank__triodos_bank,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
VAN_LANCHOT to UIValues(
|
||||
name = R.string.IdealBank__van_lanchot,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
),
|
||||
YOURSAFE to UIValues(
|
||||
name = R.string.IdealBank__yoursafe,
|
||||
icon = R.drawable.ic_person_large // TODO [sepa] -- final icon
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromCode(code: String): IdealBank {
|
||||
return values().first { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
data class UIValues(
|
||||
@StringRes val name: Int,
|
||||
@DrawableRes val icon: Int
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
|
||||
|
||||
/**
|
||||
* Dialog fragment for selecting the bank for the iDEAL donation.
|
||||
*/
|
||||
class IdealTransferDetailsBankSelectionDialogFragment : ComposeDialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val IDEAL_SELECTED_BANK = "ideal.selected.bank"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun DialogContent() {
|
||||
BankSelectionContent(
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onBankSelected = {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
setFragmentResult(
|
||||
IDEAL_SELECTED_BANK,
|
||||
bundleOf(
|
||||
IDEAL_SELECTED_BANK to it.code
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BankSelectionContentPreview() {
|
||||
BankSelectionContent(
|
||||
onNavigationClick = {},
|
||||
onBankSelected = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BankSelectionContent(
|
||||
onNavigationClick: () -> Unit,
|
||||
onBankSelected: (IdealBank) -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.IdealTransferDetailsBankSelectionDialogFragment__choose_your_bank),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_x_24)
|
||||
) { paddingValues ->
|
||||
LazyColumn(modifier = Modifier.padding(paddingValues)) {
|
||||
items(IdealBank.values()) {
|
||||
val uiValues = it.getUIValues()
|
||||
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
.clickable { onBankSelected(it) }
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter), vertical = 8.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = uiValues.icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(uiValues.name),
|
||||
modifier = Modifier.padding(start = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment for inputting necessary bank transfer information for iDEAL donation
|
||||
*/
|
||||
class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.ErrorHandlerCallback {
|
||||
|
||||
private val args: IdealTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: IdealTransferDetailsViewModel by viewModels()
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, this, args.request.uiSessionKey, errorSource)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
if (result.status == DonationProcessorActionResult.Status.SUCCESS) {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
setFragmentResult(BankTransferRequestKeys.REQUEST_KEY, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
setFragmentResultListener(IdealTransferDetailsBankSelectionDialogFragment.IDEAL_SELECTED_BANK) { _, bundle ->
|
||||
val bankCode = bundle.getString(IdealTransferDetailsBankSelectionDialogFragment.IDEAL_SELECTED_BANK)!!
|
||||
viewModel.onBankSelected(IdealBank.fromCode(bankCode))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
|
||||
val donateLabel = remember(args.request) {
|
||||
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IdealTransferDetailsContent(
|
||||
state = state,
|
||||
donateLabel = donateLabel,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onLearnMoreClick = { findNavController().navigate(IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToYourInformationIsPrivateBottomSheet()) },
|
||||
onSelectBankClick = { findNavController().navigate(IdealTransferDetailsFragmentDirections.actionIdealTransferDetailsFragmentToIdealTransferBankSelectionDialogFragment()) },
|
||||
onNameChanged = viewModel::onNameChanged,
|
||||
onEmailChanged = viewModel::onEmailChanged,
|
||||
onDonateClick = this::onDonateClick
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDonateClick() {
|
||||
stripePaymentViewModel.provideIDEALData(viewModel.state.value.asIDEALData())
|
||||
findNavController().safeNavigate(
|
||||
IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
args.request
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onUserCancelledPaymentFlow() = Unit
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
findNavController().popBackStack()
|
||||
findNavController().popBackStack()
|
||||
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to gatewayRequest))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IdealTransferDetailsContentPreview() {
|
||||
IdealTransferDetailsContent(
|
||||
state = IdealTransferDetailsState(),
|
||||
donateLabel = "Donate $5/month",
|
||||
onNavigationClick = {},
|
||||
onLearnMoreClick = {},
|
||||
onSelectBankClick = {},
|
||||
onNameChanged = {},
|
||||
onEmailChanged = {},
|
||||
onDonateClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IdealTransferDetailsContent(
|
||||
state: IdealTransferDetailsState,
|
||||
donateLabel: String,
|
||||
onNavigationClick: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onSelectBankClick: () -> Unit,
|
||||
onNameChanged: (String) -> Unit,
|
||||
onEmailChanged: (String) -> Unit,
|
||||
onDonateClick: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.GatewaySelectorBottomSheet__ideal),
|
||||
onNavigationClick = onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = CenterHorizontally,
|
||||
modifier = Modifier.padding(it)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
item {
|
||||
val learnMore = stringResource(id = R.string.IdealTransferDetailsFragment__learn_more)
|
||||
val fullString = stringResource(id = R.string.IdealTransferDetailsFragment__enter_your_bank, learnMore)
|
||||
|
||||
Texts.LinkifiedText(
|
||||
textWithUrlSpans = SpanUtil.urlSubsequence(fullString, learnMore, stringResource(id = R.string.donate_url)), // TODO [sepa] -- final URL
|
||||
onUrlClick = {
|
||||
onLearnMoreClick()
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
IdealBankSelector(
|
||||
idealBank = state.idealBank,
|
||||
onSelectBankClick = onSelectBankClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextField(
|
||||
value = state.name,
|
||||
onValueChange = onNameChanged,
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.IdealTransferDetailsFragment__name_on_bank_account))
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Words,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = { focusManager.moveFocus(FocusDirection.Down) }
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TextField(
|
||||
value = state.email,
|
||||
onValueChange = onEmailChanged,
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.IdealTransferDetailsFragment__email))
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onDonateClick() }
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = state.canProceed(),
|
||||
onClick = onDonateClick,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
Text(text = donateLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IdealBankSelectorPreview() {
|
||||
IdealBankSelector(
|
||||
idealBank = null,
|
||||
onSelectBankClick = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IdealBankSelector(
|
||||
idealBank: IdealBank?,
|
||||
onSelectBankClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uiValues: IdealBank.UIValues? = remember(idealBank) { idealBank?.getUIValues() }
|
||||
val imagePadding: Dp = if (idealBank == null) 4.dp else 0.dp
|
||||
|
||||
TextField(
|
||||
value = stringResource(id = uiValues?.name ?: R.string.IdealTransferDetailsFragment__choose_your_bank),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
readOnly = true,
|
||||
leadingIcon = {
|
||||
Image(
|
||||
painter = painterResource(id = uiValues?.icon ?: R.drawable.bank_transfer),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 12.dp)
|
||||
.size(32.dp)
|
||||
.padding(imagePadding)
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_dropdown_triangle_compat_bold_16),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledIndicatorColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
onClick = onSelectBankClick,
|
||||
role = Role.Button
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import org.signal.donations.StripeApi
|
||||
|
||||
data class IdealTransferDetailsState(
|
||||
val idealBank: IdealBank? = null,
|
||||
val name: String = "",
|
||||
val email: String = ""
|
||||
) {
|
||||
fun asIDEALData(): StripeApi.IDEALData {
|
||||
return StripeApi.IDEALData(
|
||||
bank = idealBank!!.code,
|
||||
name = name.trim(),
|
||||
email = email.trim()
|
||||
)
|
||||
}
|
||||
|
||||
fun canProceed(): Boolean {
|
||||
return idealBank != null && name.isNotBlank() && email.isNotBlank()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class IdealTransferDetailsViewModel : ViewModel() {
|
||||
|
||||
private val internalState = mutableStateOf(IdealTransferDetailsState())
|
||||
var state: State<IdealTransferDetailsState> = internalState
|
||||
|
||||
fun onNameChanged(name: String) {
|
||||
internalState.value = internalState.value.copy(
|
||||
name = name
|
||||
)
|
||||
}
|
||||
|
||||
fun onEmailChanged(email: String) {
|
||||
internalState.value = internalState.value.copy(
|
||||
email = email
|
||||
)
|
||||
}
|
||||
|
||||
fun onBankSelected(idealBank: IdealBank) {
|
||||
internalState.value = internalState.value.copy(
|
||||
idealBank = idealBank
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
@@ -91,9 +92,15 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.request)
|
||||
)
|
||||
if (args.response.gateway == GatewayResponse.Gateway.SEPA_DEBIT) {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.response.request)
|
||||
)
|
||||
} else {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToIdealTransferDetailsFragment(args.response.request)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class DonationErrorParams<V> private constructor(
|
||||
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
|
||||
PaymentSourceType.Stripe.CreditCard -> this::getTryCreditCardAgainParams
|
||||
PaymentSourceType.Stripe.GooglePay -> this::getGoToGooglePayParams
|
||||
PaymentSourceType.Stripe.SEPADebit -> this::getLearnMoreParams
|
||||
else -> this::getLearnMoreParams
|
||||
}
|
||||
|
||||
return when (declinedError.declineCode) {
|
||||
@@ -128,7 +128,7 @@ 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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -138,7 +138,7 @@ class DonationErrorParams<V> private constructor(
|
||||
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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -149,7 +149,7 @@ 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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ class DonationErrorParams<V> private constructor(
|
||||
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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -169,7 +169,7 @@ 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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -180,7 +180,7 @@ 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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -190,7 +190,7 @@ class DonationErrorParams<V> private constructor(
|
||||
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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ class DonationErrorParams<V> private constructor(
|
||||
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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -210,7 +210,7 @@ class DonationErrorParams<V> private constructor(
|
||||
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)
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ object OneTimeDonationPreference {
|
||||
PendingOneTimeDonation.PaymentMethodType.CARD -> context.getString(R.string.OneTimeDonationPreference__donation_processing)
|
||||
PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT -> context.getString(R.string.OneTimeDonationPreference__donation_pending)
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> context.getString(R.string.OneTimeDonationPreference__donation_processing)
|
||||
PendingOneTimeDonation.PaymentMethodType.IDEAL -> context.getString(R.string.OneTimeDonationPreference__donation_pending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
}
|
||||
|
||||
private boolean isForOneTimeDonation() {
|
||||
return Objects.equals(getParameters().getQueue(), ONE_TIME_QUEUE) && giftMessageId == NO_ID;
|
||||
return Objects.requireNonNull(getParameters().getQueue()).startsWith(ONE_TIME_QUEUE) && giftMessageId == NO_ID;
|
||||
}
|
||||
|
||||
private void enqueueDonationComplete(long receiptLevel) {
|
||||
|
||||
@@ -118,6 +118,7 @@ public final class FeatureFlags {
|
||||
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";
|
||||
private static final String IDEAL_DONATIONS = "android.ideal.donations";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -186,13 +187,14 @@ public final class FeatureFlags {
|
||||
INSTANT_VIDEO_PLAYBACK,
|
||||
CONVERSATION_ITEM_V2_TEXT,
|
||||
CRASH_PROMPT_CONFIG,
|
||||
BLOCK_SSE
|
||||
BLOCK_SSE,
|
||||
SEPA_DEBIT_DONATIONS,
|
||||
IDEAL_DONATIONS
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
|
||||
PHONE_NUMBER_PRIVACY,
|
||||
SEPA_DEBIT_DONATIONS
|
||||
PHONE_NUMBER_PRIVACY
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -690,6 +692,10 @@ public final class FeatureFlags {
|
||||
return getBoolean(SEPA_DEBIT_DONATIONS, Environment.IS_STAGING);
|
||||
}
|
||||
|
||||
public static boolean idealDonations() {
|
||||
return getBoolean(IDEAL_DONATIONS, Environment.IS_STAGING);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
Reference in New Issue
Block a user