From 5ac363232f9305422391e429bcde79d7dd5caba2 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 6 Oct 2023 13:48:18 -0400 Subject: [PATCH] Implement isLongRunning wiring for receipt redemption jobs. --- .../received/ViewReceivedGiftViewModel.kt | 6 +- .../app/internal/InternalSettingsFragment.kt | 2 +- .../subscription/MonthlyDonationRepository.kt | 10 ++-- .../subscription/OneTimeDonationRepository.kt | 13 +++-- .../donate/DonateToSignalAction.kt | 2 +- .../donate/DonateToSignalState.kt | 3 + .../donate/DonateToSignalViewModel.kt | 2 +- .../donate/DonationCheckoutDelegate.kt | 6 ++ .../PayPalPaymentInProgressViewModel.kt | 7 ++- .../stripe/StripePaymentInProgressFragment.kt | 2 +- .../StripePaymentInProgressViewModel.kt | 10 ++-- .../app/subscription/errors/DonationError.kt | 14 ++++- .../errors/DonationErrorParams.kt | 8 +++ .../manage/ActiveSubscriptionPreference.kt | 13 ++++- .../manage/ManageDonationsFragment.kt | 23 +++++++- .../manage/ManageDonationsState.kt | 2 + .../jobs/BoostReceiptRequestResponseJob.java | 56 ++++++++++++++----- .../jobs/SubscriptionKeepAliveJob.java | 7 ++- ...SubscriptionReceiptRequestResponseJob.java | 27 +++++---- .../main/res/navigation/donate_to_signal.xml | 6 ++ app/src/main/res/navigation/gift_flow.xml | 5 ++ app/src/main/res/values/strings.xml | 6 ++ .../org/signal/donations/PaymentSourceType.kt | 9 +-- .../api/subscriptions/ActiveSubscription.java | 35 +++++++++--- 24 files changed, 208 insertions(+), 66 deletions(-) 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 1a1ad08d92..aad13be064 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)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) } } } else { Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) } } catch (e: InterruptedException) { Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION, false)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 263aa5e0b7..d63201d897 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -725,7 +725,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } private fun enqueueSubscriptionRedemption() { - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(-1L).enqueue() + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(-1L, false).enqueue() } private fun enqueueSubscriptionKeepAlive() { 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 803816c09d..ed3fa0aa7e 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 @@ -147,7 +147,7 @@ class MonthlyDonationRepository(private val donationsService: DonationsService) } } - fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long): Completable { + fun setSubscriptionLevel(subscriptionLevel: String, uiSessionKey: Long, isLongRunning: Boolean): Completable { return getOrCreateLevelUpdateOperation(subscriptionLevel) .flatMapCompletable { levelUpdateOperation -> val subscriber = SignalStore.donationsValues().requireSubscriber() @@ -186,7 +186,7 @@ class MonthlyDonationRepository(private val donationsService: DonationsService) val countDownLatch = CountDownLatch(1) var finalJobState: JobTracker.JobState? = null - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey).enqueue { _, jobState -> + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey, isLongRunning).enqueue { _, jobState -> if (jobState.isComplete) { finalJobState = jobState countDownLatch.countDown() @@ -206,16 +206,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)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) } } } else { Log.d(TAG, "Subscription request response job timed out.", true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) } } catch (e: InterruptedException) { Log.w(TAG, "Subscription request response interrupted.", e, true) - it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION)) + it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.SUBSCRIPTION, isLongRunning)) } } }.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 c44fac4805..96b54df154 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 @@ -112,7 +112,8 @@ class OneTimeDonationRepository(private val donationsService: DonationsService) additionalMessage: String?, badgeLevel: Long, donationProcessor: DonationProcessor, - uiSessionKey: Long + uiSessionKey: Long, + isLongRunning: Boolean ): Completable { val isBoost = badgeRecipient == Recipient.self().id val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT @@ -132,9 +133,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) + BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, uiSessionKey, isLongRunning) } else { - BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey) + BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor, uiSessionKey, isLongRunning) } chain.enqueue { _, jobState -> @@ -157,16 +158,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)) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) } } } else { Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) } } catch (e: InterruptedException) { Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true) - it.onError(DonationError.timeoutWaitingForToken(donationErrorSource)) + it.onError(DonationError.timeoutWaitingForToken(donationErrorSource, isLongRunning)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt index 21edbabdd2..e4a1987adb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalAction.kt @@ -6,5 +6,5 @@ sealed class DonateToSignalAction { data class DisplayCurrencySelectionDialog(val donateToSignalType: DonateToSignalType, val supportedCurrencies: List) : DonateToSignalAction() data class DisplayGatewaySelectorDialog(val gatewayRequest: GatewayRequest) : DonateToSignalAction() data class CancelSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction() - data class UpdateSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction() + data class UpdateSubscription(val gatewayRequest: GatewayRequest, val isLongRunning: Boolean) : DonateToSignalAction() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt index ca57e4dac0..9fdb93540d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalState.kt @@ -73,6 +73,9 @@ data class DonateToSignalState( DonateToSignalType.GIFT -> error("This flow does not support gifts") } + val isUpdateLongRunning: Boolean + get() = monthlyDonationState.activeSubscription?.paymentMethod == ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT + data class OneTimeDonationState( val badge: Badge? = null, val selectedCurrency: Currency = SignalStore.donationsValues().getOneTimeCurrency(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt index 7625e4c4cf..77e6c2f163 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalViewModel.kt @@ -106,7 +106,7 @@ class DonateToSignalViewModel( fun updateSubscription() { val snapshot = store.state if (snapshot.areFieldsEnabled) { - _actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot))) + _actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot), snapshot.isUpdateLongRunning)) } } 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 42fb0c7aa9..a651efb9c7 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 @@ -262,6 +262,12 @@ class DonationCheckoutDelegate( return } + if (throwable is DonationError.BadgeRedemptionError.DonationPending) { + Log.d(TAG, "Long-running donation is still pending.", true) + // TODO [sepa] Pop donation pending sheet. + return + } + Log.d(TAG, "Displaying donation error dialog.", true) errorDialog = DonationErrorDialogs.show( fragment!!.requireContext(), 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 ad18145848..df1557304d 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)) + disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, false)) .subscribeBy( onComplete = { Log.w(TAG, "Completed subscription update", true) @@ -159,7 +159,8 @@ class PayPalPaymentInProgressViewModel( additionalMessage = request.additionalMessage, badgeLevel = request.level, donationProcessor = DonationProcessor.PAYPAL, - uiSessionKey = request.uiSessionKey + uiSessionKey = request.uiSessionKey, + isLongRunning = false ) } .subscribeOn(Schedulers.io()) @@ -192,7 +193,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)) + disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, 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/StripePaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt index 699f200d01..c9005a8837 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt @@ -66,7 +66,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog viewModel.processNewDonation(args.request, this::handleSecure3dsAction) } DonationProcessorAction.UPDATE_SUBSCRIPTION -> { - viewModel.updateSubscription(args.request) + viewModel.updateSubscription(args.request, args.isLongRunning) } DonationProcessorAction.CANCEL_SUBSCRIPTION -> { viewModel.cancelSubscription() 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 95cca1a980..2293f6575f 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) + val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, paymentSourceProvider.paymentSourceType.isLongRunning) Log.d(TAG, "Starting subscription payment pipeline...", true) store.update { DonationProcessorStage.PAYMENT_PIPELINE } @@ -205,7 +205,8 @@ class StripePaymentInProgressViewModel( additionalMessage = request.additionalMessage, badgeLevel = request.level, donationProcessor = DonationProcessor.STRIPE, - uiSessionKey = request.uiSessionKey + uiSessionKey = request.uiSessionKey, + isLongRunning = paymentSource.type.isLongRunning ) } }.subscribeBy( @@ -246,11 +247,10 @@ class StripePaymentInProgressViewModel( ) } - fun updateSubscription(request: GatewayRequest) { + 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)) + disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString(), request.uiSessionKey, 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 15829d9d80..74213e0f46 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 @@ -88,6 +88,12 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : * Errors that can occur during the badge redemption process. */ sealed class BadgeRedemptionError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) { + /** + * 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.")) + /** * Timeout elapsed while the user was waiting for badge redemption to complete. This is not an indication that * redemption failed, just that it is taking longer than we can reasonably show a spinner. @@ -210,7 +216,13 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : fun invalidCurrencyForOneTimeDonation(source: DonationErrorSource): DonationError = OneTimeDonationError.InvalidCurrencyError(source) @JvmStatic - fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source) + fun timeoutWaitingForToken(source: DonationErrorSource, isLongRunning: Boolean): DonationError { + return if (isLongRunning) { + BadgeRedemptionError.DonationPending(source) + } else { + BadgeRedemptionError.TimeoutWaitingForTokenError(source) + } + } @JvmStatic fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt index 33f0bed5f7..97769b420e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorParams.kt @@ -34,6 +34,14 @@ class DonationErrorParams private constructor( negativeAction = null ) + // TODO [sepa] -- This is only used for the notification, and will be rare, but we should probably have better copy here. + is DonationError.BadgeRedemptionError.DonationPending -> DonationErrorParams( + title = R.string.DonationsErrors__still_processing, + message = R.string.DonationsErrors__your_payment_is_still, + positiveAction = callback.onOk(context), + negativeAction = null + ) + is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> DonationErrorParams( title = R.string.DonationsErrors__still_processing, message = R.string.DonationsErrors__your_payment_is_still, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ActiveSubscriptionPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ActiveSubscriptionPreference.kt index 0e1340cb73..4f7548611f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ActiveSubscriptionPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ActiveSubscriptionPreference.kt @@ -33,7 +33,8 @@ object ActiveSubscriptionPreference { val renewalTimestamp: Long = -1L, val redemptionState: ManageDonationsState.SubscriptionRedemptionState, val activeSubscription: ActiveSubscription.Subscription, - val onContactSupport: () -> Unit + val onContactSupport: () -> Unit, + val onPendingClick: (FiatMoney) -> Unit ) : PreferenceModel() { override fun areItemsTheSame(newItem: Model): Boolean { return subscription.id == newItem.subscription.id @@ -57,6 +58,8 @@ object ActiveSubscriptionPreference { val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress) override fun bind(model: Model) { + itemView.setOnClickListener(null) + badge.setBadge(model.subscription.badge) title.text = context.getString( @@ -72,6 +75,7 @@ object ActiveSubscriptionPreference { when (model.redemptionState) { ManageDonationsState.SubscriptionRedemptionState.NONE -> presentRenewalState(model) + ManageDonationsState.SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER -> presentPendingBankTransferState(model) ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS -> presentInProgressState() ManageDonationsState.SubscriptionRedemptionState.FAILED -> presentFailureState(model) } @@ -89,6 +93,13 @@ object ActiveSubscriptionPreference { progress.visible = false } + private fun presentPendingBankTransferState(model: Model) { + expiry.text = context.getString(R.string.MySupportPreference__payment_pending) + badge.alpha = 0.2f + progress.visible = true + itemView.setOnClickListener { model.onPendingClick(model.price) } + } + private fun presentInProgressState() { expiry.text = context.getString(R.string.MySupportPreference__processing_transaction) badge.alpha = 0.2f diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index d489d9b3cf..aa82144be5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -6,6 +6,7 @@ import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.dp import org.signal.core.util.money.FiatMoney import org.thoughtcrime.securesms.R @@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadin import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.subscription.Subscription import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.SpanUtil @@ -199,7 +201,10 @@ class ManageDonationsFragment : requireActivity().finish() requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX)) }, - activeSubscription = activeSubscription + activeSubscription = activeSubscription, + onPendingClick = { + displayPendingDialog(it) + } ) ) } @@ -287,6 +292,22 @@ class ManageDonationsFragment : ) } + private fun displayPendingDialog(fiatMoney: FiatMoney) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.MySupportPreference__payment_pending) + .setMessage( + getString( + R.string.MySupportPreference__your_bank_transfer_of_s, + FiatMoneyUtil.format(resources, fiatMoney, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) + ) + ) + .setPositiveButton(android.R.string.ok) { _, _ -> } + .setNegativeButton(R.string.MySupportPreference__learn_more) { _, _ -> + // TODO [sepa] Where this go? + } + .show() + } + override fun onMakeAMonthlyDonation() { findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt index 7c585b4bc0..0a52406bd5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsState.kt @@ -25,6 +25,7 @@ data class ManageDonationsState( private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): SubscriptionRedemptionState? { return when { activeSubscription.isFailedPayment -> SubscriptionRedemptionState.FAILED + activeSubscription.isPendingBankTransfer -> SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER activeSubscription.isInProgress -> SubscriptionRedemptionState.IN_PROGRESS else -> null } @@ -40,6 +41,7 @@ data class ManageDonationsState( enum class SubscriptionRedemptionState { NONE, IN_PROGRESS, + IS_PENDING_BANK_TRANSFER, FAILED } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java index 5b828b2989..eb3f9729dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java @@ -41,8 +41,9 @@ public class BoostReceiptRequestResponseJob extends BaseJob { public static final String KEY = "BoostReceiptCredentialsSubmissionJob"; - private static final String BOOST_QUEUE = "BoostReceiptRedemption"; - private static final String GIFT_QUEUE = "GiftReceiptRedemption"; + private static final String BOOST_QUEUE = "BoostReceiptRedemption"; + private static final String GIFT_QUEUE = "GiftReceiptRedemption"; + private static final String LONG_RUNNING_SUFFIX = "__LongRunning"; private static final String DATA_REQUEST_BYTES = "data.request.bytes"; private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id"; @@ -50,6 +51,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob { private static final String DATA_BADGE_LEVEL = "data.badge.level"; private static final String DATA_DONATION_PROCESSOR = "data.donation.processor"; private static final String DATA_UI_SESSION_KEY = "data.ui.session.key"; + private static final String DATA_IS_LONG_RUNNING = "data.is.long.running"; private ReceiptCredentialRequestContext requestContext; @@ -58,14 +60,24 @@ public class BoostReceiptRequestResponseJob extends BaseJob { private final long badgeLevel; private final DonationProcessor donationProcessor; private final long uiSessionKey; + private final boolean isLongRunning; - private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor, long uiSessionKey) { + private static String resolveQueue(DonationErrorSource donationErrorSource, boolean isLongRunning) { + String baseQueue = donationErrorSource == DonationErrorSource.BOOST ? BOOST_QUEUE : GIFT_QUEUE; + return isLongRunning ? baseQueue + LONG_RUNNING_SUFFIX : baseQueue; + } + + private static long resolveLifespan(boolean isLongRunning) { + return isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1); + } + + private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor, long uiSessionKey, boolean isLongRunning) { return new BoostReceiptRequestResponseJob( new Parameters .Builder() .addConstraint(NetworkConstraint.KEY) - .setQueue(donationErrorSource == DonationErrorSource.BOOST ? BOOST_QUEUE : GIFT_QUEUE) - .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setQueue(resolveQueue(donationErrorSource, isLongRunning)) + .setLifespan(resolveLifespan(isLongRunning)) .setMaxAttempts(Parameters.UNLIMITED) .build(), null, @@ -73,15 +85,17 @@ public class BoostReceiptRequestResponseJob extends BaseJob { donationErrorSource, badgeLevel, donationProcessor, - uiSessionKey + uiSessionKey, + isLongRunning ); } public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId, @NonNull DonationProcessor donationProcessor, - long uiSessionKey) + long uiSessionKey, + boolean isLongRunning) { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey); + BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor, uiSessionKey, isLongRunning); DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(uiSessionKey); RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost(); MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); @@ -98,9 +112,10 @@ public class BoostReceiptRequestResponseJob extends BaseJob { @Nullable String additionalMessage, long badgeLevel, @NonNull DonationProcessor donationProcessor, - long uiSessionKey) + long uiSessionKey, + boolean isLongRunning) { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey); + BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor, uiSessionKey, isLongRunning); GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage); @@ -115,7 +130,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob { @NonNull DonationErrorSource donationErrorSource, long badgeLevel, @NonNull DonationProcessor donationProcessor, - long uiSessionKey) + long uiSessionKey, + boolean isLongRunning) { super(parameters); this.requestContext = requestContext; @@ -124,6 +140,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob { this.badgeLevel = badgeLevel; this.donationProcessor = donationProcessor; this.uiSessionKey = uiSessionKey; + this.isLongRunning = isLongRunning; } @Override @@ -132,7 +149,8 @@ public class BoostReceiptRequestResponseJob extends BaseJob { .putString(DATA_ERROR_SOURCE, donationErrorSource.serialize()) .putLong(DATA_BADGE_LEVEL, badgeLevel) .putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode()) - .putLong(DATA_UI_SESSION_KEY, uiSessionKey); + .putLong(DATA_UI_SESSION_KEY, uiSessionKey) + .putBoolean(DATA_IS_LONG_RUNNING, isLongRunning); if (requestContext != null) { builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize()); @@ -150,6 +168,15 @@ public class BoostReceiptRequestResponseJob extends BaseJob { public void onFailure() { } + @Override + public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) { + if (isLongRunning) { + return TimeUnit.DAYS.toMillis(1); + } else { + return super.getNextRunAttemptBackoff(pastAttemptCount, exception); + } + } + @Override protected void onRun() throws Exception { if (requestContext == null) { @@ -283,15 +310,16 @@ public class BoostReceiptRequestResponseJob extends BaseJob { String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode()); DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor); long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L); + boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false); try { if (data.hasString(DATA_REQUEST_BYTES)) { byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES); ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob); - return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey); + return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunning); } else { - return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey); + return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor, uiSessionKey, isLongRunning); } } catch (InvalidInputException e) { throw new IllegalStateException(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java index 5c807f8982..1103df58d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -16,6 +16,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -131,11 +132,13 @@ public class SubscriptionKeepAliveJob extends BaseJob { MultiDeviceSubscriptionSyncRequestJob.enqueue(); } + boolean isLongRunning = Objects.equals(activeSubscription.getActiveSubscription().getPaymentMethod(), ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT); if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodConversionStarted()) { Log.i(TAG, "Subscription end of period is after the conversion end of period. Storing it, generating a credential, and enqueuing the continuation job chain.", true); SignalStore.donationsValues().setSubscriptionEndOfPeriodConversionStarted(endOfCurrentPeriod); SignalStore.donationsValues().refreshSubscriptionRequestCredential(); - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L).enqueue(); + + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue(); } else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedemptionStarted()) { if (SignalStore.donationsValues().getSubscriptionRequestCredential() == null) { Log.i(TAG, "We have not started a redemption, but do not have a request credential. Possible that the subscription changed.", true); @@ -143,7 +146,7 @@ public class SubscriptionKeepAliveJob extends BaseJob { } Log.i(TAG, "We have a request credential and have not yet turned it into a redeemable token.", true); - SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L).enqueue(); + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(true, -1L, isLongRunning).enqueue(); } else if (endOfCurrentPeriod > SignalStore.donationsValues().getSubscriptionEndOfPeriodRedeemed()) { if (SignalStore.donationsValues().getSubscriptionReceiptCredential() == null) { Log.i(TAG, "We have successfully started redemption but have no stored token. Possible that the subscription changed.", true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java index 3bfc48dc66..5dff0373ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -48,36 +48,39 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { private static final String DATA_SUBSCRIBER_ID = "data.subscriber.id"; private static final String DATA_IS_FOR_KEEP_ALIVE = "data.is.for.keep.alive"; private static final String DATA_UI_SESSION_KEY = "data.ui.session.key"; + private static final String DATA_IS_LONG_RUNNING = "data.is.long.running"; public static final Object MUTEX = new Object(); private final SubscriberId subscriberId; private final boolean isForKeepAlive; private final long uiSessionKey; + private final boolean isLongRunning; - private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey) { + private static SubscriptionReceiptRequestResponseJob createJob(SubscriberId subscriberId, boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) { return new SubscriptionReceiptRequestResponseJob( new Parameters .Builder() .addConstraint(NetworkConstraint.KEY) .setQueue("ReceiptRedemption") .setMaxInstancesForQueue(1) - .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setLifespan(isLongRunning ? TimeUnit.DAYS.toMillis(14) : TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build(), subscriberId, isForKeepAlive, - uiSessionKey + uiSessionKey, + isLongRunning ); } - public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey) { - return createSubscriptionContinuationJobChain(false, uiSessionKey); + public static JobManager.Chain createSubscriptionContinuationJobChain(long uiSessionKey, boolean isLongRunning) { + return createSubscriptionContinuationJobChain(false, uiSessionKey, isLongRunning); } - public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey) { + public static JobManager.Chain createSubscriptionContinuationJobChain(boolean isForKeepAlive, long uiSessionKey, boolean isLongRunning) { Subscriber subscriber = SignalStore.donationsValues().requireSubscriber(); - SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey); + SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId(), isForKeepAlive, uiSessionKey, isLongRunning); DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(requestReceiptJob.getErrorSource(), uiSessionKey); RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forSubscription(); MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); @@ -92,19 +95,22 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters, @NonNull SubscriberId subscriberId, boolean isForKeepAlive, - long uiSessionKey) + long uiSessionKey, + boolean isLongRunning) { super(parameters); this.subscriberId = subscriberId; this.isForKeepAlive = isForKeepAlive; this.uiSessionKey = uiSessionKey; + this.isLongRunning = isLongRunning; } @Override public @Nullable byte[] serialize() { JsonJobData.Builder builder = new JsonJobData.Builder().putBlobAsString(DATA_SUBSCRIBER_ID, subscriberId.getBytes()) .putBoolean(DATA_IS_FOR_KEEP_ALIVE, isForKeepAlive) - .putLong(DATA_UI_SESSION_KEY, uiSessionKey); + .putLong(DATA_UI_SESSION_KEY, uiSessionKey) + .putBoolean(DATA_IS_LONG_RUNNING, isLongRunning); return builder.serialize(); } @@ -436,6 +442,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { String requestString = data.getStringOrDefault(DATA_REQUEST_BYTES, null); byte[] requestContextBytes = requestString != null ? Base64.decodeOrThrow(requestString) : null; long uiSessionKey = data.getLongOrDefault(DATA_UI_SESSION_KEY, -1L); + boolean isLongRunning = data.getBooleanOrDefault(DATA_IS_LONG_RUNNING, false); ReceiptCredentialRequestContext requestContext; if (requestContextBytes != null && SignalStore.donationsValues().getSubscriptionRequestCredential() == null) { @@ -448,7 +455,7 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { } } - return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey); + return new SubscriptionReceiptRequestResponseJob(parameters, subscriberId, isForKeepAlive, uiSessionKey, isLongRunning); } } } diff --git a/app/src/main/res/navigation/donate_to_signal.xml b/app/src/main/res/navigation/donate_to_signal.xml index 8a5ba34355..b7a162a528 100644 --- a/app/src/main/res/navigation/donate_to_signal.xml +++ b/app/src/main/res/navigation/donate_to_signal.xml @@ -91,6 +91,12 @@ android:name="request" app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest" app:nullable="false" /> + + + diff --git a/app/src/main/res/navigation/gift_flow.xml b/app/src/main/res/navigation/gift_flow.xml index e71499876f..652e2ed04f 100644 --- a/app/src/main/res/navigation/gift_flow.xml +++ b/app/src/main/res/navigation/gift_flow.xml @@ -95,6 +95,11 @@ android:name="request" app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest" app:nullable="false" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89d6a441b2..59badd3117 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4761,6 +4761,12 @@ Couldn\'t add badge. %1$s Please contact support. + + Payment pending + + Your bank transfer of %1$s is pending. Bank transfers usually take 1 business day to complete. + + Learn more Update Signal diff --git a/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt b/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt index 1f5efa05ef..f64c52449c 100644 --- a/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt +++ b/donations/lib/src/main/java/org/signal/donations/PaymentSourceType.kt @@ -2,6 +2,7 @@ package org.signal.donations sealed class PaymentSourceType { abstract val code: String + open val isLongRunning: Boolean = false object Unknown : PaymentSourceType() { override val code: String = Codes.UNKNOWN.code @@ -11,10 +12,10 @@ sealed class PaymentSourceType { override val code: String = Codes.PAY_PAL.code } - sealed class Stripe(override val code: String, val paymentMethod: String) : PaymentSourceType() { - object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD") - object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD") - object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT") + sealed class Stripe(override val code: String, val paymentMethod: String, override val isLongRunning: Boolean) : PaymentSourceType() { + object CreditCard : Stripe(Codes.CREDIT_CARD.code, "CARD", false) + object GooglePay : Stripe(Codes.GOOGLE_PAY.code, "CARD", false) + object SEPADebit : Stripe(Codes.SEPA_DEBIT.code, "SEPA_DEBIT", true) } private enum class Codes(val code: String) { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java index 926afdff25..fee8d1fe3b 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java @@ -13,6 +13,8 @@ import javax.annotation.Nullable; public final class ActiveSubscription { + public static final String PAYMENT_METHOD_SEPA_DEBIT = "SEPA_DEBIT"; + public static final ActiveSubscription EMPTY = new ActiveSubscription(null, null); public enum Processor { @@ -129,6 +131,10 @@ public final class ActiveSubscription { return activeSubscription != null && activeSubscription.isActive(); } + public boolean isPendingBankTransfer() { + return activeSubscription != null && Objects.equals(activeSubscription.paymentMethod, PAYMENT_METHOD_SEPA_DEBIT) && activeSubscription.paymentPending; + } + public boolean isInProgress() { return activeSubscription != null && !isActive() && !activeSubscription.isFailedPayment(); } @@ -147,6 +153,8 @@ public final class ActiveSubscription { private final boolean willCancelAtPeriodEnd; private final String status; private final Processor processor; + private final String paymentMethod; + private final boolean paymentPending; @JsonCreator public Subscription(@JsonProperty("level") int level, @@ -157,7 +165,9 @@ public final class ActiveSubscription { @JsonProperty("billingCycleAnchor") long billingCycleAnchor, @JsonProperty("cancelAtPeriodEnd") boolean willCancelAtPeriodEnd, @JsonProperty("status") String status, - @JsonProperty("processor") String processor) + @JsonProperty("processor") String processor, + @JsonProperty("paymentMethod") String paymentMethod, + @JsonProperty("paymentPending") boolean paymentPending) { this.level = level; this.currency = currency; @@ -168,6 +178,8 @@ public final class ActiveSubscription { this.willCancelAtPeriodEnd = willCancelAtPeriodEnd; this.status = status; this.processor = Processor.fromCode(processor); + this.paymentMethod = paymentMethod; + this.paymentPending = paymentPending; } public int getLevel() { @@ -222,8 +234,15 @@ public final class ActiveSubscription { return processor; } - public boolean isInProgress() { - return !isActive() && !Status.isPaymentFailed(getStatus()); + public String getPaymentMethod() { + return paymentMethod; + } + + /** + * @return Whether the latest invoice for the subscription is in a non-terminal state + */ + public boolean isPaymentPending() { + return paymentPending; } public boolean isFailedPayment() { @@ -234,16 +253,18 @@ public final class ActiveSubscription { return Status.getStatus(getStatus()) == Status.CANCELED; } - @Override public boolean equals(Object o) { + @Override + public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final Subscription that = (Subscription) o; return level == that.level && endOfCurrentPeriod == that.endOfCurrentPeriod && isActive == that.isActive && billingCycleAnchor == that.billingCycleAnchor && willCancelAtPeriodEnd == that.willCancelAtPeriodEnd && currency - .equals(that.currency) && amount.equals(that.amount) && status.equals(that.status); + .equals(that.currency) && amount.equals(that.amount) && status.equals(that.status) && Objects.equals(paymentMethod, that.paymentMethod) && paymentPending == that.paymentPending; } - @Override public int hashCode() { - return Objects.hash(level, currency, amount, endOfCurrentPeriod, isActive, billingCycleAnchor, willCancelAtPeriodEnd, status); + @Override + public int hashCode() { + return Objects.hash(level, currency, amount, endOfCurrentPeriod, isActive, billingCycleAnchor, willCancelAtPeriodEnd, status, paymentMethod, paymentPending); } }