From 117bbdbcdff97e9cebad07ef4824516eb96f0cc2 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 1 Nov 2023 16:52:57 -0400 Subject: [PATCH] Show dialog when attempting to donate again while still processing previous donation. --- .../app/internal/InternalSettingsFragment.kt | 13 +++ .../DonationSerializationHelper.kt | 16 ---- .../donate/DonateToSignalFragment.kt | 91 ++++++++++++------- .../donate/DonateToSignalState.kt | 18 +++- .../donate/DonateToSignalViewModel.kt | 2 +- .../subscription/donate/DonationPillToggle.kt | 3 +- .../securesms/database/JobDatabase.kt | 6 ++ .../database/model/DatabaseProtosUtil.kt | 24 +++++ .../securesms/keyvalue/DonationsValues.kt | 2 +- app/src/main/res/values/strings.xml | 11 +++ 10 files changed, 131 insertions(+), 55 deletions(-) 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 04fb4638b5..be7d7a10d5 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 @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.database.MegaphoneDatabase @@ -218,6 +219,18 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } ) + clickPref( + title = DSLSettingsText.from("Retry all jobs now"), + summary = DSLSettingsText.from("Clear backoff intervals, app will restart"), + onClick = { + SimpleTask.run({ + JobDatabase.getInstance(ApplicationDependencies.getApplication()).debugResetBackoffInterval() + }) { + AppUtil.restart(requireContext()) + } + } + ) + dividerPref() sectionHeaderPref(DSLSettingsText.from("Payments")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationSerializationHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationSerializationHelper.kt index d203438557..516b228fd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationSerializationHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationSerializationHelper.kt @@ -17,24 +17,8 @@ import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext import java.util.Currency -import kotlin.time.Duration.Companion.days object DonationSerializationHelper { - - private val PENDING_ONE_TIME_BANK_TRANSFER_TIMEOUT = 14.days - private val PENDING_ONE_TIME_NORMAL_TIMEOUT = 1.days - - val PendingOneTimeDonation.isExpired: Boolean - get() { - val timeout = if (paymentMethodType == PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT) { - PENDING_ONE_TIME_BANK_TRANSFER_TIMEOUT - } else { - PENDING_ONE_TIME_NORMAL_TIMEOUT - } - - return (timestamp + timeout.inWholeMilliseconds) < System.currentTimeMillis() - } - fun createPendingOneTimeDonationProto( badge: Badge, paymentSourceType: PaymentSourceType, 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 afdca6323b..8b1e9b3d64 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 @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.util.Projection import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import java.util.Currency /** @@ -233,7 +234,6 @@ class DonateToSignalFragment : customPref( DonationPillToggle.Model( - isEnabled = state.areFieldsEnabled, selected = state.donateToSignalType, onClick = { viewModel.toggleDonationType() @@ -256,23 +256,27 @@ class DonateToSignalFragment : text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription), isEnabled = state.canUpdate, onClick = { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.SubscribeFragment__update_subscription_question) - .setMessage( - getString( - R.string.SubscribeFragment__you_will_be_charged_the_full_amount_s_of, - FiatMoneyUtil.format( - requireContext().resources, - viewModel.getSelectedSubscriptionCost(), - FiatMoneyUtil.formatOptions().trimZerosAfterDecimal() + if (state.monthlyDonationState.transactionState.isTransactionJobPending) { + showDonationPendingDialog(state) + } else { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.SubscribeFragment__update_subscription_question) + .setMessage( + getString( + R.string.SubscribeFragment__you_will_be_charged_the_full_amount_s_of, + FiatMoneyUtil.format( + requireContext().resources, + viewModel.getSelectedSubscriptionCost(), + FiatMoneyUtil.formatOptions().trimZerosAfterDecimal() + ) ) ) - ) - .setPositiveButton(R.string.SubscribeFragment__update) { _, _ -> - viewModel.updateSubscription() - } - .setNegativeButton(android.R.string.cancel) { _, _ -> } - .show() + .setPositiveButton(R.string.SubscribeFragment__update) { _, _ -> + viewModel.updateSubscription() + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } } ) @@ -282,28 +286,58 @@ class DonateToSignalFragment : text = DSLSettingsText.from(R.string.SubscribeFragment__cancel_subscription), isEnabled = state.areFieldsEnabled, onClick = { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.SubscribeFragment__confirm_cancellation) - .setMessage(R.string.SubscribeFragment__you_wont_be_charged_again) - .setPositiveButton(R.string.SubscribeFragment__confirm) { _, _ -> - viewModel.cancelSubscription() - } - .setNegativeButton(R.string.SubscribeFragment__not_now) { _, _ -> } - .show() + if (state.monthlyDonationState.transactionState.isTransactionJobPending) { + showDonationPendingDialog(state) + } else { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.SubscribeFragment__confirm_cancellation) + .setMessage(R.string.SubscribeFragment__you_wont_be_charged_again) + .setPositiveButton(R.string.SubscribeFragment__confirm) { _, _ -> + viewModel.cancelSubscription() + } + .setNegativeButton(R.string.SubscribeFragment__not_now) { _, _ -> } + .show() + } } ) } else { primaryButton( text = DSLSettingsText.from(R.string.DonateToSignalFragment__continue), - isEnabled = state.canContinue, + isEnabled = state.continueEnabled, onClick = { - viewModel.requestSelectGateway() + if (state.canContinue) { + viewModel.requestSelectGateway() + } else { + showDonationPendingDialog(state) + } } ) } } } + private fun showDonationPendingDialog(state: DonateToSignalState) { + val message = if (state.donateToSignalType == DonateToSignalType.ONE_TIME) { + if (state.oneTimeDonationState.isOneTimeDonationLongRunning) { + R.string.DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_onetime + } else { + R.string.DonateToSignalFragment__your_payment_is_still_being_processed_onetime + } + } 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 { + R.string.DonateToSignalFragment__your_payment_is_still_being_processed_monthly + } + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.DonateToSignalFragment__you_have_a_donation_pending) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + } + private fun DSLConfiguration.displayOneTimeSelection(areFieldsEnabled: Boolean, state: DonateToSignalState.OneTimeDonationState) { when (state.donationStage) { DonateToSignalState.DonationStage.INIT -> customPref(Boost.LoadingModel()) @@ -337,11 +371,6 @@ class DonateToSignalFragment : } private fun DSLConfiguration.displayMonthlySelection(areFieldsEnabled: Boolean, state: DonateToSignalState.MonthlyDonationState) { - if (state.transactionState.isTransactionJobPending) { - customPref(Subscription.LoaderModel()) - return - } - when (state.donationStage) { DonateToSignalState.DonationStage.INIT -> customPref(Subscription.LoaderModel()) DonateToSignalState.DonationStage.FAILURE -> customPref(NetworkFailure.Model { viewModel.retryMonthlyDonationState() }) 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 dda20f93f2..ecfb39b4f2 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,8 @@ 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.database.model.isLongRunning +import org.thoughtcrime.securesms.database.model.isPending import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.subscription.Subscription import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription @@ -19,8 +21,8 @@ data class DonateToSignalState( val areFieldsEnabled: Boolean get() = when (donateToSignalType) { - DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY && !oneTimeDonationState.isOneTimeDonationPending - DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress + DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY + DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY DonateToSignalType.GIFT -> error("This flow does not support gifts") } @@ -59,13 +61,20 @@ data class DonateToSignalState( DonateToSignalType.GIFT -> error("This flow does not support gifts") } - val canContinue: Boolean + val continueEnabled: Boolean get() = when (donateToSignalType) { DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable() DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable() DonateToSignalType.GIFT -> error("This flow does not support gifts") } + val canContinue: Boolean + get() = when (donateToSignalType) { + DonateToSignalType.ONE_TIME -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending + DonateToSignalType.MONTHLY -> continueEnabled && !monthlyDonationState.isSubscriptionActive + DonateToSignalType.GIFT -> error("This flow does not support gifts") + } + val canUpdate: Boolean get() = when (donateToSignalType) { DonateToSignalType.ONE_TIME -> false @@ -85,7 +94,8 @@ data class DonateToSignalState( val isCustomAmountFocused: Boolean = false, val donationStage: DonationStage = DonationStage.INIT, val selectableCurrencyCodes: List = emptyList(), - val isOneTimeDonationPending: Boolean = SignalStore.donationsValues().getPendingOneTimeDonation() != null, + val isOneTimeDonationPending: Boolean = SignalStore.donationsValues().getPendingOneTimeDonation().isPending(), + val isOneTimeDonationLongRunning: Boolean = SignalStore.donationsValues().getPendingOneTimeDonation().isLongRunning(), private val minimumDonationAmounts: Map = emptyMap() ) { val minimumDonationAmountOfSelectedCurrency: FiatMoney = minimumDonationAmounts[selectedCurrency] ?: FiatMoney(BigDecimal.ZERO, selectedCurrency) 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 dd564f19f1..f6bcd692e0 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 @@ -13,12 +13,12 @@ import org.signal.core.util.StringUtil import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.PlatformCurrencyUtil -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.isExpired import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository 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.manage.DonationRedemptionJobWatcher +import org.thoughtcrime.securesms.database.model.isExpired import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.keyvalue.SignalStore diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt index b150b3746a..62051ce05c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonationPillToggle.kt @@ -15,14 +15,13 @@ object DonationPillToggle { } class Model( - val isEnabled: Boolean, val selected: DonateToSignalType, val onClick: () -> Unit ) : MappingModel { override fun areItemsTheSame(newItem: Model): Boolean = true override fun areContentsTheSame(newItem: Model): Boolean { - return isEnabled == newItem.isEnabled && selected == newItem.selected + return selected == newItem.selected } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt index b3978ccf75..158422d728 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/JobDatabase.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.Application import android.content.ContentValues import android.database.Cursor +import androidx.core.content.contentValuesOf import net.zetetic.database.sqlcipher.SQLiteDatabase import net.zetetic.database.sqlcipher.SQLiteOpenHelper import org.signal.core.util.CursorUtil @@ -408,6 +409,11 @@ class JobDatabase( } } + /** Should only be used for debugging! */ + fun debugResetBackoffInterval() { + writableDatabase.update(Jobs.TABLE_NAME, contentValuesOf(Jobs.NEXT_BACKOFF_INTERVAL to 0), null, null) + } + companion object { private val TAG = Log.tag(JobDatabase::class.java) private const val DATABASE_VERSION = 2 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt index 82a033dcb9..890874f4eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtosUtil.kt @@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.database.model import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList +import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation import org.whispersystems.signalservice.internal.push.BodyRange +import kotlin.time.Duration.Companion.days /** * Collection of extensions to make working with database protos cleaner. @@ -52,3 +54,25 @@ fun List?.toBodyRangeList(): BodyRangeList? { return builder.build() } + +fun PendingOneTimeDonation?.isPending(): Boolean { + return this != null && this.error == null && !this.isExpired +} + +fun PendingOneTimeDonation?.isLongRunning(): Boolean { + return isPending() && this!!.paymentMethodType == PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT +} + +val PendingOneTimeDonation.isExpired: Boolean + get() { + val pendingOneTimeBankTransferTimeout = 14.days + val pendingOneTimeNormalTimeout = 1.days + + val timeout = if (paymentMethodType == PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT) { + pendingOneTimeBankTransferTimeout + } else { + pendingOneTimeNormalTimeout + } + + return (timestamp + timeout.inWholeMilliseconds) < System.currentTimeMillis() + } 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 20d04aeb25..038d3f4a66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -14,12 +14,12 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext import org.signal.libsignal.zkgroup.receipts.ReceiptSerial import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.models.Badge -import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.isExpired import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSData import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue +import org.thoughtcrime.securesms.database.model.isExpired import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.payments.currency.CurrencyUtil diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 469594f508..25c6a67c7a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5823,6 +5823,17 @@ Continue Private messaging, funded by you. No ads, no tracking, no compromise. Donate now to support Signal. + + You have a donation pending + + Bank transfers usually take 1 business day to process. Please wait until this payment completes before updating your subscription. + + Bank transfers usually take 1 business day to process. Please wait until this payment completes before making another donation. + + Your payment is still being processed. This can take a few minutes depending on your connection. Please wait until this payment completes before updating your subscription. + + Your payment is still being processed. This can take a few minutes depending on your connection. Please wait until this payment completes before making another donation. + Monthly