Flesh out monthly iDEAL donation flow.

This commit is contained in:
Cody Henthorne
2023-11-08 16:06:35 -05:00
parent 96aec401b9
commit f062e58f7b
18 changed files with 238 additions and 53 deletions

View File

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

View File

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

View File

@@ -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<String> = emptyList(),
val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null,
val transactionState: TransactionState = TransactionState()
) {
val isSubscriptionActive: Boolean = _activeSubscription?.isActive == true

View File

@@ -305,24 +305,16 @@ class DonateToSignalViewModel(
}
private fun monitorLevelUpdateProcessing() {
val isTransactionJobInProgress: Observable<Boolean> = DonationRedemptionJobWatcher.watchSubscriptionRedemption().map {
when (it) {
is DonationRedemptionJobStatus.PendingExternalVerification,
DonationRedemptionJobStatus.PendingReceiptRedemption,
DonationRedemptionJobStatus.PendingReceiptRequest -> true
DonationRedemptionJobStatus.FailedSubscription,
DonationRedemptionJobStatus.None -> false
}
}
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = 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)
)
)
}

View File

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

View File

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

View File

@@ -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<Model>() {
@@ -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)

View File

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

View File

@@ -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<DonationRedemptionJobStatus> = watch(RedemptionType.SUBSCRIPTION)
@JvmStatic
@WorkerThread
fun getSubscriptionRedemptionJobStatus(): DonationRedemptionJobStatus {
return getDonationRedemptionJobStatus(RedemptionType.SUBSCRIPTION)
}
fun watchOneTimeRedemption(): Observable<DonationRedemptionJobStatus> = watch(RedemptionType.ONE_TIME)
private fun watch(redemptionType: RedemptionType): Observable<DonationRedemptionJobStatus> = Observable.interval(0, 5, TimeUnit.SECONDS).map {
private fun watch(redemptionType: RedemptionType): Observable<DonationRedemptionJobStatus> {
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
)
}
}

View File

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

View File

@@ -12,6 +12,7 @@ data class ManageDonationsState(
val subscriptionTransactionState: TransactionState = TransactionState.Init,
val availableSubscriptions: List<Subscription> = emptyList(),
val pendingOneTimeDonation: PendingOneTimeDonation? = null,
val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null,
private val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE
) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4768,7 +4768,9 @@
<string name="ManageDonationsFragment__donate_for_a_friend">Donate for a Friend</string>
<!-- Dialog title shown when a donation requires verifying/confirmation outside of the app and the user hasn't done that yet -->
<string name="ManageDonationsFragment__couldnt_confirm_donation">Couldn\'t confirm donation</string>
<!-- Dialog message shown when a donation requires verifying/confirmation outside of the app and the user hasn't done that yet -->
<!-- Dialog message shown when a monthly donation requires verifying/confirmation outside of the app and the user hasn't done that yet, placeholder is money amount -->
<string name="ManageDonationsFragment__your_monthly_s_donation_couldnt_be_confirmed">Your %1$s/month donation couldn\'t be confirmed. Check your banking app to approve your iDEAL payment.</string>
<!-- Dialog message shown when a one-time donation requires verifying/confirmation outside of the app and the user hasn't done that yet, placeholder is money amount -->
<string name="ManageDonationsFragment__your_one_time_s_donation_couldnt_be_confirmed">Your one-time %1$s donation couldn\'t be confirmed. Check your banking app to approve your iDEAL payment.</string>
<string name="Boost__enter_custom_amount">Enter Custom Amount</string>