Add credit card support to badge gifting.
This commit is contained in:
parent
1eb2f51398
commit
a11c40e4fe
17 changed files with 464 additions and 331 deletions
|
@ -4,10 +4,12 @@ import android.content.DialogInterface
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
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.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
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.DonationError
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
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.KeyboardPagerViewModel
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
|
||||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
|
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.Debouncer
|
||||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
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.
|
* 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,
|
EmojiKeyboardPageFragment.Callback,
|
||||||
EmojiEventListener,
|
EmojiEventListener,
|
||||||
EmojiSearchFragment.Callback {
|
EmojiSearchFragment.Callback,
|
||||||
|
DonationCheckoutDelegate.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
|
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
|
||||||
|
@ -67,9 +73,9 @@ class GiftFlowConfirmationFragment :
|
||||||
|
|
||||||
private val lifecycleDisposable = LifecycleDisposable()
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
private var errorDialog: DialogInterface? = null
|
private var errorDialog: DialogInterface? = null
|
||||||
|
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
|
||||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||||
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
|
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
|
||||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
|
||||||
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
|
||||||
|
|
||||||
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
|
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
|
||||||
|
@ -81,7 +87,7 @@ class GiftFlowConfirmationFragment :
|
||||||
|
|
||||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||||
|
|
||||||
donationPaymentComponent = requireListener()
|
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
|
||||||
|
|
||||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setView(R.layout.processing_payment_dialog)
|
.setView(R.layout.processing_payment_dialog)
|
||||||
|
@ -98,13 +104,29 @@ class GiftFlowConfirmationFragment :
|
||||||
|
|
||||||
emojiKeyboard.setFragmentManager(childFragmentManager)
|
emojiKeyboard.setFragmentManager(childFragmentManager)
|
||||||
|
|
||||||
val googlePayButton = requireView().findViewById<GooglePayButton>(R.id.google_pay_button)
|
val continueButton = requireView().findViewById<MaterialButton>(R.id.continue_button)
|
||||||
googlePayButton.setOnGooglePayClickListener {
|
continueButton.setOnClickListener {
|
||||||
viewModel.requestTokenFromGooglePay(getString(R.string.preferences__one_time))
|
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 textInput = requireView().findViewById<FrameLayout>(R.id.text_input)
|
||||||
val emojiToggle = textInput.findViewById<ImageView>(R.id.emoji_toggle)
|
val emojiToggle = textInput.findViewById<ImageView>(R.id.emoji_toggle)
|
||||||
|
val amountView = requireView().findViewById<TextView>(R.id.amount)
|
||||||
textInputViewHolder = TextInput.MultilineViewHolder(textInput, eventPublisher)
|
textInputViewHolder = TextInput.MultilineViewHolder(textInput, eventPublisher)
|
||||||
textInputViewHolder.onAttachedToWindow()
|
textInputViewHolder.onAttachedToWindow()
|
||||||
|
|
||||||
|
@ -165,29 +187,17 @@ class GiftFlowConfirmationFragment :
|
||||||
} else {
|
} else {
|
||||||
processingDonationPaymentDialog.dismiss()
|
processingDonationPaymentDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
amountView.text = FiatMoneyUtil.format(resources, state.giftPrices[state.currency]!!, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||||
|
|
||||||
lifecycleDisposable += DonationError
|
lifecycleDisposable += DonationError
|
||||||
.getErrorsForSource(DonationErrorSource.GIFT)
|
.getErrorsForSource(DonationErrorSource.GIFT)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { donationError ->
|
.subscribe { donationError ->
|
||||||
onPaymentError(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() {
|
override fun onDestroyView() {
|
||||||
|
@ -196,6 +206,7 @@ class GiftFlowConfirmationFragment :
|
||||||
processingDonationPaymentDialog.dismiss()
|
processingDonationPaymentDialog.dismiss()
|
||||||
debouncer.clear()
|
debouncer.clear()
|
||||||
verifyingRecipientDonationPaymentDialog.dismiss()
|
verifyingRecipientDonationPaymentDialog.dismiss()
|
||||||
|
donationCheckoutDelegate = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
|
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?) {
|
private fun onPaymentError(throwable: Throwable?) {
|
||||||
Log.w(TAG, "onPaymentError", throwable, true)
|
Log.w(TAG, "onPaymentError", throwable, true)
|
||||||
|
|
||||||
|
@ -276,4 +277,24 @@ class GiftFlowConfirmationFragment :
|
||||||
eventPublisher.onNext(TextInput.TextInputEvent.OnKeyEvent(keyEvent))
|
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.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
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.CurrencySelection
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||||
import org.thoughtcrime.securesms.components.settings.models.SplashImage
|
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.LifecycleDisposable
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,11 +29,7 @@ class GiftFlowStartFragment : DSLSettingsFragment(
|
||||||
private val viewModel: GiftFlowViewModel by viewModels(
|
private val viewModel: GiftFlowViewModel by viewModels(
|
||||||
ownerProducer = { requireActivity() },
|
ownerProducer = { requireActivity() },
|
||||||
factoryProducer = {
|
factoryProducer = {
|
||||||
GiftFlowViewModel.Factory(
|
GiftFlowViewModel.Factory(GiftFlowRepository())
|
||||||
GiftFlowRepository(),
|
|
||||||
requireListener<DonationPaymentComponent>().stripeRepository,
|
|
||||||
OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
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.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
@ -16,19 +11,9 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
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.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
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.contacts.paged.ContactSearchKey
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
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.
|
* Maintains state as a user works their way through the gift flow.
|
||||||
*/
|
*/
|
||||||
class GiftFlowViewModel(
|
class GiftFlowViewModel(
|
||||||
private val giftFlowRepository: GiftFlowRepository,
|
private val giftFlowRepository: GiftFlowRepository
|
||||||
private val stripeRepository: StripeRepository,
|
|
||||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private var giftToPurchase: Gift? = null
|
|
||||||
|
|
||||||
private val store = RxStore(
|
private val store = RxStore(
|
||||||
GiftFlowState(
|
GiftFlowState(
|
||||||
currency = SignalStore.donationsValues().getOneTimeCurrency()
|
currency = SignalStore.donationsValues().getOneTimeCurrency()
|
||||||
|
@ -133,86 +114,6 @@ class GiftFlowViewModel(
|
||||||
return store.state.giftPrices.keys.map { it.currencyCode }
|
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(
|
private fun getLoadState(
|
||||||
oldState: GiftFlowState,
|
oldState: GiftFlowState,
|
||||||
giftPrices: Map<Currency, FiatMoney>? = null,
|
giftPrices: Map<Currency, FiatMoney>? = null,
|
||||||
|
@ -250,16 +151,12 @@ class GiftFlowViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val repository: GiftFlowRepository,
|
private val repository: GiftFlowRepository
|
||||||
private val stripeRepository: StripeRepository,
|
|
||||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return modelClass.cast(
|
return modelClass.cast(
|
||||||
GiftFlowViewModel(
|
GiftFlowViewModel(
|
||||||
repository,
|
repository
|
||||||
stripeRepository,
|
|
||||||
oneTimeDonationRepository
|
|
||||||
)
|
)
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,10 @@ class GiftThanksSheet : DSLSettingsBottomSheetFragment() {
|
||||||
)
|
)
|
||||||
|
|
||||||
noPadTextPref(
|
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())
|
space(DimensionUnit.DP.toPixels(37f).toInt())
|
||||||
|
|
|
@ -8,23 +8,17 @@ import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.navigation.navGraphViewModels
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.lottie.LottieAnimationView
|
import com.airbnb.lottie.LottieAnimationView
|
||||||
import com.google.android.gms.wallet.PaymentData
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
|
||||||
import org.signal.core.util.dp
|
import org.signal.core.util.dp
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.signal.donations.GooglePayApi
|
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
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.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
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.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.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.DonationError
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
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.Projection
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified donation fragment which allows users to choose between monthly or one-time donations.
|
* Unified donation fragment which allows users to choose between monthly or one-time donations.
|
||||||
*/
|
*/
|
||||||
class DonateToSignalFragment : DSLSettingsFragment(
|
class DonateToSignalFragment :
|
||||||
|
DSLSettingsFragment(
|
||||||
layoutId = R.layout.donate_to_signal_fragment
|
layoutId = R.layout.donate_to_signal_fragment
|
||||||
) {
|
),
|
||||||
|
DonationCheckoutDelegate.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(DonateToSignalFragment::class.java)
|
private val TAG = Log.tag(DonateToSignalFragment::class.java)
|
||||||
|
@ -98,18 +85,10 @@ class DonateToSignalFragment : DSLSettingsFragment(
|
||||||
DonateToSignalViewModel.Factory(args.startType)
|
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 disposables = LifecycleDisposable()
|
||||||
private val binding by ViewBinderDelegate(DonateToSignalFragmentBinding::bind)
|
private val binding by ViewBinderDelegate(DonateToSignalFragmentBinding::bind)
|
||||||
|
|
||||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
|
||||||
|
|
||||||
private val supportTechSummary: CharSequence by lazy {
|
private val supportTechSummary: CharSequence by lazy {
|
||||||
SpannableStringBuilder(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant), requireContext().getString(R.string.DonateToSignalFragment__private_messaging)))
|
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) {
|
override fun bindAdapter(adapter: MappingAdapter) {
|
||||||
donationPaymentComponent = requireListener()
|
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
val recyclerView = this.recyclerView!!
|
val recyclerView = this.recyclerView!!
|
||||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
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 {
|
private fun getConfiguration(state: DonateToSignalState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
space(36.dp)
|
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) {
|
private fun showErrorDialog(throwable: Throwable) {
|
||||||
if (errorDialog != null) {
|
if (errorDialog != null) {
|
||||||
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
|
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 navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) {
|
||||||
override fun onSuccess(paymentData: PaymentData) {
|
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
|
||||||
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 onError(googlePayException: GooglePayApi.GooglePayException) {
|
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
|
||||||
Log.w(TAG, "Failed to retrieve payment data from Google Pay", googlePayException, true)
|
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
|
||||||
|
|
||||||
val error = DonationError.getGooglePayRequestTokenError(
|
|
||||||
source = when (request.donateToSignalType) {
|
|
||||||
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
|
||||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
|
|
||||||
},
|
|
||||||
throwable = googlePayException
|
|
||||||
)
|
|
||||||
|
|
||||||
DonationError.routeDonationError(requireContext(), error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancelled() {
|
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
|
||||||
Log.d(TAG, "Cancelled Google Pay.", true)
|
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(gatewayRequest.badge))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onProcessorActionProcessed() {
|
||||||
|
viewModel.refreshActiveSubscription()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,48 +21,56 @@ data class DonateToSignalState(
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
|
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||||
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress
|
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val badge: Badge?
|
val badge: Badge?
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.badge
|
DonateToSignalType.ONE_TIME -> oneTimeDonationState.badge
|
||||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription?.badge
|
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription?.badge
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val canSetCurrency: Boolean
|
val canSetCurrency: Boolean
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled
|
DonateToSignalType.ONE_TIME -> areFieldsEnabled
|
||||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedCurrency: Currency
|
val selectedCurrency: Currency
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectedCurrency
|
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectedCurrency
|
||||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedCurrency
|
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedCurrency
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectableCurrencyCodes: List<String>
|
val selectableCurrencyCodes: List<String>
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectableCurrencyCodes
|
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectableCurrencyCodes
|
||||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectableCurrencyCodes
|
DonateToSignalType.MONTHLY -> monthlyDonationState.selectableCurrencyCodes
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val level: Int
|
val level: Int
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> 1
|
DonateToSignalType.ONE_TIME -> 1
|
||||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription!!.level
|
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription!!.level
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val canContinue: Boolean
|
val canContinue: Boolean
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
val canUpdate: Boolean
|
val canUpdate: Boolean
|
||||||
get() = when (donateToSignalType) {
|
get() = when (donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> false
|
DonateToSignalType.ONE_TIME -> false
|
||||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||||
|
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class OneTimeDonationState(
|
data class OneTimeDonationState(
|
||||||
|
|
|
@ -6,5 +6,6 @@ import kotlinx.parcelize.Parcelize
|
||||||
@Parcelize
|
@Parcelize
|
||||||
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
|
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
|
||||||
ONE_TIME(16141),
|
ONE_TIME(16141),
|
||||||
MONTHLY(16142);
|
MONTHLY(16142),
|
||||||
|
GIFT(16143)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.manage.Su
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||||
import org.thoughtcrime.securesms.subscription.Subscription
|
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.thoughtcrime.securesms.util.rx.RxStore
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
import org.whispersystems.signalservice.api.util.Preconditions
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
|
@ -57,8 +57,6 @@ class DonateToSignalViewModel(
|
||||||
private val _actions = PublishSubject.create<DonateToSignalAction>()
|
private val _actions = PublishSubject.create<DonateToSignalAction>()
|
||||||
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
|
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
|
||||||
|
|
||||||
private var gatewayRequest: GatewayRequest? = null
|
|
||||||
|
|
||||||
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||||
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
|
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
|
||||||
|
@ -178,7 +176,8 @@ class DonateToSignalViewModel(
|
||||||
label = snapshot.badge!!.description,
|
label = snapshot.badge!!.description,
|
||||||
price = amount.amount,
|
price = amount.amount,
|
||||||
currencyCode = amount.currency.currencyCode,
|
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) {
|
return when (snapshot.donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||||
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
|
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
|
||||||
|
DonateToSignalType.GIFT -> error("This ViewModel does not support gifts.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,18 +348,6 @@ class DonateToSignalViewModel(
|
||||||
store.dispose()
|
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(
|
class Factory(
|
||||||
private val startType: DonateToSignalType,
|
private val startType: DonateToSignalType,
|
||||||
private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,9 @@ object DonationPillToggle {
|
||||||
DonateToSignalType.MONTHLY -> {
|
DonateToSignalType.MONTHLY -> {
|
||||||
presentButtons(model, binding.monthly, binding.oneTime)
|
presentButtons(model, binding.monthly, binding.oneTime)
|
||||||
}
|
}
|
||||||
|
DonateToSignalType.GIFT -> {
|
||||||
|
error("Unsupported donation type.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
|
|
||||||
|
@ -15,7 +17,10 @@ data class GatewayRequest(
|
||||||
val label: String,
|
val label: String,
|
||||||
val price: BigDecimal,
|
val price: BigDecimal,
|
||||||
val currencyCode: String,
|
val currencyCode: String,
|
||||||
val level: Long
|
val level: Long,
|
||||||
|
val recipientId: RecipientId,
|
||||||
|
val additionalMessage: String? = null
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
@IgnoredOnParcel
|
||||||
val fiat: FiatMoney = FiatMoney(price, Currency.getInstance(currencyCode))
|
val fiat: FiatMoney = FiatMoney(price, Currency.getInstance(currencyCode))
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||||
when (args.request.donateToSignalType) {
|
when (args.request.donateToSignalType) {
|
||||||
DonateToSignalType.MONTHLY -> presentMonthlyText()
|
DonateToSignalType.MONTHLY -> presentMonthlyText()
|
||||||
DonateToSignalType.ONE_TIME -> presentOneTimeText()
|
DonateToSignalType.ONE_TIME -> presentOneTimeText()
|
||||||
|
DonateToSignalType.GIFT -> presentGiftText()
|
||||||
}
|
}
|
||||||
|
|
||||||
space(66.dp)
|
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 {
|
companion object {
|
||||||
const val REQUEST_KEY = "payment_checkout_mode"
|
const val REQUEST_KEY = "payment_checkout_mode"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
|
||||||
import org.whispersystems.signalservice.api.util.Preconditions
|
import org.whispersystems.signalservice.api.util.Preconditions
|
||||||
|
|
||||||
class StripePaymentInProgressViewModel(
|
class StripePaymentInProgressViewModel(
|
||||||
|
@ -74,6 +72,7 @@ class StripePaymentInProgressViewModel(
|
||||||
val errorSource = when (request.donateToSignalType) {
|
val errorSource = when (request.donateToSignalType) {
|
||||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
|
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
|
||||||
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
||||||
|
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||||
}
|
}
|
||||||
|
|
||||||
val paymentSourceProvider: Single<StripeApi.PaymentSource> = resolvePaymentSourceProvider(errorSource)
|
val paymentSourceProvider: Single<StripeApi.PaymentSource> = resolvePaymentSourceProvider(errorSource)
|
||||||
|
@ -81,6 +80,7 @@ class StripePaymentInProgressViewModel(
|
||||||
return when (request.donateToSignalType) {
|
return when (request.donateToSignalType) {
|
||||||
DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentSourceProvider, nextActionHandler)
|
DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentSourceProvider, nextActionHandler)
|
||||||
DonateToSignalType.ONE_TIME -> proceedOneTime(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)
|
Log.w(TAG, "Beginning one-time payment pipeline...", true)
|
||||||
|
|
||||||
val amount = request.fiat
|
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)
|
val intentAndSource: Single<Pair<StripeIntentAccessor, StripeApi.PaymentSource>> = Single.zip(continuePayment, paymentSourceProvider, ::Pair)
|
||||||
|
|
||||||
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
|
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
|
||||||
stripeRepository.confirmPayment(paymentSource, paymentIntent, recipient)
|
stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId)
|
||||||
.flatMap { nextActionHandler(it) }
|
.flatMap { nextActionHandler(it) }
|
||||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(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(
|
}.subscribeBy(
|
||||||
onError = { throwable ->
|
onError = { throwable ->
|
||||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:viewBindingIgnore="true"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
tools:layout_height="match_parent">
|
tools:layout_height="match_parent"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<include layout="@layout/dsl_settings_toolbar" />
|
<include layout="@layout/dsl_settings_toolbar" />
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="@string/GiftFlowConfirmationFragment__one_time_donation"
|
android:text="@string/GiftFlowConfirmationFragment__one_time_donation"
|
||||||
android:textAppearance="@style/Signal.Text.Body"
|
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_constraintEnd_toStartOf="@id/amount"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
@ -55,20 +55,21 @@
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:textAppearance="@style/Signal.Text.Title"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/description"
|
app:layout_constraintStart_toEndOf="@id/description"
|
||||||
tools:text="$10" />
|
tools:text="$10" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.badges.gifts.flow.GooglePayButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/google_pay_button"
|
android:id="@+id/continue_button"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Primary"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
android:layout_marginHorizontal="@dimen/dsl_settings_gutter"
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:insetTop="2dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:insetBottom="2dp"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
android:text="@string/DonateToSignalFragment__continue"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
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">
|
app:startDestination="@id/giftFlowStartFragment">
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -48,5 +49,87 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/giftFlowConfirmationFragment"
|
android:id="@+id/giftFlowConfirmationFragment"
|
||||||
android:name="org.thoughtcrime.securesms.badges.gifts.flow.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>
|
</navigation>
|
|
@ -5546,6 +5546,8 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<!-- Button label for paying with a credit card -->
|
<!-- Button label for paying with a credit card -->
|
||||||
<string name="GatewaySelectorBottomSheet__credit_or_debit_card">Credit or debit card</string>
|
<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 -->
|
<!-- StripePaymentInProgressFragment -->
|
||||||
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
<string name="StripePaymentInProgressFragment__cancelling">Cancelling…</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue