Add credit card support to badge gifting.

main
Alex Hart 2022-11-16 12:10:26 -04:00
rodzic 1eb2f51398
commit a11c40e4fe
17 zmienionych plików z 464 dodań i 331 usunięć

Wyświetl plik

@ -4,10 +4,12 @@ import android.content.DialogInterface
import android.view.KeyEvent
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.subjects.PublishSubject
@ -20,8 +22,10 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
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.gateway.GatewayRequest
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.DonationErrorSource
@ -33,10 +37,11 @@ import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
@ -48,7 +53,8 @@ class GiftFlowConfirmationFragment :
),
EmojiKeyboardPageFragment.Callback,
EmojiEventListener,
EmojiSearchFragment.Callback {
EmojiSearchFragment.Callback,
DonationCheckoutDelegate.Callback {
companion object {
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
@ -67,9 +73,9 @@ class GiftFlowConfirmationFragment :
private val lifecycleDisposable = LifecycleDisposable()
private var errorDialog: DialogInterface? = null
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
private lateinit var processingDonationPaymentDialog: AlertDialog
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
private lateinit var donationPaymentComponent: DonationPaymentComponent
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
@ -81,7 +87,7 @@ class GiftFlowConfirmationFragment :
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
donationPaymentComponent = requireListener()
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog)
@ -98,13 +104,29 @@ class GiftFlowConfirmationFragment :
emojiKeyboard.setFragmentManager(childFragmentManager)
val googlePayButton = requireView().findViewById<GooglePayButton>(R.id.google_pay_button)
googlePayButton.setOnGooglePayClickListener {
viewModel.requestTokenFromGooglePay(getString(R.string.preferences__one_time))
val continueButton = requireView().findViewById<MaterialButton>(R.id.continue_button)
continueButton.setOnClickListener {
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
with(viewModel.snapshot) {
GatewayRequest(
donateToSignalType = DonateToSignalType.GIFT,
badge = giftBadge!!,
label = getString(R.string.preferences__one_time),
price = giftPrices[currency]!!.amount,
currencyCode = currency.currencyCode,
level = giftLevel!!,
recipientId = recipient!!.id,
additionalMessage = additionalMessage?.toString()
)
}
)
)
}
val textInput = requireView().findViewById<FrameLayout>(R.id.text_input)
val emojiToggle = textInput.findViewById<ImageView>(R.id.emoji_toggle)
val amountView = requireView().findViewById<TextView>(R.id.amount)
textInputViewHolder = TextInput.MultilineViewHolder(textInput, eventPublisher)
textInputViewHolder.onAttachedToWindow()
@ -165,29 +187,17 @@ class GiftFlowConfirmationFragment :
} else {
processingDonationPaymentDialog.dismiss()
}
amountView.text = FiatMoneyUtil.format(resources, state.giftPrices[state.currency]!!, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += DonationError
.getErrorsForSource(DonationErrorSource.GIFT)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { donationError ->
onPaymentError(donationError)
}
lifecycleDisposable += viewModel.events.observeOn(AndroidSchedulers.mainThread()).subscribe { donationEvent ->
when (donationEvent) {
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed()
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> Unit
is DonationEvent.SubscriptionCancellationFailed -> Unit
}
}
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
}
}
override fun onDestroyView() {
@ -196,6 +206,7 @@ class GiftFlowConfirmationFragment :
processingDonationPaymentDialog.dismiss()
debouncer.clear()
verifyingRecipientDonationPaymentDialog.dismiss()
donationCheckoutDelegate = null
}
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
@ -225,16 +236,6 @@ class GiftFlowConfirmationFragment :
}
}
private fun onPaymentConfirmed() {
val mainActivityIntent = MainActivity.clearTop(requireContext())
val conversationIntent = ConversationIntents
.createBuilder(requireContext(), viewModel.snapshot.recipient!!.id, -1L)
.withGiftBadge(viewModel.snapshot.giftBadge!!)
.build()
requireActivity().startActivities(arrayOf(mainActivityIntent, conversationIntent))
}
private fun onPaymentError(throwable: Throwable?) {
Log.w(TAG, "onPaymentError", throwable, true)
@ -276,4 +277,24 @@ class GiftFlowConfirmationFragment :
eventPublisher.onNext(TextInput.TextInputEvent.OnKeyEvent(keyEvent))
}
}
override fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
}
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest))
}
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
val mainActivityIntent = MainActivity.clearTop(requireContext())
val conversationIntent = ConversationIntents
.createBuilder(requireContext(), viewModel.snapshot.recipient!!.id, -1L)
.withGiftBadge(viewModel.snapshot.giftBadge!!)
.build()
requireActivity().startActivities(arrayOf(mainActivityIntent, conversationIntent))
}
override fun onProcessorActionProcessed() = Unit
}

Wyświetl plik

@ -9,18 +9,14 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@ -33,11 +29,7 @@ class GiftFlowStartFragment : DSLSettingsFragment(
private val viewModel: GiftFlowViewModel by viewModels(
ownerProducer = { requireActivity() },
factoryProducer = {
GiftFlowViewModel.Factory(
GiftFlowRepository(),
requireListener<DonationPaymentComponent>().stripeRepository,
OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
)
GiftFlowViewModel.Factory(GiftFlowRepository())
}
)

Wyświetl plik

@ -1,14 +1,9 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.android.gms.wallet.PaymentData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
@ -16,19 +11,9 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.signal.donations.GooglePayPaymentSource
import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.badges.gifts.Gifts
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
@ -39,13 +24,9 @@ import java.util.Currency
* Maintains state as a user works their way through the gift flow.
*/
class GiftFlowViewModel(
private val giftFlowRepository: GiftFlowRepository,
private val stripeRepository: StripeRepository,
private val oneTimeDonationRepository: OneTimeDonationRepository
private val giftFlowRepository: GiftFlowRepository
) : ViewModel() {
private var giftToPurchase: Gift? = null
private val store = RxStore(
GiftFlowState(
currency = SignalStore.donationsValues().getOneTimeCurrency()
@ -133,86 +114,6 @@ class GiftFlowViewModel(
return store.state.giftPrices.keys.map { it.currencyCode }
}
fun requestTokenFromGooglePay(label: String) {
val giftLevel = store.state.giftLevel ?: return
val giftPrice = store.state.giftPrices[store.state.currency] ?: return
val giftRecipient = store.state.recipient?.id ?: return
this.giftToPurchase = Gift(giftLevel, giftPrice)
store.update { it.copy(stage = GiftFlowState.Stage.RECIPIENT_VERIFICATION) }
disposables += giftFlowRepository.verifyRecipientIsAllowedToReceiveAGift(giftRecipient)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onComplete = {
store.update { it.copy(stage = GiftFlowState.Stage.TOKEN_REQUEST) }
stripeRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE)
},
onError = this::onPaymentFlowError
)
}
fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
val gift = giftToPurchase
giftToPurchase = null
val recipient = store.state.recipient?.id
stripeRepository.onActivityResult(
requestCode, resultCode, data, Gifts.GOOGLE_PAY_REQUEST_CODE,
object : GooglePayApi.PaymentRequestCallback {
override fun onSuccess(paymentData: PaymentData) {
if (gift != null && recipient != null) {
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) }
val continuePayment: Single<StripeIntentAccessor> = stripeRepository.continuePayment(gift.price, recipient, gift.level)
val intentAndSource: Single<Pair<StripeIntentAccessor, StripeApi.PaymentSource>> = Single.zip(continuePayment, Single.just(GooglePayPaymentSource(paymentData)), ::Pair)
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
stripeRepository.confirmPayment(paymentSource, paymentIntent, recipient)
.flatMapCompletable { Completable.complete() } // We do not currently handle 3DS for gifts.
.andThen(oneTimeDonationRepository.waitForOneTimeRedemption(gift.price, paymentIntent.intentId, recipient, store.state.additionalMessage?.toString(), gift.level))
}.subscribeBy(
onError = this@GiftFlowViewModel::onPaymentFlowError,
onComplete = {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!))
}
)
} else {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
}
}
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.GIFT, googlePayException))
}
override fun onCancelled() {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
}
}
)
}
private fun onPaymentFlowError(throwable: Throwable) {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
val donationError: DonationError = if (throwable is DonationError) {
throwable
} else {
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT)
}
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
}
private fun getLoadState(
oldState: GiftFlowState,
giftPrices: Map<Currency, FiatMoney>? = null,
@ -250,16 +151,12 @@ class GiftFlowViewModel(
}
class Factory(
private val repository: GiftFlowRepository,
private val stripeRepository: StripeRepository,
private val oneTimeDonationRepository: OneTimeDonationRepository
private val repository: GiftFlowRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(
GiftFlowViewModel(
repository,
stripeRepository,
oneTimeDonationRepository
repository
)
) as T
}

Wyświetl plik

@ -62,7 +62,10 @@ class GiftThanksSheet : DSLSettingsBottomSheetFragment() {
)
noPadTextPref(
title = DSLSettingsText.from(getString(R.string.GiftThanksSheet__youve_gifted_a_badge_to_s, recipient.getDisplayName(requireContext())))
title = DSLSettingsText.from(
getString(R.string.GiftThanksSheet__youve_gifted_a_badge_to_s, recipient.getDisplayName(requireContext())),
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(37f).toInt())

Wyświetl plik

@ -8,23 +8,17 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView
import com.google.android.gms.wallet.PaymentData
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
@ -33,16 +27,8 @@ import org.thoughtcrime.securesms.components.WrapperDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardResult
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.donate.gateway.GatewaySelectorBottomSheet
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.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
@ -57,16 +43,17 @@ import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.Projection
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Currency
/**
* Unified donation fragment which allows users to choose between monthly or one-time donations.
*/
class DonateToSignalFragment : DSLSettingsFragment(
layoutId = R.layout.donate_to_signal_fragment
) {
class DonateToSignalFragment :
DSLSettingsFragment(
layoutId = R.layout.donate_to_signal_fragment
),
DonationCheckoutDelegate.Callback {
companion object {
private val TAG = Log.tag(DonateToSignalFragment::class.java)
@ -98,18 +85,10 @@ class DonateToSignalFragment : DSLSettingsFragment(
DonateToSignalViewModel.Factory(args.startType)
})
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
R.id.donate_to_signal,
factoryProducer = {
donationPaymentComponent = requireListener()
StripePaymentInProgressViewModel.Factory(donationPaymentComponent.stripeRepository)
}
)
private val disposables = LifecycleDisposable()
private val binding by ViewBinderDelegate(DonateToSignalFragmentBinding::bind)
private lateinit var donationPaymentComponent: DonationPaymentComponent
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
private val supportTechSummary: CharSequence by lazy {
SpannableStringBuilder(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant), requireContext().getString(R.string.DonateToSignalFragment__private_messaging)))
@ -133,23 +112,7 @@ class DonateToSignalFragment : DSLSettingsFragment(
}
override fun bindAdapter(adapter: MappingAdapter) {
donationPaymentComponent = requireListener()
registerGooglePayCallback()
setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
val response: GatewayResponse = bundle.getParcelable(GatewaySelectorBottomSheet.REQUEST_KEY)!!
handleGatewaySelectionResponse(response)
}
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
handleDonationProcessorActionResult(result)
}
setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle ->
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
handleCreditCardResult(result)
}
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
val recyclerView = this.recyclerView!!
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
@ -242,6 +205,11 @@ class DonateToSignalFragment : DSLSettingsFragment(
}
}
override fun onDestroyView() {
super.onDestroyView()
donationCheckoutDelegate = null
}
private fun getConfiguration(state: DonateToSignalState): DSLConfiguration {
return configure {
space(36.dp)
@ -419,84 +387,6 @@ class DonateToSignalFragment : DSLSettingsFragment(
}
}
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
when (gatewayResponse.gateway) {
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
}
}
private fun handleCreditCardResult(creditCardResult: CreditCardResult) {
Log.d(TAG, "Received credit card information from fragment.")
stripePaymentViewModel.provideCardData(creditCardResult.creditCardData)
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, creditCardResult.gatewayRequest))
}
private fun handleDonationProcessorActionResult(result: DonationProcessorActionResult) {
when (result.status) {
DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result)
DonationProcessorActionResult.Status.FAILURE -> handleFailedDonationProcessorActionResult(result)
}
viewModel.refreshActiveSubscription()
}
private fun handleSuccessfulDonationProcessorActionResult(result: DonationProcessorActionResult) {
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
} else {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(result.request.badge))
}
}
private fun handleFailedDonationProcessorActionResult(result: DonationProcessorActionResult) {
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
.setPositiveButton(android.R.string.ok) { _, _ ->
findNavController().popBackStack()
}
.show()
} else {
Log.w(TAG, "Stripe action failed: ${result.action}")
}
}
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
label = gatewayResponse.request.label,
requestCode = gatewayResponse.request.donateToSignalType.requestCode.toInt()
)
}
private fun launchCreditCard(gatewayResponse: GatewayResponse) {
if (InAppDonations.isCreditCardAvailable()) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayResponse.request))
} else {
error("Credit cards are not currently enabled.")
}
}
private fun registerGooglePayCallback() {
disposables += donationPaymentComponent.googlePayResultPublisher.subscribeBy(
onNext = { paymentResult ->
viewModel.consumeGatewayRequestForGooglePay()?.let {
donationPaymentComponent.stripeRepository.onActivityResult(
paymentResult.requestCode,
paymentResult.resultCode,
paymentResult.data,
paymentResult.requestCode,
GooglePayRequestCallback(it)
)
}
}
)
}
private fun showErrorDialog(throwable: Throwable) {
if (errorDialog != null) {
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
@ -543,29 +433,19 @@ class DonateToSignalFragment : DSLSettingsFragment(
}
}
inner class GooglePayRequestCallback(private val request: GatewayRequest) : GooglePayApi.PaymentRequestCallback {
override fun onSuccess(paymentData: PaymentData) {
Log.d(TAG, "Successfully retrieved payment data from Google Pay", true)
stripePaymentViewModel.providePaymentData(paymentData)
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, request))
}
override fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
}
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
Log.w(TAG, "Failed to retrieve payment data from Google Pay", googlePayException, true)
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
}
val error = DonationError.getGooglePayRequestTokenError(
source = when (request.donateToSignalType) {
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
},
throwable = googlePayException
)
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(gatewayRequest.badge))
}
DonationError.routeDonationError(requireContext(), error)
}
override fun onCancelled() {
Log.d(TAG, "Cancelled Google Pay.", true)
}
override fun onProcessorActionProcessed() {
viewModel.refreshActiveSubscription()
}
}

Wyświetl plik

@ -21,48 +21,56 @@ data class DonateToSignalState(
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val badge: Badge?
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> oneTimeDonationState.badge
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription?.badge
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val canSetCurrency: Boolean
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> areFieldsEnabled
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val selectedCurrency: Currency
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectedCurrency
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedCurrency
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val selectableCurrencyCodes: List<String>
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectableCurrencyCodes
DonateToSignalType.MONTHLY -> monthlyDonationState.selectableCurrencyCodes
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val level: Int
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> 1
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription!!.level
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val canContinue: Boolean
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
val canUpdate: Boolean
get() = when (donateToSignalType) {
DonateToSignalType.ONE_TIME -> false
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid
DonateToSignalType.GIFT -> error("This flow does not support gifts")
}
data class OneTimeDonationState(

Wyświetl plik

@ -6,5 +6,6 @@ import kotlinx.parcelize.Parcelize
@Parcelize
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
ONE_TIME(16141),
MONTHLY(16142);
MONTHLY(16142),
GIFT(16143)
}

Wyświetl plik

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.manage.Su
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.subscription.LevelUpdate
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.subscription.Subscription
@ -29,7 +30,6 @@ import org.thoughtcrime.securesms.util.next
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.util.Preconditions
import java.math.BigDecimal
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
@ -57,8 +57,6 @@ class DonateToSignalViewModel(
private val _actions = PublishSubject.create<DonateToSignalAction>()
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
private var gatewayRequest: GatewayRequest? = null
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
@ -178,7 +176,8 @@ class DonateToSignalViewModel(
label = snapshot.badge!!.description,
price = amount.amount,
currencyCode = amount.currency.currencyCode,
level = snapshot.level.toLong()
level = snapshot.level.toLong(),
recipientId = Recipient.self().id
)
}
@ -186,6 +185,7 @@ class DonateToSignalViewModel(
return when (snapshot.donateToSignalType) {
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
DonateToSignalType.GIFT -> error("This ViewModel does not support gifts.")
}
}
@ -348,18 +348,6 @@ class DonateToSignalViewModel(
store.dispose()
}
fun provideGatewayRequestForGooglePay(request: GatewayRequest) {
Log.d(TAG, "Provided with a gateway request.")
Preconditions.checkState(gatewayRequest == null)
gatewayRequest = request
}
fun consumeGatewayRequestForGooglePay(): GatewayRequest? {
val request = gatewayRequest
gatewayRequest = null
return request
}
class Factory(
private val startType: DonateToSignalType,
private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),

Wyświetl plik

@ -0,0 +1,193 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import com.google.android.gms.wallet.PaymentData
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardResult
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.donate.gateway.GatewaySelectorBottomSheet
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.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener
import java.util.Currency
/**
* Abstracts out some common UI-level interactions between gift flow and normal donate flow.
*/
class DonationCheckoutDelegate(
private val fragment: Fragment,
private val callback: Callback
) : DefaultLifecycleObserver {
companion object {
private val TAG = Log.tag(DonationCheckoutDelegate::class.java)
}
private lateinit var donationPaymentComponent: DonationPaymentComponent
private val disposables = LifecycleDisposable()
private val viewModel: DonationCheckoutViewModel by fragment.viewModels()
private val stripePaymentViewModel: StripePaymentInProgressViewModel by fragment.navGraphViewModels(
R.id.donate_to_signal,
factoryProducer = {
donationPaymentComponent = fragment.requireListener()
StripePaymentInProgressViewModel.Factory(donationPaymentComponent.stripeRepository)
}
)
init {
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
}
override fun onCreate(owner: LifecycleOwner) {
disposables.bindTo(fragment.viewLifecycleOwner)
donationPaymentComponent = fragment.requireListener()
registerGooglePayCallback()
fragment.setFragmentResultListener(GatewaySelectorBottomSheet.REQUEST_KEY) { _, bundle ->
val response: GatewayResponse = bundle.getParcelable(GatewaySelectorBottomSheet.REQUEST_KEY)!!
handleGatewaySelectionResponse(response)
}
fragment.setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
handleDonationProcessorActionResult(result)
}
fragment.setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle ->
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
handleCreditCardResult(result)
}
}
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
when (gatewayResponse.gateway) {
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
}
}
private fun handleCreditCardResult(creditCardResult: CreditCardResult) {
Log.d(TAG, "Received credit card information from fragment.")
stripePaymentViewModel.provideCardData(creditCardResult.creditCardData)
callback.navigateToStripePaymentInProgress(creditCardResult.gatewayRequest)
}
private fun handleDonationProcessorActionResult(result: DonationProcessorActionResult) {
when (result.status) {
DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result)
DonationProcessorActionResult.Status.FAILURE -> handleFailedDonationProcessorActionResult(result)
}
callback.onProcessorActionProcessed()
}
private fun handleSuccessfulDonationProcessorActionResult(result: DonationProcessorActionResult) {
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
Snackbar.make(fragment.requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
} else {
callback.onPaymentComplete(result.request)
}
}
private fun handleFailedDonationProcessorActionResult(result: DonationProcessorActionResult) {
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
MaterialAlertDialogBuilder(fragment.requireContext())
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
.setPositiveButton(android.R.string.ok) { _, _ ->
fragment.findNavController().popBackStack()
}
.show()
} else {
Log.w(TAG, "Stripe action failed: ${result.action}")
}
}
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
label = gatewayResponse.request.label,
requestCode = gatewayResponse.request.donateToSignalType.requestCode.toInt()
)
}
private fun launchCreditCard(gatewayResponse: GatewayResponse) {
if (InAppDonations.isCreditCardAvailable()) {
callback.navigateToCreditCardForm(gatewayResponse.request)
} else {
error("Credit cards are not currently enabled.")
}
}
private fun registerGooglePayCallback() {
disposables += donationPaymentComponent.googlePayResultPublisher.subscribeBy(
onNext = { paymentResult ->
viewModel.consumeGatewayRequestForGooglePay()?.let {
donationPaymentComponent.stripeRepository.onActivityResult(
paymentResult.requestCode,
paymentResult.resultCode,
paymentResult.data,
paymentResult.requestCode,
GooglePayRequestCallback(it)
)
}
}
)
}
inner class GooglePayRequestCallback(private val request: GatewayRequest) : GooglePayApi.PaymentRequestCallback {
override fun onSuccess(paymentData: PaymentData) {
Log.d(TAG, "Successfully retrieved payment data from Google Pay", true)
stripePaymentViewModel.providePaymentData(paymentData)
callback.navigateToStripePaymentInProgress(request)
}
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
Log.w(TAG, "Failed to retrieve payment data from Google Pay", googlePayException, true)
val error = DonationError.getGooglePayRequestTokenError(
source = when (request.donateToSignalType) {
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
},
throwable = googlePayException
)
DonationError.routeDonationError(fragment.requireContext(), error)
}
override fun onCancelled() {
Log.d(TAG, "Cancelled Google Pay.", true)
}
}
interface Callback {
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
fun onPaymentComplete(gatewayRequest: GatewayRequest)
fun onProcessorActionProcessed()
}
}

Wyświetl plik

@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import androidx.lifecycle.ViewModel
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
import org.whispersystems.signalservice.api.util.Preconditions
/**
* State holder for the checkout flow when utilizing Google Pay.
*/
class DonationCheckoutViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(DonationCheckoutViewModel::class.java)
}
private var gatewayRequest: GatewayRequest? = null
fun provideGatewayRequestForGooglePay(request: GatewayRequest) {
Log.d(TAG, "Provided with a gateway request.")
Preconditions.checkState(gatewayRequest == null)
gatewayRequest = request
}
fun consumeGatewayRequestForGooglePay(): GatewayRequest? {
val request = gatewayRequest
gatewayRequest = null
return request
}
}

Wyświetl plik

@ -35,6 +35,9 @@ object DonationPillToggle {
DonateToSignalType.MONTHLY -> {
presentButtons(model, binding.monthly, binding.oneTime)
}
DonateToSignalType.GIFT -> {
error("Unsupported donation type.")
}
}
}

Wyświetl plik

@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
import org.thoughtcrime.securesms.recipients.RecipientId
import java.math.BigDecimal
import java.util.Currency
@ -15,7 +17,10 @@ data class GatewayRequest(
val label: String,
val price: BigDecimal,
val currencyCode: String,
val level: Long
val level: Long,
val recipientId: RecipientId,
val additionalMessage: String? = null
) : Parcelable {
@IgnoredOnParcel
val fiat: FiatMoney = FiatMoney(price, Currency.getInstance(currencyCode))
}

Wyświetl plik

@ -62,6 +62,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
when (args.request.donateToSignalType) {
DonateToSignalType.MONTHLY -> presentMonthlyText()
DonateToSignalType.ONE_TIME -> presentOneTimeText()
DonateToSignalType.GIFT -> presentGiftText()
}
space(66.dp)
@ -138,6 +139,25 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
)
}
private fun DSLConfiguration.presentGiftText() {
noPadTextPref(
title = DSLSettingsText.from(
getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
DSLSettingsText.CenterModifier,
DSLSettingsText.TitleLargeModifier
)
)
space(6.dp)
noPadTextPref(
title = DSLSettingsText.from(
R.string.GatewaySelectorBottomSheet__send_a_gift_badge,
DSLSettingsText.CenterModifier,
DSLSettingsText.BodyLargeModifier,
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
)
)
}
companion object {
const val REQUEST_KEY = "payment_checkout_mode"
}

Wyświetl plik

@ -25,9 +25,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
import org.whispersystems.signalservice.api.util.Preconditions
class StripePaymentInProgressViewModel(
@ -74,6 +72,7 @@ class StripePaymentInProgressViewModel(
val errorSource = when (request.donateToSignalType) {
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
}
val paymentSourceProvider: Single<StripeApi.PaymentSource> = resolvePaymentSourceProvider(errorSource)
@ -81,6 +80,7 @@ class StripePaymentInProgressViewModel(
return when (request.donateToSignalType) {
DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentSourceProvider, nextActionHandler)
DonateToSignalType.ONE_TIME -> proceedOneTime(request, paymentSourceProvider, nextActionHandler)
DonateToSignalType.GIFT -> proceedOneTime(request, paymentSourceProvider, nextActionHandler)
}
}
@ -164,17 +164,23 @@ class StripePaymentInProgressViewModel(
Log.w(TAG, "Beginning one-time payment pipeline...", true)
val amount = request.fiat
val recipient = Recipient.self().id
val level = SubscriptionLevels.BOOST_LEVEL.toLong()
val continuePayment: Single<StripeIntentAccessor> = stripeRepository.continuePayment(amount, recipient, level)
val continuePayment: Single<StripeIntentAccessor> = stripeRepository.continuePayment(amount, request.recipientId, request.level)
val intentAndSource: Single<Pair<StripeIntentAccessor, StripeApi.PaymentSource>> = Single.zip(continuePayment, paymentSourceProvider, ::Pair)
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
stripeRepository.confirmPayment(paymentSource, paymentIntent, recipient)
stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId)
.flatMap { nextActionHandler(it) }
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
.flatMapCompletable { oneTimeDonationRepository.waitForOneTimeRedemption(amount, paymentIntent.intentId, recipient, null, level) }
.flatMapCompletable {
oneTimeDonationRepository.waitForOneTimeRedemption(
amount,
paymentIntent.intentId,
request.recipientId,
request.additionalMessage,
request.level
)
}
}.subscribeBy(
onError = { throwable ->
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)

Wyświetl plik

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout_height="match_parent">
tools:layout_height="match_parent"
tools:viewBindingIgnore="true">
<include layout="@layout/dsl_settings_toolbar" />
@ -33,7 +33,7 @@
android:gravity="center_vertical"
android:text="@string/GiftFlowConfirmationFragment__one_time_donation"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintBottom_toTopOf="@id/google_pay_button"
app:layout_constraintBottom_toTopOf="@id/continue_button"
app:layout_constraintEnd_toStartOf="@id/amount"
app:layout_constraintStart_toStartOf="parent" />
@ -55,20 +55,21 @@
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:gravity="center_vertical"
android:textAppearance="@style/Signal.Text.Title"
app:layout_constraintBottom_toTopOf="@id/google_pay_button"
app:layout_constraintBottom_toTopOf="@id/continue_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/description"
tools:text="$10" />
<org.thoughtcrime.securesms.badges.gifts.flow.GooglePayButton
android:id="@+id/google_pay_button"
<com.google.android.material.button.MaterialButton
android:id="@+id/continue_button"
style="@style/Signal.Widget.Button.Large.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:layout_height="48dp"
android:layout_marginHorizontal="@dimen/dsl_settings_gutter"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
android:insetTop="2dp"
android:insetBottom="2dp"
android:text="@string/DonateToSignalFragment__continue"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/gift_flow"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/donate_to_signal"
app:startDestination="@id/giftFlowStartFragment">
<fragment
@ -48,5 +49,87 @@
<fragment
android:id="@+id/giftFlowConfirmationFragment"
android:name="org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowConfirmationFragment"
android:label="GiftFlowConfirmationFragment" />
android:label="GiftFlowConfirmationFragment" >
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_creditCardFragment"
app:destination="@id/creditCardFragment" />
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_stripePaymentInProgressFragment"
app:destination="@id/stripePaymentInProgressFragment" />
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_gatewaySelectorBottomSheet"
app:destination="@id/gatewaySelectorBottomSheet" />
</fragment>
<dialog
android:id="@+id/gatewaySelectorBottomSheet"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet"
android:label="gateway_selector_bottom_sheet"
tools:layout="@layout/dsl_settings_bottom_sheet">
<argument
android:name="request"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
app:nullable="false" />
</dialog>
<dialog
android:id="@+id/stripePaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
android:label="stripe_payment_in_progress_fragment"
tools:layout="@layout/stripe_payment_in_progress_fragment">
<argument
android:name="action"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction"
app:nullable="false" />
<argument
android:name="request"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
app:nullable="false" />
<action
android:id="@+id/action_stripePaymentInProgressFragment_to_stripe3dsDialogFragment"
app:destination="@id/stripe3dsDialogFragment" />
</dialog>
<fragment
android:id="@+id/creditCardFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment"
android:label="credit_card_fragment"
tools:layout="@layout/credit_card_fragment">
<argument
android:name="request"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
app:nullable="false" />
<action
android:id="@+id/action_creditCardFragment_to_yourInformationIsPrivateBottomSheet"
app:destination="@id/yourInformationIsPrivateBottomSheet" />
</fragment>
<dialog
android:id="@+id/stripe3dsDialogFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
android:label="stripe_3ds_dialog_fragment"
tools:layout="@layout/stripe_3ds_dialog_fragment">
<argument
android:name="uri"
app:argType="android.net.Uri"
app:nullable="false" />
<argument
android:name="return_uri"
app:argType="android.net.Uri"
app:nullable="false" />
</dialog>
<dialog
android:id="@+id/yourInformationIsPrivateBottomSheet"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.YourInformationIsPrivateBottomSheet"
android:label="your_information_is_private_bottom_sheet"
tools:layout="@layout/dsl_settings_bottom_sheet" />
</navigation>

Wyświetl plik

@ -5546,6 +5546,8 @@
</plurals>
<!-- Button label for paying with a credit card -->
<string name="GatewaySelectorBottomSheet__credit_or_debit_card">Credit or debit card</string>
<!-- Sheet summary when giving a gift -->
<string name="GatewaySelectorBottomSheet__send_a_gift_badge">Send a gift badge</string>
<!-- StripePaymentInProgressFragment -->
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>