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)
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.GIFT)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog)

Wyświetl plik

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.content.Context
import android.content.DialogInterface
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
@ -16,7 +14,6 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.LottieAnimationView
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.logging.Log
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.app.subscription.boost.Boost
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.models.CurrencySelection
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 viewModel: DonateToSignalViewModel by viewModels(factoryProducer = {
DonateToSignalViewModel.Factory(args.startType)
@ -114,7 +106,7 @@ class DonateToSignalFragment :
}
override fun bindAdapter(adapter: MappingAdapter) {
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.BOOST, DonationErrorSource.SUBSCRIPTION)
val recyclerView = this.recyclerView!!
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
@ -139,19 +131,6 @@ class DonateToSignalFragment :
DonationPillToggle.register(adapter)
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 ->
when (action) {
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) {
val animationView = getAnimationContainer(view)
val viewProjection = Projection.relativeToViewRoot(view, null)

Wyświetl plik

@ -1,5 +1,7 @@
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.setFragmentResultListener
import androidx.fragment.app.viewModels
@ -10,6 +12,8 @@ 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.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log
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.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
@ -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.StripePaymentInProgressViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener
@ -36,7 +41,9 @@ import java.util.Currency
*/
class DonationCheckoutDelegate(
private val fragment: Fragment,
private val callback: Callback
private val callback: Callback,
errorSource: DonationErrorSource,
vararg additionalSources: DonationErrorSource
) : DefaultLifecycleObserver {
companion object {
@ -57,6 +64,7 @@ class DonationCheckoutDelegate(
init {
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
ErrorHandler().attach(fragment, errorSource, *additionalSources)
}
override fun onCreate(owner: LifecycleOwner) {
@ -75,8 +83,8 @@ class DonationCheckoutDelegate(
}
fragment.setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle ->
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
handleCreditCardResult(result)
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
handleDonationProcessorActionResult(result)
}
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) {
when (result.status) {
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 {
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)

Wyświetl plik

@ -7,21 +7,30 @@ import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import org.thoughtcrime.securesms.R
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.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.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
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 viewModel: CreditCardViewModel by viewModels()
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?) {
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) {
getString(
R.string.CreditCardFragment__donation_amount_s_per_month,
@ -85,16 +116,13 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
}
binding.continueButton.setOnClickListener {
findNavController().popBackStack()
val resultBundle = bundleOf(
REQUEST_KEY to CreditCardResult(
args.request,
viewModel.getCardData()
stripePaymentViewModel.provideCardData(viewModel.getCardData())
findNavController().safeNavigate(
CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
DonationProcessorAction.PROCESS_NEW_DONATION,
args.request
)
)
setFragmentResult(REQUEST_KEY, resultBundle)
}
binding.toolbar.setNavigationOnClickListener {
@ -195,7 +223,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
}
companion object {
val REQUEST_KEY = "card.data"
const val REQUEST_KEY = "card.result"
private val NO_ERROR = ErrorState(false, -1)
}

Wyświetl plik

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

Wyświetl plik

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