diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/TerminalDonationDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/TerminalDonationDelegate.kt index 5ba2e04e68..5add1735bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/TerminalDonationDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/TerminalDonationDelegate.kt @@ -10,6 +10,9 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import org.signal.core.util.concurrent.LifecycleDisposable +import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPendingBottomSheet +import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPendingBottomSheetArgs +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragment import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragmentArgs import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue @@ -35,7 +38,7 @@ class TerminalDonationDelegate( for (donation in donations) { if (donation.isLongRunningPaymentMethod && (donation.error == null || donation.error.type != DonationErrorValue.Type.REDEMPTION)) { TerminalDonationBottomSheet.show(fragmentManager, donation) - } else { + } else if (donation.error != null) { lifecycleDisposable += badgeRepository.getBadge(donation).observeOn(AndroidSchedulers.mainThread()).subscribe { badge -> val args = ThanksForYourSupportBottomSheetDialogFragmentArgs.Builder(badge).build().toBundle() val sheet = ThanksForYourSupportBottomSheetDialogFragment() @@ -45,5 +48,12 @@ class TerminalDonationDelegate( } } } + + val verifiedMonthlyDonation: Stripe3DSData? = SignalStore.donationsValues().consumeVerifiedSubscription3DSData() + if (verifiedMonthlyDonation != null) { + DonationPendingBottomSheet().apply { + arguments = DonationPendingBottomSheetArgs.Builder(verifiedMonthlyDonation.gatewayRequest).build().toBundle() + }.show(fragmentManager, null) + } } } 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 0257caa766..ad1f568832 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 @@ -328,6 +328,8 @@ class DonateToSignalFragment : } else { if (state.monthlyDonationState.activeSubscription?.paymentMethod == ActiveSubscription.PAYMENT_METHOD_SEPA_DEBIT) { R.string.DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_monthly + } else if (state.monthlyDonationState.nonVerifiedMonthlyDonation != null) { + R.string.DonateToSignalFragment__your_ideal_payment_is_still_processing } else { R.string.DonateToSignalFragment__your_payment_is_still_being_processed_monthly } 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 130a5d2060..3ae60975bf 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 @@ -4,6 +4,7 @@ import org.signal.core.util.money.FiatMoney import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost +import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation import org.thoughtcrime.securesms.database.model.isLongRunning import org.thoughtcrime.securesms.database.model.isPending @@ -72,7 +73,7 @@ data class DonateToSignalState( val canContinue: Boolean get() = when (donateToSignalType) { DonateToSignalType.ONE_TIME -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending - DonateToSignalType.MONTHLY -> continueEnabled && !monthlyDonationState.isSubscriptionActive + DonateToSignalType.MONTHLY -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress DonateToSignalType.GIFT -> error("This flow does not support gifts") } @@ -117,6 +118,7 @@ data class DonateToSignalState( val selectedSubscription: Subscription? = null, val donationStage: DonationStage = DonationStage.INIT, val selectableCurrencyCodes: List = emptyList(), + val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null, val transactionState: TransactionState = TransactionState() ) { val isSubscriptionActive: Boolean = _activeSubscription?.isActive == true 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 4be31f6677..bd3a83dee3 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 @@ -305,24 +305,16 @@ class DonateToSignalViewModel( } private fun monitorLevelUpdateProcessing() { - val isTransactionJobInProgress: Observable = DonationRedemptionJobWatcher.watchSubscriptionRedemption().map { - when (it) { - is DonationRedemptionJobStatus.PendingExternalVerification, - DonationRedemptionJobStatus.PendingReceiptRedemption, - DonationRedemptionJobStatus.PendingReceiptRequest -> true - - DonationRedemptionJobStatus.FailedSubscription, - DonationRedemptionJobStatus.None -> false - } - } + val redemptionJobStatus: Observable = DonationRedemptionJobWatcher.watchSubscriptionRedemption() monthlyDonationDisposables += Observable - .combineLatest(isTransactionJobInProgress, LevelUpdate.isProcessing, DonateToSignalState::TransactionState) - .subscribeBy { transactionState -> + .combineLatest(redemptionJobStatus, LevelUpdate.isProcessing, ::Pair) + .subscribeBy { (jobStatus, levelUpdateProcessing) -> store.update { state -> state.copy( monthlyDonationState = state.monthlyDonationState.copy( - transactionState = transactionState + nonVerifiedMonthlyDonation = if (jobStatus is DonationRedemptionJobStatus.PendingExternalVerification) jobStatus.nonVerifiedMonthlyDonation else null, + transactionState = DonateToSignalState.TransactionState(jobStatus.isInProgress(), levelUpdateProcessing) ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt index e91e7ebac5..6cb5e037a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSData.kt @@ -29,6 +29,9 @@ data class Stripe3DSData( @IgnoredOnParcel val paymentSourceType: PaymentSourceType = PaymentSourceType.fromCode(rawPaymentSourceType) + @IgnoredOnParcel + val isLongRunning: Boolean = paymentSourceType == PaymentSourceType.Stripe.SEPADebit || (gatewayRequest.donateToSignalType == DonateToSignalType.MONTHLY && paymentSourceType.isBankTransfer) + fun toProtoBytes(): ByteArray { return ExternalLaunchTransactionState( stripeIntentAccessor = ExternalLaunchTransactionState.StripeIntentAccessor( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt index c5c4cd3872..246722893c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt @@ -231,7 +231,6 @@ private fun IdealTransferDetailsContent( onSelectBankClick = onSelectBankClick, modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp) ) } @@ -249,9 +248,11 @@ private fun IdealTransferDetailsContent( keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Down) } ), + supportingText = {}, modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp) + .padding(top = 16.dp) + .defaultMinSize(minHeight = 78.dp) ) } @@ -273,9 +274,11 @@ private fun IdealTransferDetailsContent( } } ), + supportingText = {}, modifier = Modifier .fillMaxWidth() - .padding(bottom = 16.dp) + .padding(top = 16.dp) + .defaultMinSize(minHeight = 78.dp) ) } } @@ -339,7 +342,9 @@ private fun IdealBankSelector( disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, disabledIndicatorColor = MaterialTheme.colorScheme.onSurface ), + supportingText = {}, modifier = modifier + .defaultMinSize(minHeight = 78.dp) .clickable( onClick = onSelectBankClick, role = Role.Button 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 a000712b99..b929d46af4 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 @@ -32,7 +32,7 @@ object ActiveSubscriptionPreference { val subscription: Subscription, val renewalTimestamp: Long = -1L, val redemptionState: ManageDonationsState.RedemptionState, - val activeSubscription: ActiveSubscription.Subscription, + val activeSubscription: ActiveSubscription.Subscription?, val onContactSupport: () -> Unit, val onPendingClick: (FiatMoney) -> Unit ) : PreferenceModel() { @@ -104,7 +104,7 @@ object ActiveSubscriptionPreference { } private fun presentFailureState(model: Model) { - if (model.activeSubscription.isFailedPayment || SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) { + if (model.activeSubscription?.isFailedPayment == true || SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) { presentPaymentFailureState(model) } else { presentRedemptionFailureState(model) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobStatus.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobStatus.kt index f12f67c05c..d4ac1683e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobStatus.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobStatus.kt @@ -10,36 +10,50 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDo /** * Represent the status of a donation as represented in the job system. */ -sealed interface DonationRedemptionJobStatus { +sealed class DonationRedemptionJobStatus { /** * No pending/running jobs for a donation type. */ - object None : DonationRedemptionJobStatus + object None : DonationRedemptionJobStatus() /** * Donation is pending external user verification (e.g., iDEAL). * * For one-time, pending donation data is provided via the job data as it is not in the store yet. */ - class PendingExternalVerification(val pendingOneTimeDonation: PendingOneTimeDonation? = null) : DonationRedemptionJobStatus + class PendingExternalVerification( + val pendingOneTimeDonation: PendingOneTimeDonation? = null, + val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null + ) : DonationRedemptionJobStatus() /** * Donation is at the receipt request status. * * For one-time donations, pending donation data available via the store. */ - object PendingReceiptRequest : DonationRedemptionJobStatus + object PendingReceiptRequest : DonationRedemptionJobStatus() /** * Donation is at the receipt redemption status. * * For one-time donations, pending donation data available via the store. */ - object PendingReceiptRedemption : DonationRedemptionJobStatus + object PendingReceiptRedemption : DonationRedemptionJobStatus() /** * Representation of a failed subscription job chain derived from no pending/running jobs and * a failure state in the store. */ - object FailedSubscription : DonationRedemptionJobStatus + object FailedSubscription : DonationRedemptionJobStatus() + + fun isInProgress(): Boolean { + return when (this) { + is PendingExternalVerification, + PendingReceiptRedemption, + PendingReceiptRequest -> true + + FailedSubscription, + None -> false + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt index fc5a351c2a..e776f66ac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/DonationRedemptionJobWatcher.kt @@ -1,7 +1,10 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.manage +import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Observable import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData +import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob @@ -23,9 +26,24 @@ object DonationRedemptionJobWatcher { fun watchSubscriptionRedemption(): Observable = watch(RedemptionType.SUBSCRIPTION) + @JvmStatic + @WorkerThread + fun getSubscriptionRedemptionJobStatus(): DonationRedemptionJobStatus { + return getDonationRedemptionJobStatus(RedemptionType.SUBSCRIPTION) + } + fun watchOneTimeRedemption(): Observable = watch(RedemptionType.ONE_TIME) - private fun watch(redemptionType: RedemptionType): Observable = Observable.interval(0, 5, TimeUnit.SECONDS).map { + private fun watch(redemptionType: RedemptionType): Observable { + return Observable + .interval(0, 5, TimeUnit.SECONDS) + .map { + getDonationRedemptionJobStatus(redemptionType) + } + .distinctUntilChanged() + } + + private fun getDonationRedemptionJobStatus(redemptionType: RedemptionType): DonationRedemptionJobStatus { val queue = when (redemptionType) { RedemptionType.SUBSCRIPTION -> DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE RedemptionType.ONE_TIME -> DonationReceiptRedemptionJob.ONE_TIME_QUEUE @@ -55,27 +73,20 @@ object DonationRedemptionJobWatcher { val jobSpec: JobSpec? = externalLaunchJobSpec ?: redemptionJobSpec ?: receiptJobSpec - if (redemptionType == RedemptionType.SUBSCRIPTION && jobSpec == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) { + return if (redemptionType == RedemptionType.SUBSCRIPTION && jobSpec == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) { DonationRedemptionJobStatus.FailedSubscription } else { - jobSpec?.toDonationRedemptionStatus() ?: DonationRedemptionJobStatus.None + jobSpec?.toDonationRedemptionStatus(redemptionType) ?: DonationRedemptionJobStatus.None } - }.distinctUntilChanged() + } - private fun JobSpec.toDonationRedemptionStatus(): DonationRedemptionJobStatus { + private fun JobSpec.toDonationRedemptionStatus(redemptionType: RedemptionType): DonationRedemptionJobStatus { return when (factoryKey) { ExternalLaunchDonationJob.KEY -> { val stripe3DSData = ExternalLaunchDonationJob.Factory.parseSerializedData(serializedData!!) DonationRedemptionJobStatus.PendingExternalVerification( - pendingOneTimeDonation = DonationSerializationHelper.createPendingOneTimeDonationProto( - badge = stripe3DSData.gatewayRequest.badge, - paymentSourceType = stripe3DSData.paymentSourceType, - amount = stripe3DSData.gatewayRequest.fiat - ).copy( - timestamp = createTime, - pendingVerification = true, - checkedVerification = runAttempt > 0 - ) + pendingOneTimeDonation = pendingOneTimeDonation(redemptionType, stripe3DSData), + nonVerifiedMonthlyDonation = nonVerifiedMonthlyDonation(redemptionType, stripe3DSData) ) } @@ -89,4 +100,33 @@ object DonationRedemptionJobWatcher { } } } + + private fun JobSpec.pendingOneTimeDonation(redemptionType: RedemptionType, stripe3DSData: Stripe3DSData): PendingOneTimeDonation? { + if (redemptionType != RedemptionType.ONE_TIME) { + return null + } + + return DonationSerializationHelper.createPendingOneTimeDonationProto( + badge = stripe3DSData.gatewayRequest.badge, + paymentSourceType = stripe3DSData.paymentSourceType, + amount = stripe3DSData.gatewayRequest.fiat + ).copy( + timestamp = createTime, + pendingVerification = true, + checkedVerification = runAttempt > 0 + ) + } + + private fun JobSpec.nonVerifiedMonthlyDonation(redemptionType: RedemptionType, stripe3DSData: Stripe3DSData): NonVerifiedMonthlyDonation? { + if (redemptionType != RedemptionType.SUBSCRIPTION) { + return null + } + + return NonVerifiedMonthlyDonation( + timestamp = createTime, + price = stripe3DSData.gatewayRequest.fiat, + level = stripe3DSData.gatewayRequest.level.toInt(), + checkedVerification = runAttempt > 0 + ) + } } 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 0aff041fa8..75fafea881 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 @@ -99,7 +99,19 @@ class ManageDonationsFragment : viewModel.state.observe(viewLifecycleOwner) { state -> adapter.submitList(getConfiguration(state).toMappingModelList()) - if (state.pendingOneTimeDonation?.pendingVerification == true && + if (state.nonVerifiedMonthlyDonation?.checkedVerification == true && + !alertedIdealDonations.contains(state.nonVerifiedMonthlyDonation.timestamp) + ) { + alertedIdealDonations += state.nonVerifiedMonthlyDonation.timestamp + + val amount = FiatMoneyUtil.format(resources, state.nonVerifiedMonthlyDonation.price) + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.ManageDonationsFragment__couldnt_confirm_donation) + .setMessage(getString(R.string.ManageDonationsFragment__your_monthly_s_donation_couldnt_be_confirmed, amount)) + .setPositiveButton(android.R.string.ok, null) + .show() + } else if (state.pendingOneTimeDonation?.pendingVerification == true && state.pendingOneTimeDonation.checkedVerification && !alertedIdealDonations.contains(state.pendingOneTimeDonation.timestamp) ) { @@ -170,6 +182,13 @@ class ManageDonationsFragment : } else { customPref(IndeterminateLoadingCircle) } + } else if (state.nonVerifiedMonthlyDonation != null) { + val subscription: Subscription? = state.availableSubscriptions.firstOrNull { it.level == state.nonVerifiedMonthlyDonation.level } + if (subscription != null) { + presentNonVerifiedSubscriptionSettings(state.nonVerifiedMonthlyDonation, subscription, state) + } else { + customPref(IndeterminateLoadingCircle) + } } else if (state.hasOneTimeBadge || state.pendingOneTimeDonation != null) { presentActiveOneTimeDonorSettings(state) } else { @@ -262,6 +281,25 @@ class ManageDonationsFragment : } } + private fun DSLConfiguration.presentNonVerifiedSubscriptionSettings( + nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation, + subscription: Subscription, + state: ManageDonationsState + ) { + presentSubscriptionSettingsWithState(state) { + customPref( + ActiveSubscriptionPreference.Model( + price = nonVerifiedMonthlyDonation.price, + subscription = subscription, + redemptionState = ManageDonationsState.RedemptionState.IN_PROGRESS, + onContactSupport = {}, + activeSubscription = null, + onPendingClick = {} + ) + ) + } + } + private fun DSLConfiguration.presentSubscriptionSettingsWithState( state: ManageDonationsState, subscriptionBlock: DSLConfiguration.() -> Unit 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 9d009b79c5..03c31bb8c8 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 @@ -12,6 +12,7 @@ data class ManageDonationsState( val subscriptionTransactionState: TransactionState = TransactionState.Init, val availableSubscriptions: List = emptyList(), val pendingOneTimeDonation: PendingOneTimeDonation? = null, + val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null, private val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt index c282e41fb3..98d2e901fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsViewModel.kt @@ -79,6 +79,7 @@ class ManageDonationsViewModel( disposables += DonationRedemptionJobWatcher.watchSubscriptionRedemption().subscribeBy { redemptionStatus -> store.update { manageDonationsState -> manageDonationsState.copy( + nonVerifiedMonthlyDonation = if (redemptionStatus is DonationRedemptionJobStatus.PendingExternalVerification) redemptionStatus.nonVerifiedMonthlyDonation else null, subscriptionRedemptionState = mapStatusToRedemptionState(redemptionStatus) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/NonVerifiedMonthlyDonation.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/NonVerifiedMonthlyDonation.kt new file mode 100644 index 0000000000..3f4e6e64d9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/NonVerifiedMonthlyDonation.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.subscription.manage + +import org.signal.core.util.money.FiatMoney + +/** + * Represents a monthly donation via iDEAL that is still pending user verification in + * their 3rd party app. + */ +data class NonVerifiedMonthlyDonation( + val timestamp: Long, + val price: FiatMoney, + val level: Int, + val checkedVerification: Boolean +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java index 9a0b9944ca..6f43327080 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -151,6 +151,11 @@ public class DonationReceiptRedemptionJob extends BaseJob { @Override public void onFailure() { + if (getInputData() == null) { + Log.d(TAG, "No input data, assuming upstream job in chain failed and properly set error state. Failing without side effects."); + return; + } + if (isForSubscription()) { Log.d(TAG, "Marking subscription failure", true); SignalStore.donationsValues().markSubscriptionRedemptionFailed(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt index 83b05e8e53..874caafb71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ExternalLaunchDonationJob.kt @@ -122,15 +122,31 @@ class ExternalLaunchDonationJob private constructor( override fun onFailure() { if (donationError != null) { - SignalStore.donationsValues().setPendingOneTimeDonation( - DonationSerializationHelper.createPendingOneTimeDonationProto( - stripe3DSData.gatewayRequest.badge, - stripe3DSData.paymentSourceType, - stripe3DSData.gatewayRequest.fiat - ).copy( - error = donationError?.toDonationErrorValue() - ) - ) + when (stripe3DSData.gatewayRequest.donateToSignalType) { + DonateToSignalType.ONE_TIME -> { + SignalStore.donationsValues().setPendingOneTimeDonation( + DonationSerializationHelper.createPendingOneTimeDonationProto( + stripe3DSData.gatewayRequest.badge, + stripe3DSData.paymentSourceType, + stripe3DSData.gatewayRequest.fiat + ).copy( + error = donationError?.toDonationErrorValue() + ) + ) + } + + DonateToSignalType.MONTHLY -> { + SignalStore.donationsValues().appendToTerminalDonationQueue( + TerminalDonationQueue.TerminalDonation( + level = stripe3DSData.gatewayRequest.level, + isLongRunningPaymentMethod = stripe3DSData.isLongRunning, + error = donationError?.toDonationErrorValue() + ) + ) + } + + else -> Log.w(TAG, "Job failed with donation error for type: ${stripe3DSData.gatewayRequest.donateToSignalType}") + } } } @@ -215,6 +231,7 @@ class ExternalLaunchDonationJob private constructor( if (updateSubscriptionLevelResponse.status in listOf(200, 204)) { Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${updateSubscriptionLevelResponse.status}", true) SignalStore.donationsValues().updateLocalStateForLocalSubscribe() + SignalStore.donationsValues().setVerifiedSubscription3DSData(stripe3DSData) SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() } else { @@ -263,8 +280,7 @@ class ExternalLaunchDonationJob private constructor( SignalStore.donationsValues().appendToTerminalDonationQueue( TerminalDonationQueue.TerminalDonation( level = stripe3DSData.gatewayRequest.level, - isLongRunningPaymentMethod = stripe3DSData.gatewayRequest.donateToSignalType == DonateToSignalType.MONTHLY && stripe3DSData.paymentSourceType.isBankTransfer || - stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.SEPADebit, + isLongRunningPaymentMethod = stripe3DSData.isLongRunning, error = DonationErrorValue( DonationErrorValue.Type.PAYMENT, code = serviceResponse.status.toString() 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 13bfac1af3..1c77e4b95f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -5,6 +5,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobStatus; +import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher; import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Job; @@ -120,6 +122,12 @@ public class SubscriptionKeepAliveJob extends BaseJob { return; } + DonationRedemptionJobStatus status = DonationRedemptionJobWatcher.getSubscriptionRedemptionJobStatus(); + if (status != DonationRedemptionJobStatus.None.INSTANCE && status != DonationRedemptionJobStatus.FailedSubscription.INSTANCE) { + Log.i(TAG, "Already trying to redeem donation, current status: " + status.getClass().getSimpleName(), true); + return; + } + final long endOfCurrentPeriod = activeSubscription.getActiveSubscription().getEndOfCurrentPeriod(); if (endOfCurrentPeriod > SignalStore.donationsValues().getLastEndOfPeriod()) { Log.i(TAG, diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index 038d3f4a66..1e2d2add15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -132,6 +132,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign * completing a 3DS prompt or iDEAL prompt. */ private const val PENDING_3DS_DATA = "pending.3ds.data" + + /** + * Data about a monthly donation that required external verification and said verification was successful. + * Needed to show donation pending sheet after returning to Signal. + */ + private const val VERIFIED_IDEAL_SUBSCRIPTION_3DS_DATA = "donation.verified_ideal_subscription_3ds_data" } override fun onFirstEverAppLaunch() = Unit @@ -584,6 +590,27 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign } } + fun consumeVerifiedSubscription3DSData(): Stripe3DSData? { + synchronized(this) { + val data = getBlob(VERIFIED_IDEAL_SUBSCRIPTION_3DS_DATA, null)?.let { + Stripe3DSData.fromProtoBytes(it, -1) + } + + setVerifiedSubscription3DSData(null) + return data + } + } + + fun setVerifiedSubscription3DSData(stripe3DSData: Stripe3DSData?) { + synchronized(this) { + if (stripe3DSData != null) { + putBlob(VERIFIED_IDEAL_SUBSCRIPTION_3DS_DATA, stripe3DSData.toProtoBytes()) + } else { + remove(VERIFIED_IDEAL_SUBSCRIPTION_3DS_DATA) + } + } + } + private fun generateRequestCredential(): ReceiptCredentialRequestContext { Log.d(TAG, "Generating request credentials context for token redemption...", true) val secureRandom = SecureRandom() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5922f3c531..a83ede1dfb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4768,7 +4768,9 @@ Donate for a Friend Couldn\'t confirm donation - + + Your %1$s/month donation couldn\'t be confirmed. Check your banking app to approve your iDEAL payment. + Your one-time %1$s donation couldn\'t be confirmed. Check your banking app to approve your iDEAL payment. Enter Custom Amount