mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add credit card support to badge gifting.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user