diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt index 9735757234..d46b38881a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt index 7338dde573..d6d26ac6d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt @@ -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? { - 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt index 619f1cd2ee..a31606769c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationCheckoutDelegate.kt @@ -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? { + 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt index fbc48cd4a5..89fc0279a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt @@ -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().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) } diff --git a/app/src/main/res/navigation/donate_to_signal.xml b/app/src/main/res/navigation/donate_to_signal.xml index 9775f13493..780a19a3bd 100644 --- a/app/src/main/res/navigation/donate_to_signal.xml +++ b/app/src/main/res/navigation/donate_to_signal.xml @@ -130,6 +130,9 @@ + +