mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Flesh out monthly iDEAL donation flow.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user