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 b294bbfc4b..e70c37a79a 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 @@ -283,4 +283,8 @@ class GiftFlowConfirmationFragment : override fun onUserCancelledPaymentFlow() { findNavController().popBackStack(R.id.giftFlowConfirmationFragment, false) } + + override fun navigateToDonationPending(gatewayRequest: GatewayRequest) { + findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToDonationPendingBottomSheet(gatewayRequest)) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt index aad13be064..1a1ad08d92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/viewgift/received/ViewReceivedGiftViewModel.kt @@ -124,16 +124,16 @@ class ViewReceivedGiftViewModel( } else -> { Log.w(TAG, "Gift redemption job chain ignored due to in-progress jobs.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION)) } } } else { Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION)) } } catch (e: InterruptedException) { Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt index ed3fa0aa7e..81d359e092 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/MonthlyDonationRepository.kt @@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.badges.Badges +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.DonationErrorSource import org.thoughtcrime.securesms.database.SignalDatabase @@ -147,7 +148,10 @@ class MonthlyDonationRepository(private val donationsService: DonationsService) } } - fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long, isLongRunning: Boolean): Completable { + fun setSubscriptionLevel(gatewayRequest: GatewayRequest, isLongRunning: Boolean): Completable { + val subscriptionLevel = gatewayRequest.level.toString() + val uiSessionKey = gatewayRequest.uiSessionKey + return getOrCreateLevelUpdateOperation(subscriptionLevel) .flatMapCompletable { levelUpdateOperation -> val subscriber = SignalStore.donationsValues().requireSubscriber() @@ -193,6 +197,12 @@ class MonthlyDonationRepository(private val donationsService: DonationsService) } } + val timeoutError: DonationError = if (isLongRunning) { + DonationError.donationPending(DonationErrorSource.SUBSCRIPTION, gatewayRequest) + } else { + DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION) + } + try { if (countDownLatch.await(10, TimeUnit.SECONDS)) { when (finalJobState) { @@ -206,16 +216,16 @@ class MonthlyDonationRepository(private val donationsService: DonationsService) } else -> { Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) + it.onError(timeoutError) } } } else { Log.d(TAG, "Subscription request response job timed out.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) + it.onError(timeoutError) } } catch (e: InterruptedException) { Log.w(TAG, "Subscription request response interrupted.", e, true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) + it.onError(timeoutError) } } }.doOnError { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt index 96b54df154..7ad1f2a7dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/OneTimeDonationRepository.kt @@ -8,6 +8,7 @@ import org.signal.core.util.money.FiatMoney import org.signal.donations.PaymentSourceType import org.thoughtcrime.securesms.badges.models.Badge 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.DonationErrorSource import org.thoughtcrime.securesms.database.RecipientTable @@ -106,23 +107,19 @@ class OneTimeDonationRepository(private val donationsService: DonationsService) } fun waitForOneTimeRedemption( - price: FiatMoney, + gatewayRequest: GatewayRequest, paymentIntentId: String, - badgeRecipient: RecipientId, - additionalMessage: String?, - badgeLevel: Long, donationProcessor: DonationProcessor, - uiSessionKey: Long, isLongRunning: Boolean ): Completable { - val isBoost = badgeRecipient == Recipient.self().id + val isBoost = gatewayRequest.recipientId == Recipient.self().id val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT val waitOnRedemption = Completable.create { val donationReceiptRecord = if (isBoost) { - DonationReceiptRecord.createForBoost(price) + DonationReceiptRecord.createForBoost(gatewayRequest.fiat) } else { - DonationReceiptRecord.createForGift(price) + DonationReceiptRecord.createForGift(gatewayRequest.fiat) } val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() } @@ -133,9 +130,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService) val countDownLatch = CountDownLatch(1) var finalJobState: JobTracker.JobState? = null val chain = if (isBoost) { - BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, uiSessionKey, isLongRunning) + BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning) } else { - BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey, isLongRunning) + BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, gatewayRequest.recipientId, gatewayRequest.additionalMessage, gatewayRequest.level, donationProcessor, gatewayRequest.uiSessionKey, isLongRunning) } chain.enqueue { _, jobState -> @@ -145,6 +142,12 @@ class OneTimeDonationRepository(private val donationsService: DonationsService) } } + val timeoutError: DonationError = if (isLongRunning) { + DonationError.donationPending(donationErrorSource, gatewayRequest) + } else { + DonationError.timeoutWaitingForToken(donationErrorSource) + } + try { if (countDownLatch.await(10, TimeUnit.SECONDS)) { when (finalJobState) { @@ -158,16 +161,16 @@ class OneTimeDonationRepository(private val donationsService: DonationsService) } else -> { Log.d(TAG, "$donationTypeLabel request response job chain ignored due to in-progress jobs.", true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) + it.onError(timeoutError) } } } else { Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) + it.onError(timeoutError) } } catch (e: InterruptedException) { Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) + it.onError(timeoutError) } } 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 cfcfd68ff9..00a24f9560 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 @@ -432,4 +432,8 @@ class DonateToSignalFragment : override fun onUserCancelledPaymentFlow() { findNavController().popBackStack(R.id.donateToSignalFragment, false) } + + override fun navigateToDonationPending(gatewayRequest: GatewayRequest) { + findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(gatewayRequest)) + } } 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 a651efb9c7..7cea1bcf22 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 @@ -210,11 +210,11 @@ class DonationCheckoutDelegate( private var fragment: Fragment? = null private var errorDialog: DialogInterface? = null - private var userCancelledFlowCallback: UserCancelledFlowCallback? = null + private var errorHandlerCallback: ErrorHandlerCallback? = null - fun attach(fragment: Fragment, userCancelledFlowCallback: UserCancelledFlowCallback?, uiSessionKey: Long, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) { + fun attach(fragment: Fragment, errorHandlerCallback: ErrorHandlerCallback?, uiSessionKey: Long, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) { this.fragment = fragment - this.userCancelledFlowCallback = userCancelledFlowCallback + this.errorHandlerCallback = errorHandlerCallback val disposables = LifecycleDisposable() fragment.viewLifecycleOwner.lifecycle.addObserver(this) @@ -231,7 +231,7 @@ class DonationCheckoutDelegate( override fun onDestroy(owner: LifecycleOwner) { errorDialog?.dismiss() fragment = null - userCancelledFlowCallback = null + errorHandlerCallback = null } private fun registerErrorSource(errorSource: DonationErrorSource): Disposable { @@ -264,7 +264,7 @@ class DonationCheckoutDelegate( if (throwable is DonationError.BadgeRedemptionError.DonationPending) { Log.d(TAG, "Long-running donation is still pending.", true) - // TODO [sepa] Pop donation pending sheet. + errorHandlerCallback?.navigateToDonationPending(throwable.gatewayRequest) return } @@ -295,11 +295,12 @@ class DonationCheckoutDelegate( } } - interface UserCancelledFlowCallback { + interface ErrorHandlerCallback { fun onUserCancelledPaymentFlow() + fun navigateToDonationPending(gatewayRequest: GatewayRequest) } - interface Callback : UserCancelledFlowCallback { + interface Callback : ErrorHandlerCallback { fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt index df1557304d..d0a8469e9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt @@ -83,7 +83,7 @@ class PayPalPaymentInProgressViewModel( Log.d(TAG, "Beginning subscription update...", true) store.update { DonationProcessorStage.PAYMENT_PIPELINE } - disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, false)) + disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, false)) .subscribeBy( onComplete = { Log.w(TAG, "Completed subscription update", true) @@ -153,13 +153,9 @@ class PayPalPaymentInProgressViewModel( } .flatMapCompletable { response -> oneTimeDonationRepository.waitForOneTimeRedemption( - price = request.fiat, + gatewayRequest = request, paymentIntentId = response.paymentId, - badgeRecipient = request.recipientId, - additionalMessage = request.additionalMessage, - badgeLevel = request.level, donationProcessor = DonationProcessor.PAYPAL, - uiSessionKey = request.uiSessionKey, isLongRunning = false ) } @@ -193,7 +189,7 @@ class PayPalPaymentInProgressViewModel( .flatMapCompletable { payPalRepository.setDefaultPaymentMethod(it.paymentId) } .onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, PaymentSourceType.PayPal)) } - disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, false)) + disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request, false)) .subscribeBy( onError = { throwable -> Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt index 2293f6575f..05bf271612 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt @@ -135,7 +135,7 @@ class StripePaymentInProgressViewModel( stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe) } - val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, paymentSourceProvider.paymentSourceType.isLongRunning) + val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request, paymentSourceProvider.paymentSourceType.isLongRunning) Log.d(TAG, "Starting subscription payment pipeline...", true) store.update { DonationProcessorStage.PAYMENT_PIPELINE } @@ -199,13 +199,9 @@ class StripePaymentInProgressViewModel( .flatMap { stripeRepository.getStatusAndPaymentMethodId(it) } .flatMapCompletable { oneTimeDonationRepository.waitForOneTimeRedemption( - price = amount, + gatewayRequest = request, paymentIntentId = paymentIntent.intentId, - badgeRecipient = request.recipientId, - additionalMessage = request.additionalMessage, - badgeLevel = request.level, donationProcessor = DonationProcessor.STRIPE, - uiSessionKey = request.uiSessionKey, isLongRunning = paymentSource.type.isLongRunning ) } @@ -250,7 +246,7 @@ class StripePaymentInProgressViewModel( fun updateSubscription(request: GatewayRequest, isLongRunning: Boolean) { Log.d(TAG, "Beginning subscription update...", true) store.update { DonationProcessorStage.PAYMENT_PIPELINE } - disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, isLongRunning)) + disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, isLongRunning)) .subscribeBy( onComplete = { Log.w(TAG, "Completed subscription update", true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt index 74213e0f46..df1bf5018b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationError.kt @@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log import org.signal.donations.PaymentSourceType import org.signal.donations.StripeDeclineCode import org.signal.donations.StripeError +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) { @@ -92,7 +93,7 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : * Timeout elapsed while the user was waiting for badge redemption to complete for a long-running payment. * This is not an indication that redemption failed, just that it could take a few days to process the payment. */ - class DonationPending(source: DonationErrorSource) : BadgeRedemptionError(source, Exception("Long-running donation is still pending.")) + class DonationPending(source: DonationErrorSource, val gatewayRequest: GatewayRequest) : BadgeRedemptionError(source, Exception("Long-running donation is still pending.")) /** * Timeout elapsed while the user was waiting for badge redemption to complete. This is not an indication that @@ -216,13 +217,10 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : fun invalidCurrencyForOneTimeDonation(source: DonationErrorSource): DonationError = OneTimeDonationError.InvalidCurrencyError(source) @JvmStatic - fun timeoutWaitingForToken(source: DonationErrorSource, isLongRunning: Boolean): DonationError { - return if (isLongRunning) { - BadgeRedemptionError.DonationPending(source) - } else { - BadgeRedemptionError.TimeoutWaitingForTokenError(source) - } - } + fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source) + + @JvmStatic + fun donationPending(source: DonationErrorSource, gatewayRequest: GatewayRequest) = BadgeRedemptionError.DonationPending(source, gatewayRequest) @JvmStatic fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source)