Add improved handling for credit card errors.

main
Alex Hart 2022-12-06 10:26:04 -04:00 zatwierdzone przez Cody Henthorne
rodzic 643206b946
commit 40cf87307a
6 zmienionych plików z 123 dodań i 73 usunięć

Wyświetl plik

@ -87,7 +87,7 @@ class GiftFlowConfirmationFragment :
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI) keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
donationCheckoutDelegate = DonationCheckoutDelegate(this, this) donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.GIFT)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext()) processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog) .setView(R.layout.processing_payment_dialog)

Wyświetl plik

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.content.Context
import android.content.DialogInterface
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -16,7 +14,6 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieAnimationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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
@ -30,9 +27,6 @@ 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.boost.Boost import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.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.DonationErrorParams
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
@ -80,8 +74,6 @@ class DonateToSignalFragment :
} }
} }
private var errorDialog: DialogInterface? = null
private val args: DonateToSignalFragmentArgs by navArgs() private val args: DonateToSignalFragmentArgs by navArgs()
private val viewModel: DonateToSignalViewModel by viewModels(factoryProducer = { private val viewModel: DonateToSignalViewModel by viewModels(factoryProducer = {
DonateToSignalViewModel.Factory(args.startType) DonateToSignalViewModel.Factory(args.startType)
@ -114,7 +106,7 @@ class DonateToSignalFragment :
} }
override fun bindAdapter(adapter: MappingAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
donationCheckoutDelegate = DonationCheckoutDelegate(this, this) donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.BOOST, DonationErrorSource.SUBSCRIPTION)
val recyclerView = this.recyclerView!! val recyclerView = this.recyclerView!!
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
@ -139,19 +131,6 @@ class DonateToSignalFragment :
DonationPillToggle.register(adapter) DonationPillToggle.register(adapter)
disposables.bindTo(viewLifecycleOwner) disposables.bindTo(viewLifecycleOwner)
disposables += DonationError.getErrorsForSource(DonationErrorSource.BOOST)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { error ->
showErrorDialog(error)
}
disposables += DonationError.getErrorsForSource(DonationErrorSource.SUBSCRIPTION)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { error ->
showErrorDialog(error)
}
disposables += viewModel.actions.subscribe { action -> disposables += viewModel.actions.subscribe { action ->
when (action) { when (action) {
is DonateToSignalAction.DisplayCurrencySelectionDialog -> { is DonateToSignalAction.DisplayCurrencySelectionDialog -> {
@ -389,36 +368,6 @@ class DonateToSignalFragment :
} }
} }
private fun showErrorDialog(throwable: Throwable) {
if (errorDialog != null) {
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
} else {
Log.d(TAG, "Displaying donation error dialog.", true)
errorDialog = DonationErrorDialogs.show(
requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
var tryCCAgain = false
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = R.string.DeclineCode__try,
action = {
tryCCAgain = true
}
)
}
override fun onDialogDismissed() {
errorDialog = null
if (!tryCCAgain) {
findNavController().popBackStack()
}
}
}
)
}
}
private fun startAnimationAboveSelectedBoost(view: View) { private fun startAnimationAboveSelectedBoost(view: View) {
val animationView = getAnimationContainer(view) val animationView = getAnimationContainer(view)
val viewProjection = Projection.relativeToViewRoot(view, null) val viewProjection = Projection.relativeToViewRoot(view, null)

Wyświetl plik

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.content.Context
import android.content.DialogInterface
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -10,6 +12,8 @@ import androidx.navigation.navGraphViewModels
import com.google.android.gms.wallet.PaymentData 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 com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
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
@ -18,7 +22,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent 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.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment 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.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
@ -26,6 +29,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.pa
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel import org.thoughtcrime.securesms.components.settings.app.subscription.donate.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.DonationErrorParams
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
@ -36,7 +41,9 @@ import java.util.Currency
*/ */
class DonationCheckoutDelegate( class DonationCheckoutDelegate(
private val fragment: Fragment, private val fragment: Fragment,
private val callback: Callback private val callback: Callback,
errorSource: DonationErrorSource,
vararg additionalSources: DonationErrorSource
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
companion object { companion object {
@ -57,6 +64,7 @@ class DonationCheckoutDelegate(
init { init {
fragment.viewLifecycleOwner.lifecycle.addObserver(this) fragment.viewLifecycleOwner.lifecycle.addObserver(this)
ErrorHandler().attach(fragment, errorSource, *additionalSources)
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
@ -75,8 +83,8 @@ class DonationCheckoutDelegate(
} }
fragment.setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle -> fragment.setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle ->
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!! val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
handleCreditCardResult(result) handleDonationProcessorActionResult(result)
} }
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle -> fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
@ -97,12 +105,6 @@ class DonationCheckoutDelegate(
} }
} }
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) { private fun handleDonationProcessorActionResult(result: DonationProcessorActionResult) {
when (result.status) { when (result.status) {
DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result) DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result)
@ -194,6 +196,71 @@ class DonationCheckoutDelegate(
} }
} }
/**
* Shared logic for handling checkout errors.
*/
class ErrorHandler : DefaultLifecycleObserver {
private var fragment: Fragment? = null
private var errorDialog: DialogInterface? = null
fun attach(fragment: Fragment, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) {
this.fragment = fragment
val disposables = LifecycleDisposable()
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
disposables.bindTo(fragment.viewLifecycleOwner)
disposables += registerErrorSource(errorSource)
additionalSources.forEach { source ->
disposables += registerErrorSource(source)
}
}
override fun onDestroy(owner: LifecycleOwner) {
errorDialog?.dismiss()
fragment = null
}
private fun registerErrorSource(errorSource: DonationErrorSource): Disposable {
return DonationError.getErrorsForSource(errorSource)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { error ->
showErrorDialog(error)
}
}
private fun showErrorDialog(throwable: Throwable) {
if (errorDialog != null) {
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
return
}
Log.d(TAG, "Displaying donation error dialog.", true)
errorDialog = DonationErrorDialogs.show(
fragment!!.requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
var tryCCAgain = false
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit>? {
return DonationErrorParams.ErrorAction(
label = R.string.DeclineCode__try,
action = {
tryCCAgain = true
}
)
}
override fun onDialogDismissed() {
errorDialog = null
if (!tryCCAgain) {
fragment!!.findNavController().popBackStack()
}
}
}
)
}
}
interface Callback { interface Callback {
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)

Wyświetl plik

@ -7,21 +7,30 @@ import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
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 org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
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.DonateToSignalType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class CreditCardFragment : Fragment(R.layout.credit_card_fragment) { class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
@ -30,8 +39,30 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
private val args: CreditCardFragmentArgs by navArgs() private val args: CreditCardFragmentArgs by navArgs()
private val viewModel: CreditCardViewModel by viewModels() private val viewModel: CreditCardViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
R.id.donate_to_signal,
factoryProducer = {
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
}
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
}
DonationCheckoutDelegate.ErrorHandler().attach(this, errorSource)
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
if (result.status == DonationProcessorActionResult.Status.SUCCESS) {
findNavController().popBackStack()
setFragmentResult(REQUEST_KEY, bundle)
}
}
binding.title.text = if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) { binding.title.text = if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
getString( getString(
R.string.CreditCardFragment__donation_amount_s_per_month, R.string.CreditCardFragment__donation_amount_s_per_month,
@ -85,16 +116,13 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
} }
binding.continueButton.setOnClickListener { binding.continueButton.setOnClickListener {
findNavController().popBackStack() stripePaymentViewModel.provideCardData(viewModel.getCardData())
findNavController().safeNavigate(
val resultBundle = bundleOf( CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
REQUEST_KEY to CreditCardResult( DonationProcessorAction.PROCESS_NEW_DONATION,
args.request, args.request
viewModel.getCardData()
) )
) )
setFragmentResult(REQUEST_KEY, resultBundle)
} }
binding.toolbar.setNavigationOnClickListener { binding.toolbar.setNavigationOnClickListener {
@ -195,7 +223,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
} }
companion object { companion object {
val REQUEST_KEY = "card.data" const val REQUEST_KEY = "card.result"
private val NO_ERROR = ErrorState(false, -1) private val NO_ERROR = ErrorState(false, -1)
} }

Wyświetl plik

@ -130,6 +130,9 @@
<action <action
android:id="@+id/action_creditCardFragment_to_yourInformationIsPrivateBottomSheet" android:id="@+id/action_creditCardFragment_to_yourInformationIsPrivateBottomSheet"
app:destination="@id/yourInformationIsPrivateBottomSheet" /> app:destination="@id/yourInformationIsPrivateBottomSheet" />
<action
android:id="@+id/action_creditCardFragment_to_stripePaymentInProgressFragment"
app:destination="@id/stripePaymentInProgressFragment" />
</fragment> </fragment>
<dialog <dialog

Wyświetl plik

@ -111,6 +111,9 @@
<action <action
android:id="@+id/action_creditCardFragment_to_yourInformationIsPrivateBottomSheet" android:id="@+id/action_creditCardFragment_to_yourInformationIsPrivateBottomSheet"
app:destination="@id/yourInformationIsPrivateBottomSheet" /> app:destination="@id/yourInformationIsPrivateBottomSheet" />
<action
android:id="@+id/action_creditCardFragment_to_stripePaymentInProgressFragment"
app:destination="@id/stripePaymentInProgressFragment" />
</fragment> </fragment>
<dialog <dialog