mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Implement donations one-time pending state.
This commit is contained in:
committed by
Cody Henthorne
parent
57135ea2c6
commit
627c47b155
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
@@ -110,8 +111,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
gatewayRequest: GatewayRequest,
|
||||
paymentIntentId: String,
|
||||
donationProcessor: DonationProcessor,
|
||||
isLongRunning: Boolean
|
||||
paymentSourceType: PaymentSourceType
|
||||
): Completable {
|
||||
val isLongRunning = paymentSourceType == PaymentSourceType.Stripe.SEPADebit
|
||||
val isBoost = gatewayRequest.recipientId == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
|
||||
@@ -127,6 +129,14 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
Log.d(TAG, "Confirmed payment intent. Recording $donationTypeLabel receipt and submitting badge reimbursement job chain.", true)
|
||||
SignalDatabase.donationReceipts.addReceipt(donationReceiptRecord)
|
||||
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(
|
||||
PendingOneTimeDonationSerializer.createProto(
|
||||
gatewayRequest.badge,
|
||||
paymentSourceType,
|
||||
gatewayRequest.fiat
|
||||
)
|
||||
)
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val chain = if (isBoost) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecimalValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.MathContext
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
object PendingOneTimeDonationSerializer {
|
||||
|
||||
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 createProto(
|
||||
badge: Badge,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
amount: FiatMoney
|
||||
): PendingOneTimeDonation {
|
||||
return PendingOneTimeDonation(
|
||||
badge = Badges.toDatabaseBadge(badge),
|
||||
paymentMethodType = when (paymentSourceType) {
|
||||
PaymentSourceType.PayPal -> PendingOneTimeDonation.PaymentMethodType.PAYPAL
|
||||
PaymentSourceType.Stripe.CreditCard, PaymentSourceType.Stripe.GooglePay, PaymentSourceType.Unknown -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
PaymentSourceType.Stripe.SEPADebit -> PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT
|
||||
},
|
||||
amount = amount.toFiatValue(),
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
fun FiatValue.toFiatMoney(): FiatMoney {
|
||||
return FiatMoney(
|
||||
amount!!.toBigDecimal(),
|
||||
Currency.getInstance(currencyCode)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DecimalValue.toBigDecimal(): BigDecimal {
|
||||
return BigDecimal(
|
||||
BigInteger(value_.toByteArray()),
|
||||
scale,
|
||||
MathContext(precision)
|
||||
)
|
||||
}
|
||||
|
||||
private fun FiatMoney.toFiatValue(): FiatValue {
|
||||
return FiatValue(
|
||||
currencyCode = currency.currencyCode,
|
||||
amount = amount.toDecimalValue()
|
||||
)
|
||||
}
|
||||
|
||||
private fun BigDecimal.toDecimalValue(): DecimalValue {
|
||||
return DecimalValue(
|
||||
scale = scale(),
|
||||
precision = precision(),
|
||||
value_ = ByteString.of(*this.unscaledValue().toByteArray())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ data class DonateToSignalState(
|
||||
|
||||
val areFieldsEnabled: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY && !oneTimeDonationState.isOneTimeDonationPending
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY && !monthlyDonationState.transactionState.isInProgress
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
}
|
||||
@@ -33,7 +33,7 @@ data class DonateToSignalState(
|
||||
|
||||
val canSetCurrency: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
}
|
||||
@@ -85,6 +85,7 @@ data class DonateToSignalState(
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
val donationStage: DonationStage = DonationStage.INIT,
|
||||
val selectableCurrencyCodes: List<String> = emptyList(),
|
||||
val isOneTimeDonationPending: Boolean = SignalStore.donationsValues().getPendingOneTimeDonation() != null,
|
||||
private val minimumDonationAmounts: Map<Currency, FiatMoney> = emptyMap()
|
||||
) {
|
||||
val minimumDonationAmountOfSelectedCurrency: FiatMoney = minimumDonationAmounts[selectedCurrency] ?: FiatMoney(BigDecimal.ZERO, selectedCurrency)
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDo
|
||||
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.SubscriptionRedemptionJobWatcher
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -207,6 +207,13 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun initializeOneTimeDonationState(oneTimeDonationRepository: OneTimeDonationRepository) {
|
||||
oneTimeDonationDisposables += SignalStore.donationsValues().observablePendingOneTimeDonation
|
||||
.map { it.isPresent }
|
||||
.distinctUntilChanged()
|
||||
.subscribe { hasPendingOneTimeDonation ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(isOneTimeDonationPending = hasPendingOneTimeDonation)) }
|
||||
}
|
||||
|
||||
oneTimeDonationDisposables += oneTimeDonationRepository.getBoostBadge().subscribeBy(
|
||||
onSuccess = { badge ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) }
|
||||
@@ -274,7 +281,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun monitorLevelUpdateProcessing() {
|
||||
val isTransactionJobInProgress: Observable<Boolean> = SubscriptionRedemptionJobWatcher.watch().map {
|
||||
val isTransactionJobInProgress: Observable<Boolean> = DonationRedemptionJobWatcher.watchSubscriptionRedemption().map {
|
||||
it.map { jobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> true
|
||||
|
||||
@@ -156,7 +156,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
gatewayRequest = request,
|
||||
paymentIntentId = response.paymentId,
|
||||
donationProcessor = DonationProcessor.PAYPAL,
|
||||
isLongRunning = false
|
||||
paymentSourceType = PaymentSourceType.PayPal
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
@@ -202,7 +202,7 @@ class StripePaymentInProgressViewModel(
|
||||
gatewayRequest = request,
|
||||
paymentIntentId = paymentIntent.intentId,
|
||||
donationProcessor = DonationProcessor.STRIPE,
|
||||
isLongRunning = paymentSource.type.isLongRunning
|
||||
paymentSourceType = paymentSource.type
|
||||
)
|
||||
}
|
||||
}.subscribeBy(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -9,14 +8,15 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.databinding.MySupportPreferenceBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Locale
|
||||
@@ -31,7 +31,7 @@ object ActiveSubscriptionPreference {
|
||||
val price: FiatMoney,
|
||||
val subscription: Subscription,
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
val redemptionState: ManageDonationsState.RedemptionState,
|
||||
val activeSubscription: ActiveSubscription.Subscription,
|
||||
val onContactSupport: () -> Unit,
|
||||
val onPendingClick: (FiatMoney) -> Unit
|
||||
@@ -50,12 +50,12 @@ object ActiveSubscriptionPreference {
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
class ViewHolder(binding: MySupportPreferenceBinding) : BindingViewHolder<Model, MySupportPreferenceBinding>(binding) {
|
||||
|
||||
val badge: BadgeImageView = itemView.findViewById(R.id.my_support_badge)
|
||||
val title: TextView = itemView.findViewById(R.id.my_support_title)
|
||||
val expiry: TextView = itemView.findViewById(R.id.my_support_expiry)
|
||||
val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress)
|
||||
val badge: BadgeImageView = binding.mySupportBadge
|
||||
val title: TextView = binding.mySupportTitle
|
||||
val expiry: TextView = binding.mySupportExpiry
|
||||
val progress: ProgressBar = binding.mySupportProgress
|
||||
|
||||
override fun bind(model: Model) {
|
||||
itemView.setOnClickListener(null)
|
||||
@@ -74,10 +74,10 @@ object ActiveSubscriptionPreference {
|
||||
expiry.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
when (model.redemptionState) {
|
||||
ManageDonationsState.SubscriptionRedemptionState.NONE -> presentRenewalState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER -> presentPendingBankTransferState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS -> presentInProgressState()
|
||||
ManageDonationsState.SubscriptionRedemptionState.FAILED -> presentFailureState(model)
|
||||
ManageDonationsState.RedemptionState.NONE -> presentRenewalState(model)
|
||||
ManageDonationsState.RedemptionState.IS_PENDING_BANK_TRANSFER -> presentPendingBankTransferState(model)
|
||||
ManageDonationsState.RedemptionState.IN_PROGRESS -> presentInProgressState()
|
||||
ManageDonationsState.RedemptionState.FAILED -> presentFailureState(model)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,6 @@ object ActiveSubscriptionPreference {
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.my_support_preference))
|
||||
adapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, MySupportPreferenceBinding::inflate))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Allows observer to poll for the status of the latest pending, running, or completed redemption job for subscriptions or one time payments.
|
||||
*/
|
||||
object DonationRedemptionJobWatcher {
|
||||
|
||||
enum class RedemptionType {
|
||||
SUBSCRIPTION,
|
||||
ONE_TIME
|
||||
}
|
||||
|
||||
fun watchSubscriptionRedemption(): Observable<Optional<JobTracker.JobState>> = watch(RedemptionType.SUBSCRIPTION)
|
||||
|
||||
fun watchOneTimeRedemption(): Observable<Optional<JobTracker.JobState>> = watch(RedemptionType.ONE_TIME)
|
||||
|
||||
private fun watch(redemptionType: RedemptionType): Observable<Optional<JobTracker.JobState>> = Observable.interval(0, 5, TimeUnit.SECONDS).map {
|
||||
val queue = when (redemptionType) {
|
||||
RedemptionType.SUBSCRIPTION -> DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
RedemptionType.ONE_TIME -> DonationReceiptRedemptionJob.ONE_TIME_QUEUE
|
||||
}
|
||||
|
||||
val redemptionJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == DonationReceiptRedemptionJob.KEY && it.parameters.queue?.startsWith(queue) == true
|
||||
}
|
||||
|
||||
val receiptRequestJobKey = when (redemptionType) {
|
||||
RedemptionType.SUBSCRIPTION -> SubscriptionReceiptRequestResponseJob.KEY
|
||||
RedemptionType.ONE_TIME -> BoostReceiptRequestResponseJob.KEY
|
||||
}
|
||||
|
||||
val receiptJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == receiptRequestJobKey && it.parameters.queue?.startsWith(queue) == true
|
||||
}
|
||||
|
||||
val jobState: JobTracker.JobState? = redemptionJobState ?: receiptJobState
|
||||
|
||||
if (redemptionType == RedemptionType.SUBSCRIPTION && jobState == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) {
|
||||
Optional.of(JobTracker.JobState.FAILURE)
|
||||
} else {
|
||||
Optional.ofNullable(jobState)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@@ -70,6 +70,7 @@ class ManageDonationsFragment :
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
ActiveSubscriptionPreference.register(adapter)
|
||||
OneTimeDonationPreference.register(adapter)
|
||||
IndeterminateLoadingCircle.register(adapter)
|
||||
BadgePreview.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
@@ -129,34 +130,36 @@ class ManageDonationsFragment :
|
||||
|
||||
space(16.dp)
|
||||
|
||||
if (state.transactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.transactionState.activeSubscription.activeSubscription
|
||||
if (state.subscriptionTransactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.subscriptionTransactionState.activeSubscription.activeSubscription
|
||||
|
||||
if (activeSubscription != null) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { it.level == activeSubscription.level }
|
||||
if (subscription != null) {
|
||||
presentSubscriptionSettings(activeSubscription, subscription, state.getMonthlyDonorRedemptionState())
|
||||
presentSubscriptionSettings(activeSubscription, subscription, state)
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
} else if (state.hasOneTimeBadge) {
|
||||
presentActiveOneTimeDonorSettings()
|
||||
presentActiveOneTimeDonorSettings(state)
|
||||
} else {
|
||||
presentNotADonorSettings(state.hasReceipts)
|
||||
}
|
||||
} else if (state.transactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
presentNetworkFailureSettings(state.getMonthlyDonorRedemptionState(), state.hasReceipts)
|
||||
} else if (state.subscriptionTransactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
presentNetworkFailureSettings(state, state.hasReceipts)
|
||||
} else {
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentActiveOneTimeDonorSettings() {
|
||||
private fun DSLConfiguration.presentActiveOneTimeDonorSettings(state: ManageDonationsState) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__my_support)
|
||||
|
||||
presentPendingOrProcessingOneTimeDonationState(state)
|
||||
|
||||
presentBadges()
|
||||
|
||||
presentOtherWaysToGive()
|
||||
@@ -164,16 +167,30 @@ class ManageDonationsFragment :
|
||||
presentMore()
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentNetworkFailureSettings(redemptionState: ManageDonationsState.SubscriptionRedemptionState, hasReceipts: Boolean) {
|
||||
private fun DSLConfiguration.presentPendingOrProcessingOneTimeDonationState(state: ManageDonationsState) {
|
||||
val pendingOneTimeDonation = state.pendingOneTimeDonation
|
||||
if (pendingOneTimeDonation != null) {
|
||||
customPref(
|
||||
OneTimeDonationPreference.Model(
|
||||
pendingOneTimeDonation = pendingOneTimeDonation,
|
||||
onPendingClick = {
|
||||
displayPendingDialog(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentNetworkFailureSettings(state: ManageDonationsState, hasReceipts: Boolean) {
|
||||
if (SignalStore.donationsValues().isLikelyASustainer()) {
|
||||
presentSubscriptionSettingsWithNetworkError(redemptionState)
|
||||
presentSubscriptionSettingsWithNetworkError(state)
|
||||
} else {
|
||||
presentNotADonorSettings(hasReceipts)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentSubscriptionSettingsWithNetworkError(redemptionState: ManageDonationsState.SubscriptionRedemptionState) {
|
||||
presentSubscriptionSettingsWithState(redemptionState) {
|
||||
private fun DSLConfiguration.presentSubscriptionSettingsWithNetworkError(state: ManageDonationsState) {
|
||||
presentSubscriptionSettingsWithState(state) {
|
||||
customPref(
|
||||
NetworkFailure.Model(
|
||||
onRetryClick = {
|
||||
@@ -187,9 +204,9 @@ class ManageDonationsFragment :
|
||||
private fun DSLConfiguration.presentSubscriptionSettings(
|
||||
activeSubscription: ActiveSubscription.Subscription,
|
||||
subscription: Subscription,
|
||||
redemptionState: ManageDonationsState.SubscriptionRedemptionState
|
||||
state: ManageDonationsState
|
||||
) {
|
||||
presentSubscriptionSettingsWithState(redemptionState) {
|
||||
presentSubscriptionSettingsWithState(state) {
|
||||
val activeCurrency = Currency.getInstance(activeSubscription.currency)
|
||||
val activeAmount = activeSubscription.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
@@ -198,7 +215,7 @@ class ManageDonationsFragment :
|
||||
price = FiatMoney(activeAmount, activeCurrency),
|
||||
subscription = subscription,
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = redemptionState,
|
||||
redemptionState = state.getMonthlyDonorRedemptionState(),
|
||||
onContactSupport = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
@@ -213,7 +230,7 @@ class ManageDonationsFragment :
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentSubscriptionSettingsWithState(
|
||||
redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
state: ManageDonationsState,
|
||||
subscriptionBlock: DSLConfiguration.() -> Unit
|
||||
) {
|
||||
dividerPref()
|
||||
@@ -222,10 +239,12 @@ class ManageDonationsFragment :
|
||||
|
||||
subscriptionBlock()
|
||||
|
||||
presentPendingOrProcessingOneTimeDonationState(state)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__manage_subscription),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_person_24),
|
||||
isEnabled = redemptionState != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
isEnabled = state.getMonthlyDonorRedemptionState() != ManageDonationsState.RedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
|
||||
@@ -8,25 +9,26 @@ data class ManageDonationsState(
|
||||
val hasOneTimeBadge: Boolean = false,
|
||||
val hasReceipts: Boolean = false,
|
||||
val featuredBadge: Badge? = null,
|
||||
val transactionState: TransactionState = TransactionState.Init,
|
||||
val subscriptionTransactionState: TransactionState = TransactionState.Init,
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
private val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
val pendingOneTimeDonation: PendingOneTimeDonation? = null,
|
||||
private val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE
|
||||
) {
|
||||
|
||||
fun getMonthlyDonorRedemptionState(): SubscriptionRedemptionState {
|
||||
return when (transactionState) {
|
||||
fun getMonthlyDonorRedemptionState(): RedemptionState {
|
||||
return when (subscriptionTransactionState) {
|
||||
TransactionState.Init -> subscriptionRedemptionState
|
||||
TransactionState.NetworkFailure -> subscriptionRedemptionState
|
||||
TransactionState.InTransaction -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
is TransactionState.NotInTransaction -> getStateFromActiveSubscription(transactionState.activeSubscription) ?: subscriptionRedemptionState
|
||||
TransactionState.InTransaction -> RedemptionState.IN_PROGRESS
|
||||
is TransactionState.NotInTransaction -> getStateFromActiveSubscription(subscriptionTransactionState.activeSubscription) ?: subscriptionRedemptionState
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): SubscriptionRedemptionState? {
|
||||
private fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): RedemptionState? {
|
||||
return when {
|
||||
activeSubscription.isFailedPayment -> SubscriptionRedemptionState.FAILED
|
||||
activeSubscription.isPendingBankTransfer -> SubscriptionRedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
activeSubscription.isInProgress -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
activeSubscription.isFailedPayment -> RedemptionState.FAILED
|
||||
activeSubscription.isPendingBankTransfer -> RedemptionState.IS_PENDING_BANK_TRANSFER
|
||||
activeSubscription.isInProgress -> RedemptionState.IN_PROGRESS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -38,7 +40,7 @@ data class ManageDonationsState(
|
||||
class NotInTransaction(val activeSubscription: ActiveSubscription) : TransactionState()
|
||||
}
|
||||
|
||||
enum class SubscriptionRedemptionState {
|
||||
enum class RedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
IS_PENDING_BANK_TRANSFER,
|
||||
|
||||
@@ -11,9 +11,11 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
@@ -50,8 +52,8 @@ class ManageDonationsViewModel(
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.transactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
store.update { it.copy(transactionState = ManageDonationsState.TransactionState.Init) }
|
||||
if (!disposables.isDisposed && store.state.subscriptionTransactionState == ManageDonationsState.TransactionState.NetworkFailure) {
|
||||
store.update { it.copy(subscriptionTransactionState = ManageDonationsState.TransactionState.Init) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
@@ -74,22 +76,21 @@ class ManageDonationsViewModel(
|
||||
store.update { it.copy(hasReceipts = hasReceipts) }
|
||||
}
|
||||
|
||||
disposables += SubscriptionRedemptionJobWatcher.watch().subscribeBy { jobStateOptional ->
|
||||
disposables += DonationRedemptionJobWatcher.watchSubscriptionRedemption().subscribeBy { jobStateOptional ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
subscriptionRedemptionState = jobStateOptional.map { jobState: JobTracker.JobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.RUNNING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.SUCCESS -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
JobTracker.JobState.FAILURE -> ManageDonationsState.SubscriptionRedemptionState.FAILED
|
||||
JobTracker.JobState.IGNORED -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
}
|
||||
}.orElse(ManageDonationsState.SubscriptionRedemptionState.NONE)
|
||||
subscriptionRedemptionState = jobStateOptional.map(this::mapJobStateToRedemptionState).orElse(ManageDonationsState.RedemptionState.NONE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += SignalStore.donationsValues()
|
||||
.observablePendingOneTimeDonation
|
||||
.distinctUntilChanged()
|
||||
.subscribeBy { pending ->
|
||||
store.update { it.copy(pendingOneTimeDonation = pending.orNull()) }
|
||||
}
|
||||
|
||||
disposables += levelUpdateOperationEdges.switchMapSingle { isProcessing ->
|
||||
if (isProcessing) {
|
||||
Single.just(ManageDonationsState.TransactionState.InTransaction)
|
||||
@@ -99,14 +100,14 @@ class ManageDonationsViewModel(
|
||||
}.subscribeBy(
|
||||
onNext = { transactionState ->
|
||||
store.update {
|
||||
it.copy(transactionState = transactionState)
|
||||
it.copy(subscriptionTransactionState = transactionState)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Error retrieving subscription transaction state", throwable)
|
||||
|
||||
store.update {
|
||||
it.copy(transactionState = ManageDonationsState.TransactionState.NetworkFailure)
|
||||
it.copy(subscriptionTransactionState = ManageDonationsState.TransactionState.NetworkFailure)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -121,6 +122,16 @@ class ManageDonationsViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapJobStateToRedemptionState(jobState: JobTracker.JobState): ManageDonationsState.RedemptionState {
|
||||
return when (jobState) {
|
||||
JobTracker.JobState.PENDING -> ManageDonationsState.RedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.RUNNING -> ManageDonationsState.RedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.SUCCESS -> ManageDonationsState.RedemptionState.NONE
|
||||
JobTracker.JobState.FAILURE -> ManageDonationsState.RedemptionState.FAILED
|
||||
JobTracker.JobState.IGNORED -> ManageDonationsState.RedemptionState.NONE
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val subscriptionsRepository: MonthlyDonationRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PendingOneTimeDonationSerializer.toFiatMoney
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.databinding.MySupportPreferenceBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
/**
|
||||
* Holds state information about pending one-time donations.
|
||||
*/
|
||||
object OneTimeDonationPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, MySupportPreferenceBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val pendingOneTimeDonation: PendingOneTimeDonation,
|
||||
val onPendingClick: (FiatMoney) -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return this.pendingOneTimeDonation == newItem.pendingOneTimeDonation
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(binding: MySupportPreferenceBinding) : BindingViewHolder<Model, MySupportPreferenceBinding>(binding) {
|
||||
|
||||
val badge: BadgeImageView = binding.mySupportBadge
|
||||
val title: TextView = binding.mySupportTitle
|
||||
val expiry: TextView = binding.mySupportExpiry
|
||||
val progress: ProgressBar = binding.mySupportProgress
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadge(Badges.fromDatabaseBadge(model.pendingOneTimeDonation.badge!!))
|
||||
title.text = context.getString(
|
||||
R.string.OneTimeDonationPreference__one_time_s,
|
||||
FiatMoneyUtil.format(context.resources, model.pendingOneTimeDonation.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
|
||||
expiry.text = getPendingSubtitle(model.pendingOneTimeDonation.paymentMethodType)
|
||||
|
||||
if (model.pendingOneTimeDonation.paymentMethodType == PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT) {
|
||||
itemView.setOnClickListener { model.onPendingClick(model.pendingOneTimeDonation.amount.toFiatMoney()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPendingSubtitle(paymentMethodType: PendingOneTimeDonation.PaymentMethodType): String {
|
||||
return when (paymentMethodType) {
|
||||
PendingOneTimeDonation.PaymentMethodType.CARD -> context.getString(R.string.OneTimeDonationPreference__donation_processing)
|
||||
PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT -> context.getString(R.string.OneTimeDonationPreference__donation_pending)
|
||||
PendingOneTimeDonation.PaymentMethodType.PAYPAL -> context.getString(R.string.OneTimeDonationPreference__donation_processing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Allows observer to poll for the status of the latest pending, running, or completed redemption job for subscriptions.
|
||||
*/
|
||||
object SubscriptionRedemptionJobWatcher {
|
||||
fun watch(): Observable<Optional<JobTracker.JobState>> = Observable.interval(0, 5, TimeUnit.SECONDS).map {
|
||||
val redemptionJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == DonationReceiptRedemptionJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val receiptJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == SubscriptionReceiptRequestResponseJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val jobState: JobTracker.JobState? = redemptionJobState ?: receiptJobState
|
||||
|
||||
if (jobState == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) {
|
||||
Optional.of(JobTracker.JobState.FAILURE)
|
||||
} else {
|
||||
Optional.ofNullable(jobState)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@@ -38,7 +38,11 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
private static final long NO_ID = -1L;
|
||||
|
||||
public static final String SUBSCRIPTION_QUEUE = "ReceiptRedemption";
|
||||
public static final String ONE_TIME_QUEUE = "BoostReceiptRedemption";
|
||||
public static final String KEY = "DonationReceiptRedemptionJob";
|
||||
|
||||
private static final String LONG_RUNNING_QUEUE_SUFFIX = "__LONG_RUNNING";
|
||||
|
||||
public static final String INPUT_RECEIPT_CREDENTIAL_PRESENTATION = "data.receipt.credential.presentation";
|
||||
public static final String INPUT_KEEP_ALIVE_409 = "data.keep.alive.409";
|
||||
public static final String DATA_ERROR_SOURCE = "data.error.source";
|
||||
@@ -63,7 +67,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(SUBSCRIPTION_QUEUE)
|
||||
.setQueue(SUBSCRIPTION_QUEUE + (isLongRunningDonationPaymentType ? LONG_RUNNING_QUEUE_SUFFIX : ""))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setMaxInstancesForQueue(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
@@ -80,7 +84,7 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
new Job.Parameters
|
||||
.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("BoostReceiptRedemption")
|
||||
.setQueue(ONE_TIME_QUEUE + (isLongRunningDonationPaymentType ? LONG_RUNNING_QUEUE_SUFFIX : ""))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
@@ -154,6 +158,8 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue();
|
||||
} else if (giftMessageId != NO_ID) {
|
||||
SignalDatabase.messages().markGiftRedemptionFailed(giftMessageId);
|
||||
} else {
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +234,10 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId()));
|
||||
}
|
||||
}
|
||||
|
||||
if (isForOneTimeDonation()) {
|
||||
SignalStore.donationsValues().setPendingOneTimeDonation(null);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable ReceiptCredentialPresentation getPresentation() throws InvalidInputException, NoSuchMessageException {
|
||||
@@ -287,6 +297,10 @@ public class DonationReceiptRedemptionJob extends BaseJob {
|
||||
return Objects.equals(getParameters().getQueue(), SUBSCRIPTION_QUEUE);
|
||||
}
|
||||
|
||||
private boolean isForOneTimeDonation() {
|
||||
return Objects.equals(getParameters().getQueue(), ONE_TIME_QUEUE) && giftMessageId == NO_ID;
|
||||
}
|
||||
|
||||
private void enqueueDonationComplete(long receiptLevel) {
|
||||
if (errorSource == DonationErrorSource.GIFT || errorSource == DonationErrorSource.GIFT_REDEMPTION) {
|
||||
Log.i(TAG, "Skipping donation complete sheet for GIFT related redemption.");
|
||||
|
||||
@@ -14,8 +14,10 @@ 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.PendingOneTimeDonationSerializer.isExpired
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationCompletedQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
@@ -29,6 +31,7 @@ import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import java.security.SecureRandom
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class DonationsValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
@@ -115,6 +118,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
* Popped from whenever we enter the conversation list.
|
||||
*/
|
||||
private const val DONATION_COMPLETE_QUEUE = "donation.complete.queue"
|
||||
|
||||
/**
|
||||
* The current one-time donation we are processing, if we are doing so. This is used for showing
|
||||
* the donation processing / donation pending state in the ManageDonationsFragment.
|
||||
*/
|
||||
private const val PENDING_ONE_TIME_DONATION = "pending.one.time.donation"
|
||||
}
|
||||
|
||||
override fun onFirstEverAppLaunch() = Unit
|
||||
@@ -142,6 +151,14 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
private val oneTimeCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getOneTimeCurrency()) }
|
||||
val observableOneTimeCurrency: Observable<Currency> by lazy { oneTimeCurrencyPublisher }
|
||||
|
||||
private var _pendingOneTimeDonation: PendingOneTimeDonation? by protoValue(PENDING_ONE_TIME_DONATION, PendingOneTimeDonation.ADAPTER)
|
||||
private val pendingOneTimeDonationPublisher: Subject<Optional<PendingOneTimeDonation>> by lazy { BehaviorSubject.createDefault(Optional.ofNullable(_pendingOneTimeDonation)) }
|
||||
val observablePendingOneTimeDonation: Observable<Optional<PendingOneTimeDonation>> by lazy {
|
||||
pendingOneTimeDonationPublisher.map { optionalPendingOneTimeDonation ->
|
||||
optionalPendingOneTimeDonation.filter { !it.isExpired }
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptionCurrency(): Currency {
|
||||
val currencyCode = getString(KEY_SUBSCRIPTION_CURRENCY_CODE, null)
|
||||
val currency: Currency? = if (currencyCode == null) {
|
||||
@@ -501,6 +518,13 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||
}
|
||||
}
|
||||
|
||||
fun getPendingOneTimeDonation(): PendingOneTimeDonation? = _pendingOneTimeDonation.takeUnless { it?.isExpired == true }
|
||||
|
||||
fun setPendingOneTimeDonation(pendingOneTimeDonation: PendingOneTimeDonation?) {
|
||||
this._pendingOneTimeDonation = pendingOneTimeDonation
|
||||
pendingOneTimeDonationPublisher.onNext(Optional.ofNullable(pendingOneTimeDonation))
|
||||
}
|
||||
|
||||
private fun generateRequestCredential(): ReceiptCredentialRequestContext {
|
||||
Log.d(TAG, "Generating request credentials context for token redemption...", true)
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import com.squareup.wire.ProtoAdapter
|
||||
import org.signal.core.util.LongSerializer
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -31,6 +32,10 @@ internal fun <T : Any?> SignalStoreValues.enumValue(key: String, default: T, ser
|
||||
return KeyValueEnumValue(key, default, serializer, this.store)
|
||||
}
|
||||
|
||||
internal fun <M> SignalStoreValues.protoValue(key: String, adapter: ProtoAdapter<M>): SignalStoreValueDelegate<M?> {
|
||||
return KeyValueProtoValue(key, adapter, this.store)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin delegate that serves as a base for all other value types. This allows us to only expose this sealed
|
||||
* class to callers and protect the individual implementations as private behind the various extension functions.
|
||||
@@ -109,6 +114,28 @@ private class BlobValue(private val key: String, private val default: ByteArray,
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyValueProtoValue<M>(
|
||||
private val key: String,
|
||||
private val adapter: ProtoAdapter<M>,
|
||||
store: KeyValueStore
|
||||
) : SignalStoreValueDelegate<M?>(store) {
|
||||
override fun getValue(values: KeyValueStore): M? {
|
||||
return if (values.containsKey(key)) {
|
||||
adapter.decode(values.getBlob(key, null))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(values: KeyValueStore, value: M?) {
|
||||
if (value != null) {
|
||||
values.beginWrite().putBlob(key, adapter.encode(value)).apply()
|
||||
} else {
|
||||
values.beginWrite().remove(key).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyValueEnumValue<T>(private val key: String, private val default: T, private val serializer: LongSerializer<T>, store: KeyValueStore) : SignalStoreValueDelegate<T>(store) {
|
||||
override fun getValue(values: KeyValueStore): T {
|
||||
return if (values.containsKey(key)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package org.thoughtcrime.securesms.payments;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -273,6 +273,31 @@ message SessionSwitchoverEvent {
|
||||
string e164 = 1;
|
||||
}
|
||||
|
||||
message DecimalValue {
|
||||
uint32 scale = 1;
|
||||
uint32 precision = 2;
|
||||
bytes value = 3;
|
||||
}
|
||||
|
||||
message FiatValue {
|
||||
string currencyCode = 1;
|
||||
DecimalValue amount = 2;
|
||||
uint64 timestamp = 3;
|
||||
}
|
||||
|
||||
message PendingOneTimeDonation {
|
||||
enum PaymentMethodType {
|
||||
CARD = 0;
|
||||
SEPA_DEBIT = 1;
|
||||
PAYPAL = 2;
|
||||
}
|
||||
|
||||
PaymentMethodType paymentMethodType = 1;
|
||||
FiatValue amount = 2;
|
||||
BadgeList.Badge badge = 3;
|
||||
int64 timestamp = 4;
|
||||
}
|
||||
|
||||
message DonationCompletedQueue {
|
||||
message DonationCompleted {
|
||||
int64 level = 1;
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/active_subscription_gutter_start"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
tools:viewBindingIgnore="true">
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -151,6 +151,14 @@
|
||||
<!-- Button label to confirm credit card input and proceed with subscription payment -->
|
||||
<string name="CreditCardFragment__donate_s_month">Donate %1$s/month</string>
|
||||
|
||||
<!-- OneTimeDonationPreference -->
|
||||
<!-- Preference title with placeholder for amount. -->
|
||||
<string name="OneTimeDonationPreference__one_time_s">One time %1$s</string>
|
||||
<!-- Preference subtitle when donation is pending -->
|
||||
<string name="OneTimeDonationPreference__donation_pending">Donation pending</string>
|
||||
<!-- Preference subtitle when donation is processing -->
|
||||
<string name="OneTimeDonationPreference__donation_processing">Donation processing</string>
|
||||
|
||||
<!-- BlockUnblockDialog -->
|
||||
<string name="BlockUnblockDialog_block_and_leave_s">Block and leave %1$s?</string>
|
||||
<string name="BlockUnblockDialog_block_s">Block %1$s?</string>
|
||||
|
||||
@@ -71,12 +71,17 @@ class StripeApi(
|
||||
return Single.fromCallable {
|
||||
val paymentMethodId = createPaymentMethodAndParseId(paymentSource)
|
||||
|
||||
val parameters = mapOf(
|
||||
val parameters = mutableMapOf(
|
||||
"client_secret" to setupIntent.intentClientSecret,
|
||||
"payment_method" to paymentMethodId,
|
||||
"return_url" to RETURN_URL_3DS
|
||||
)
|
||||
|
||||
if (paymentSource.type == PaymentSourceType.Stripe.SEPADebit) {
|
||||
parameters["mandate_data[customer_acceptance][type]"] = "online"
|
||||
parameters["mandate_data[customer_acceptance][online][infer_from_client]"] = "true"
|
||||
}
|
||||
|
||||
val (nextActionUri, returnUri) = postForm("setup_intents/${setupIntent.intentId}/confirm", parameters).use { response ->
|
||||
getNextAction(response)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user