Show dialog when attempting to donate again while still processing previous donation.

This commit is contained in:
Cody Henthorne
2023-11-01 16:52:57 -04:00
committed by Greyson Parrelli
parent b96b99c1c4
commit 117bbdbcdf
10 changed files with 131 additions and 55 deletions

View File

@@ -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"))

View File

@@ -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,

View File

@@ -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() })

View File

@@ -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<String> = 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<Currency, FiatMoney> = emptyMap()
) {
val minimumDonationAmountOfSelectedCurrency: FiatMoney = minimumDonationAmounts[selectedCurrency] ?: FiatMoney(BigDecimal.ZERO, selectedCurrency)

View File

@@ -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

View File

@@ -15,14 +15,13 @@ object DonationPillToggle {
}
class Model(
val isEnabled: Boolean,
val selected: DonateToSignalType,
val onClick: () -> Unit
) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = true
override fun areContentsTheSame(newItem: Model): Boolean {
return isEnabled == newItem.isEnabled && selected == newItem.selected
return selected == newItem.selected
}
}

View File

@@ -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

View File

@@ -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<BodyRange>?.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()
}

View File

@@ -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

View File

@@ -5823,6 +5823,17 @@
<string name="DonateToSignalFragment__continue">Continue</string>
<!-- Description below title -->
<string name="DonateToSignalFragment__private_messaging">Private messaging, funded by you. No ads, no tracking, no compromise. Donate now to support Signal.</string>
<!-- Dialog title when a user tries to donate while they already have a pending donation. -->
<string name="DonateToSignalFragment__you_have_a_donation_pending">You have a donation pending</string>
<!-- Dialog body when a user tries to donate while they already have a pending monthly donation. -->
<string name="DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_monthly">Bank transfers usually take 1 business day to process. Please wait until this payment completes before updating your subscription.</string>
<!-- Dialog body when a user tries to donate while they already have a pending one time donation. -->
<string name="DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_onetime">Bank transfers usually take 1 business day to process. Please wait until this payment completes before making another donation.</string>
<!-- Dialog body when a user tries to donate while they already have a pending monthly donation. -->
<string name="DonateToSignalFragment__your_payment_is_still_being_processed_monthly">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.</string>
<!-- Dialog body when a user tries to donate while they already have a pending one time donation. -->
<string name="DonateToSignalFragment__your_payment_is_still_being_processed_onetime">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.</string>
<!-- Donation pill toggle monthly text -->
<string name="DonationPillToggle__monthly">Monthly</string>
<!-- Donation pill toggle one-time text -->