mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Rewrite in-app-payment flows to prepare for backups support.
This commit is contained in:
committed by
Cody Henthorne
parent
b36b00a11c
commit
d719edf104
@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -55,8 +55,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(DonateToSignalType.MONTHLY)
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(DonateToSignalType.ONE_TIME)
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.RECURRING_DONATION)
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToDonateToSignal(InAppPaymentTable.Type.ONE_TIME_DONATION)
|
||||
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
|
||||
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
|
||||
|
||||
@@ -25,8 +25,10 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
@@ -289,7 +291,8 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
private fun copySubscriberIdToClipboard(): Boolean {
|
||||
val subscriber = SignalStore.donationsValues().getSubscriber()
|
||||
// TODO [alex] -- db access on main thread!
|
||||
val subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
return if (subscriber == null) {
|
||||
false
|
||||
} else {
|
||||
|
||||
@@ -6,8 +6,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -15,7 +16,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class AppSettingsViewModel(
|
||||
monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(
|
||||
@@ -39,7 +40,7 @@ class AppSettingsViewModel(
|
||||
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
|
||||
store.update(selfLiveData) { self, state -> state.copy(self = self) }
|
||||
|
||||
disposables += monthlyDonationRepository.getActiveSubscription().subscribeBy(
|
||||
disposables += recurringInAppPaymentRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION).subscribeBy(
|
||||
onSuccess = { activeSubscription ->
|
||||
store.update { state ->
|
||||
state.copy(allowUserToGoToDonationManagementScreen = activeSubscription.isActive || InAppDonations.hasAtLeastOnePaymentMethodAvailable())
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
@@ -33,11 +34,12 @@ import org.thoughtcrime.securesms.database.LogDatabase
|
||||
import org.thoughtcrime.securesms.database.MegaphoneDatabase
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
@@ -45,8 +47,6 @@ import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
@@ -63,7 +63,7 @@ import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||
@@ -538,7 +538,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
|
||||
dividerPref()
|
||||
|
||||
if (SignalStore.donationsValues().getSubscriber() != null) {
|
||||
// TODO [alex] -- db access on main thread!
|
||||
if (InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION) != null) {
|
||||
sectionHeaderPref(DSLSettingsText.from("Badges"))
|
||||
|
||||
clickPref(
|
||||
@@ -571,7 +572,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
Toast.makeText(context, "Cleared", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
}
|
||||
|
||||
@@ -938,16 +938,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionRedemption() {
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(
|
||||
-1L,
|
||||
TerminalDonationQueue.TerminalDonation(
|
||||
level = 1000
|
||||
)
|
||||
).enqueue()
|
||||
viewModel.enqueueSubscriptionRedemption()
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionKeepAlive() {
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis())
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
}
|
||||
|
||||
private fun clearCdsHistory() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.internal
|
||||
import android.content.Context
|
||||
import org.json.JSONObject
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
|
||||
@@ -13,6 +14,7 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
|
||||
import org.thoughtcrime.securesms.jobs.FetchRemoteMegaphoneImageJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentRecurringContextJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -30,6 +32,15 @@ class InternalSettingsRepository(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun enqueueSubscriptionRedemption() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val latest = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(InAppPaymentTable.Type.RECURRING_DONATION)
|
||||
if (latest != null) {
|
||||
InAppPaymentRecurringContextJob.createJobChain(latest).enqueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addSampleReleaseNote() {
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
ApplicationDependencies.getJobManager().runSynchronously(CreateReleaseChannelJob.create(), 5000)
|
||||
|
||||
@@ -136,6 +136,10 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
repository.addRemoteMegaphone(RemoteMegaphoneRecord.ActionId.DONATE_FOR_FRIEND)
|
||||
}
|
||||
|
||||
fun enqueueSubscriptionRedemption() {
|
||||
repository.enqueueSubscriptionRedemption()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Un
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getBoostBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
@@ -101,7 +101,7 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
|
||||
fun save(): Completable {
|
||||
val snapshot = store.state
|
||||
val saveState = Completable.fromAction {
|
||||
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
|
||||
synchronized(InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
when {
|
||||
snapshot.selectedBadge?.isGift() == true -> handleGiftExpiration(snapshot)
|
||||
snapshot.selectedBadge?.isBoost() == true -> handleBoostExpiration(snapshot)
|
||||
@@ -116,7 +116,7 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
|
||||
|
||||
fun clearErrorState(): Completable {
|
||||
return Completable.fromAction {
|
||||
synchronized(SubscriptionReceiptRequestResponseJob.MUTEX) {
|
||||
synchronized(InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.donationsValues().setExpiredBadge(null)
|
||||
SignalStore.donationsValues().setExpiredGiftBadge(null)
|
||||
SignalStore.donationsValues().unexpectedSubscriptionCancelationReason = null
|
||||
|
||||
@@ -30,9 +30,9 @@ import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
@@ -47,7 +47,7 @@ class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
DonationPendingBottomSheetContent(
|
||||
badge = args.request.badge,
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
onDoneClick = this::onDoneClick
|
||||
)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
|
||||
if (args.request.donateToSignalType == DonateToSignalType.ONE_TIME) {
|
||||
if (!args.inAppPayment.type.recurring) {
|
||||
findNavController().popBackStack()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
@@ -24,21 +24,23 @@ object InAppDonations {
|
||||
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable() || isSEPADebitAvailable() || isIDEALAvailable()
|
||||
}
|
||||
|
||||
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, donateToSignalType: DonateToSignalType): Boolean {
|
||||
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, inAppPaymentType: InAppPaymentTable.Type): Boolean {
|
||||
return when (paymentSourceType) {
|
||||
PaymentSourceType.PayPal -> isPayPalAvailableForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.PayPal -> isPayPalAvailableForDonateToSignalType(inAppPaymentType)
|
||||
PaymentSourceType.Stripe.CreditCard -> isCreditCardAvailable()
|
||||
PaymentSourceType.Stripe.GooglePay -> isGooglePayAvailable()
|
||||
PaymentSourceType.Stripe.SEPADebit -> isSEPADebitAvailableForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.Stripe.IDEAL -> isIDEALAvailbleForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.Stripe.SEPADebit -> isSEPADebitAvailableForDonateToSignalType(inAppPaymentType)
|
||||
PaymentSourceType.Stripe.IDEAL -> isIDEALAvailbleForDonateToSignalType(inAppPaymentType)
|
||||
PaymentSourceType.Unknown -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPayPalAvailableForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME, DonateToSignalType.GIFT -> FeatureFlags.paypalOneTimeDonations()
|
||||
DonateToSignalType.MONTHLY -> FeatureFlags.paypalRecurringDonations()
|
||||
private fun isPayPalAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
|
||||
return when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION, InAppPaymentTable.Type.ONE_TIME_GIFT -> FeatureFlags.paypalOneTimeDonations()
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> FeatureFlags.paypalRecurringDonations()
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> FeatureFlags.messageBackups() && FeatureFlags.paypalRecurringDonations()
|
||||
} && !LocaleFeatureFlags.isPayPalDisabled()
|
||||
}
|
||||
|
||||
@@ -81,15 +83,15 @@ object InAppDonations {
|
||||
* Whether the user is in a region which supports SEPA Debit transfers, based off local phone number
|
||||
* and donation type.
|
||||
*/
|
||||
fun isSEPADebitAvailableForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return donateToSignalType != DonateToSignalType.GIFT && isSEPADebitAvailable()
|
||||
fun isSEPADebitAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
|
||||
return inAppPaymentType != InAppPaymentTable.Type.ONE_TIME_GIFT && isSEPADebitAvailable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region which suports IDEAL transfers, based off local phone number and
|
||||
* donation type
|
||||
*/
|
||||
fun isIDEALAvailbleForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return donateToSignalType != DonateToSignalType.GIFT && isIDEALAvailable()
|
||||
fun isIDEALAvailbleForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean {
|
||||
return inAppPaymentType != InAppPaymentTable.Type.ONE_TIME_GIFT && isIDEALAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.squareup.wire.get
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.toDonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobStatus
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver.InAppPaymentObserver
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
import java.security.SecureRandom
|
||||
import java.util.Currency
|
||||
import java.util.Optional
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Unifies legacy access and new access to in app payment data.
|
||||
*/
|
||||
object InAppPaymentsRepository {
|
||||
|
||||
private const val JOB_PREFIX = "InAppPayments__"
|
||||
private val TAG = Log.tag(InAppPaymentsRepository::class.java)
|
||||
|
||||
private val temporaryErrorProcessor = PublishProcessor.create<Pair<InAppPaymentTable.InAppPaymentId, Throwable>>()
|
||||
|
||||
/**
|
||||
* Wraps an in-app-payment update in a completable.
|
||||
*/
|
||||
fun updateInAppPayment(inAppPayment: InAppPaymentTable.InAppPayment): Completable {
|
||||
return Completable.fromAction {
|
||||
SignalDatabase.inAppPayments.update(inAppPayment)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Common logic for handling errors coming from the Rx chains that handle payments. These errors
|
||||
* are analyzed and then either written to the database or dispatched to the temporary error processor.
|
||||
*/
|
||||
fun handlePipelineError(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
donationErrorSource: DonationErrorSource,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
error: Throwable
|
||||
) {
|
||||
if (error is InAppPaymentError) {
|
||||
setErrorIfNotPresent(inAppPaymentId, error.inAppPaymentDataError)
|
||||
return
|
||||
}
|
||||
|
||||
val donationError: DonationError = when (error) {
|
||||
is DonationError -> error
|
||||
is DonationProcessorError -> error.toDonationError(donationErrorSource, paymentSourceType)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(donationErrorSource)
|
||||
}
|
||||
|
||||
val inAppPaymentError = InAppPaymentError.fromDonationError(donationError)?.inAppPaymentDataError
|
||||
if (inAppPaymentError != null) {
|
||||
Log.w(TAG, "Detected a terminal error.")
|
||||
setErrorIfNotPresent(inAppPaymentId, inAppPaymentError).subscribe()
|
||||
} else {
|
||||
Log.w(TAG, "Detected a temporary error.")
|
||||
temporaryErrorProcessor.onNext(inAppPaymentId to donationError)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe a stream of "temporary errors". These are situations in which either the user cancelled out, opened an external application,
|
||||
* or needs to wait a longer time period than 10s for the completion of their payment.
|
||||
*/
|
||||
fun observeTemporaryErrors(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Flowable<Pair<InAppPaymentTable.InAppPaymentId, Throwable>> {
|
||||
return temporaryErrorProcessor.filter { (id, _) -> id == inAppPaymentId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given error to the database, if and only if there is not already an error set.
|
||||
*/
|
||||
private fun setErrorIfNotPresent(inAppPaymentId: InAppPaymentTable.InAppPaymentId, error: InAppPaymentData.Error?): Completable {
|
||||
return Completable.fromAction {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
if (inAppPayment.data.error == null) {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(error = error)
|
||||
)
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Single that can give a snapshot of the given payment, and will throw if it is not found.
|
||||
*/
|
||||
fun requireInAppPayment(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Single<InAppPaymentTable.InAppPayment> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId) ?: throw Exception("Not found.")
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Flowable source of InAppPayments that emits whenever the payment with the given id is updated. This
|
||||
* flowable is primed with the current state.
|
||||
*/
|
||||
fun observeUpdates(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Flowable<InAppPaymentTable.InAppPayment> {
|
||||
return Flowable.create({ emitter ->
|
||||
val observer = InAppPaymentObserver {
|
||||
if (it.id == inAppPaymentId) {
|
||||
emitter.onNext(it)
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerInAppPaymentObserver(observer)
|
||||
emitter.setCancellable {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)?.also {
|
||||
observer.onInAppPaymentChanged(it)
|
||||
}
|
||||
}, BackpressureStrategy.LATEST)
|
||||
}
|
||||
|
||||
/**
|
||||
* For one-time:
|
||||
* - Each job chain is serialized with respect to the in-app-payment ID
|
||||
*
|
||||
* For recurring:
|
||||
* - Each job chain is serialized with respect to the in-app-payment type
|
||||
*/
|
||||
fun resolveJobQueueKey(inAppPayment: InAppPaymentTable.InAppPayment): String {
|
||||
return when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT, InAppPaymentTable.Type.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
|
||||
InAppPaymentTable.Type.RECURRING_DONATION, InAppPaymentTable.Type.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a duration to utilize for jobs tied to different payment methods. For long running bank transfers, we need to
|
||||
* allow extra time for completion.
|
||||
*/
|
||||
fun resolveContextJobLifespan(inAppPayment: InAppPaymentTable.InAppPayment): Duration {
|
||||
return when (inAppPayment.data.paymentMethodType) {
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT, InAppPaymentData.PaymentMethodType.IDEAL -> 30.days
|
||||
else -> 1.days
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object to utilize as a mutex for recurring subscriptions.
|
||||
*/
|
||||
fun resolveMutex(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Any {
|
||||
val payment = SignalDatabase.inAppPayments.getById(inAppPaymentId) ?: error("Not found")
|
||||
|
||||
return payment.type.requireSubscriberType()
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a payment type into a request code for grabbing a Google Pay token.
|
||||
*/
|
||||
fun getGooglePayRequestCode(inAppPaymentType: InAppPaymentTable.Type): Int {
|
||||
return when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> 16143
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> 16141
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> 16142
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> 16144
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an error source to a persistable type. For types that don't map,
|
||||
* UNKNOWN is returned.
|
||||
*/
|
||||
fun DonationErrorSource.toInAppPaymentType(): InAppPaymentTable.Type {
|
||||
return when (this) {
|
||||
DonationErrorSource.ONE_TIME -> InAppPaymentTable.Type.ONE_TIME_DONATION
|
||||
DonationErrorSource.MONTHLY -> InAppPaymentTable.Type.RECURRING_DONATION
|
||||
DonationErrorSource.GIFT -> InAppPaymentTable.Type.ONE_TIME_GIFT
|
||||
DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentTable.Type.UNKNOWN
|
||||
DonationErrorSource.KEEP_ALIVE -> InAppPaymentTable.Type.UNKNOWN
|
||||
DonationErrorSource.UNKNOWN -> InAppPaymentTable.Type.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the structured payment source type into a type we can write to the database.
|
||||
*/
|
||||
fun PaymentSourceType.toPaymentMethodType(): InAppPaymentData.PaymentMethodType {
|
||||
return when (this) {
|
||||
PaymentSourceType.PayPal -> InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
PaymentSourceType.Stripe.CreditCard -> InAppPaymentData.PaymentMethodType.CARD
|
||||
PaymentSourceType.Stripe.GooglePay -> InAppPaymentData.PaymentMethodType.GOOGLE_PAY
|
||||
PaymentSourceType.Stripe.IDEAL -> InAppPaymentData.PaymentMethodType.IDEAL
|
||||
PaymentSourceType.Stripe.SEPADebit -> InAppPaymentData.PaymentMethodType.SEPA_DEBIT
|
||||
PaymentSourceType.Unknown -> InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the database payment method type to the structured sealed type
|
||||
*/
|
||||
fun InAppPaymentData.PaymentMethodType.toPaymentSourceType(): PaymentSourceType {
|
||||
return when (this) {
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> PaymentSourceType.PayPal
|
||||
InAppPaymentData.PaymentMethodType.CARD -> PaymentSourceType.Stripe.CreditCard
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> PaymentSourceType.Stripe.GooglePay
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> PaymentSourceType.Stripe.IDEAL
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> PaymentSourceType.Stripe.SEPADebit
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> PaymentSourceType.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts network ChargeFailure objects into the form we can persist in the database.
|
||||
*/
|
||||
fun ActiveSubscription.ChargeFailure.toInAppPaymentDataChargeFailure(): InAppPaymentData.ChargeFailure {
|
||||
return InAppPaymentData.ChargeFailure(
|
||||
code = this.code ?: "",
|
||||
message = this.message ?: "",
|
||||
outcomeNetworkStatus = outcomeNetworkStatus ?: "",
|
||||
outcomeNetworkReason = outcomeNetworkReason ?: "",
|
||||
outcomeType = outcomeType ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts our database persistable ChargeFailure objects into the form we expect from the network.
|
||||
*/
|
||||
fun InAppPaymentData.ChargeFailure.toActiveSubscriptionChargeFailure(): ActiveSubscription.ChargeFailure {
|
||||
return ActiveSubscription.ChargeFailure(
|
||||
code,
|
||||
message,
|
||||
outcomeNetworkStatus,
|
||||
outcomeNetworkReason,
|
||||
outcomeType
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the latest payment method type, biasing the result towards what is available in the database and falling
|
||||
* back on information in SignalStore. This information is utilized in some error presentation as well as in subscription
|
||||
* updates.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getLatestPaymentMethodType(subscriberType: InAppPaymentSubscriberRecord.Type): InAppPaymentData.PaymentMethodType {
|
||||
val paymentMethodType = getSubscriber(subscriberType)?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
return if (paymentMethodType != InAppPaymentData.PaymentMethodType.UNKNOWN) {
|
||||
paymentMethodType
|
||||
} else if (subscriberType == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.donationsValues().getSubscriptionPaymentSourceType().toPaymentMethodType()
|
||||
} else {
|
||||
return InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the latest subscription was manually cancelled by the user. We bias towards what the database tells us and
|
||||
* fall back on the SignalStore value (which is deprecated and will be removed in a future release)
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun isUserManuallyCancelled(subscriberType: InAppPaymentSubscriberRecord.Type): Boolean {
|
||||
val subscriber = getSubscriber(subscriberType) ?: return false
|
||||
val latestSubscription = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(subscriber.type.inAppPaymentType)
|
||||
|
||||
return if (latestSubscription == null) {
|
||||
SignalStore.donationsValues().isUserManuallyCancelled()
|
||||
} else {
|
||||
latestSubscription.data.cancellation?.reason == InAppPaymentData.Cancellation.Reason.MANUAL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether we should force a cancellation before our next subscription attempt. This is to help clean up
|
||||
* bad state in some edge cases.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun setShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriber: InAppPaymentSubscriberRecord, shouldCancel: Boolean) {
|
||||
if (subscriber.type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt = shouldCancel
|
||||
}
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.setRequiresCancel(
|
||||
subscriberId = subscriber.subscriberId,
|
||||
requiresCancel = shouldCancel
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether or not we should force a cancel before next subscribe attempt for in app payments of the given
|
||||
* type. This method will first check the database, and then fall back on the deprecated SignalStore value.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun getShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriberType: InAppPaymentSubscriberRecord.Type): Boolean {
|
||||
val latestSubscriber = getSubscriber(subscriberType)
|
||||
|
||||
return latestSubscriber?.requiresCancel ?: if (subscriberType == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs a subscriber based off the type and currency
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("DiscouragedApi")
|
||||
@WorkerThread
|
||||
fun getSubscriber(currency: Currency, type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
val subscriber = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode(currency.currencyCode, type)
|
||||
|
||||
return if (subscriber == null && type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.donationsValues().getSubscriber(currency)
|
||||
} else {
|
||||
subscriber
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the "active" subscriber according to the selected currency in the value store.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun getSubscriber(type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
val currency = SignalStore.donationsValues().getSubscriptionCurrency(type)
|
||||
|
||||
return getSubscriber(currency, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a non-null subscriber for the given type, or throws.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun requireSubscriber(type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord {
|
||||
return requireNotNull(getSubscriber(type))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the subscriber, writing them to the database.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun setSubscriber(subscriber: InAppPaymentSubscriberRecord) {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a pending donation exists either in the database or via the legacy job watcher.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun hasPendingDonation(): Boolean {
|
||||
return SignalDatabase.inAppPayments.hasPendingDonation() || DonationRedemptionJobWatcher.hasPendingRedemptionJob()
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a stream of status updates for donations of the given type. Only One-time donations and recurring donations are currently supported.
|
||||
*/
|
||||
fun observeInAppPaymentRedemption(type: InAppPaymentTable.Type): Observable<DonationRedemptionJobStatus> {
|
||||
val jobStatusObservable: Observable<DonationRedemptionJobStatus> = when (type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> Observable.empty()
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> Observable.empty()
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> DonationRedemptionJobWatcher.watchOneTimeRedemption()
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> DonationRedemptionJobWatcher.watchSubscriptionRedemption()
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> Observable.empty()
|
||||
}
|
||||
|
||||
val fromDatabase: Observable<DonationRedemptionJobStatus> = Observable.create { emitter ->
|
||||
val observer = InAppPaymentObserver {
|
||||
val latestInAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(type)
|
||||
|
||||
emitter.onNext(Optional.ofNullable(latestInAppPayment))
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerInAppPaymentObserver(observer)
|
||||
emitter.setCancellable { ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer) }
|
||||
}.switchMap { inAppPaymentOptional ->
|
||||
val inAppPayment = inAppPaymentOptional.getOrNull() ?: return@switchMap jobStatusObservable
|
||||
|
||||
val value = when (inAppPayment.state) {
|
||||
InAppPaymentTable.State.CREATED -> error("This should have been filtered out.")
|
||||
InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION -> {
|
||||
DonationRedemptionJobStatus.PendingExternalVerification(
|
||||
pendingOneTimeDonation = inAppPayment.toPendingOneTimeDonation(),
|
||||
nonVerifiedMonthlyDonation = inAppPayment.toNonVerifiedMonthlyDonation()
|
||||
)
|
||||
}
|
||||
InAppPaymentTable.State.PENDING -> {
|
||||
if (inAppPayment.data.redemption?.stage == InAppPaymentData.RedemptionState.Stage.REDEMPTION_STARTED) {
|
||||
DonationRedemptionJobStatus.PendingReceiptRedemption
|
||||
} else {
|
||||
DonationRedemptionJobStatus.PendingReceiptRequest
|
||||
}
|
||||
}
|
||||
InAppPaymentTable.State.END -> {
|
||||
if (type.recurring && inAppPayment.data.error != null) {
|
||||
DonationRedemptionJobStatus.FailedSubscription
|
||||
} else {
|
||||
DonationRedemptionJobStatus.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Observable.just(value)
|
||||
}
|
||||
|
||||
return fromDatabase
|
||||
.switchMap {
|
||||
if (it == DonationRedemptionJobStatus.None) {
|
||||
jobStatusObservable
|
||||
} else {
|
||||
Observable.just(it)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun InAppPaymentTable.InAppPayment.toPendingOneTimeDonation(): PendingOneTimeDonation? {
|
||||
if (type.recurring) {
|
||||
return null
|
||||
}
|
||||
|
||||
return PendingOneTimeDonation(
|
||||
paymentMethodType = when (data.paymentMethodType) {
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.CARD -> PendingOneTimeDonation.PaymentMethodType.CARD
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> PendingOneTimeDonation.PaymentMethodType.SEPA_DEBIT
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> PendingOneTimeDonation.PaymentMethodType.IDEAL
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> PendingOneTimeDonation.PaymentMethodType.PAYPAL
|
||||
},
|
||||
amount = data.amount!!,
|
||||
badge = data.badge!!,
|
||||
timestamp = insertedAt.inWholeMilliseconds,
|
||||
error = null,
|
||||
pendingVerification = true,
|
||||
checkedVerification = data.waitForAuth!!.checkedVerification
|
||||
)
|
||||
}
|
||||
|
||||
private fun InAppPaymentTable.InAppPayment.toNonVerifiedMonthlyDonation(): NonVerifiedMonthlyDonation? {
|
||||
if (!type.recurring) {
|
||||
return null
|
||||
}
|
||||
|
||||
return NonVerifiedMonthlyDonation(
|
||||
timestamp = insertedAt.inWholeMilliseconds,
|
||||
price = data.amount!!.toFiatMoney(),
|
||||
level = data.level.toInt(),
|
||||
checkedVerification = data.waitForAuth!!.checkedVerification
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new request credential that can be used to retrieve a presentation that can be submitted to get a badge or backup.
|
||||
*/
|
||||
fun generateRequestCredential(): ReceiptCredentialRequestContext {
|
||||
Log.d(TAG, "Generating request credentials context for token redemption...", true)
|
||||
val secureRandom = SecureRandom()
|
||||
val randomBytes = Util.getSecretBytes(ReceiptSerial.SIZE)
|
||||
|
||||
return try {
|
||||
val receiptSerial = ReceiptSerial(randomBytes)
|
||||
val operations = ApplicationDependencies.getClientZkReceiptOperations()
|
||||
operations.createReceiptCredentialRequestContext(secureRandom, receiptSerial)
|
||||
} catch (e: InvalidInputException) {
|
||||
Log.e(TAG, "Failed to create credential.", e)
|
||||
throw AssertionError(e)
|
||||
} catch (e: VerificationFailedException) {
|
||||
Log.e(TAG, "Failed to create credential.", e)
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common logic for building failures based off payment failure state.
|
||||
*/
|
||||
fun buildPaymentFailure(inAppPayment: InAppPaymentTable.InAppPayment, chargeFailure: ActiveSubscription.ChargeFailure?): InAppPaymentData.Error {
|
||||
val builder = InAppPaymentData.Error.Builder()
|
||||
|
||||
if (chargeFailure == null) {
|
||||
builder.type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
val donationProcessor = inAppPayment.data.paymentMethodType.toDonationProcessor()
|
||||
if (donationProcessor == DonationProcessor.PAYPAL) {
|
||||
builder.type = InAppPaymentData.Error.Type.PAYPAL_CODED_ERROR
|
||||
builder.data_ = chargeFailure.code
|
||||
} else {
|
||||
val declineCode = StripeDeclineCode.getFromCode(chargeFailure.outcomeNetworkReason)
|
||||
val failureCode = StripeFailureCode.getFromCode(chargeFailure.code)
|
||||
|
||||
if (failureCode.isKnown) {
|
||||
builder.type = InAppPaymentData.Error.Type.STRIPE_FAILURE
|
||||
builder.data_ = failureCode.toString()
|
||||
} else if (declineCode.isKnown()) {
|
||||
builder.type = InAppPaymentData.Error.Type.STRIPE_DECLINED_ERROR
|
||||
builder.data_ = declineCode.toString()
|
||||
} else {
|
||||
builder.type = InAppPaymentData.Error.Type.STRIPE_CODED_ERROR
|
||||
builder.data_ = chargeFailure.code
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a payment method type into the processor that manages it, either Stripe or PayPal.
|
||||
*/
|
||||
fun InAppPaymentData.PaymentMethodType.toDonationProcessor(): DonationProcessor {
|
||||
return when (this) {
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> DonationProcessor.STRIPE
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> DonationProcessor.STRIPE
|
||||
InAppPaymentData.PaymentMethodType.CARD -> DonationProcessor.STRIPE
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> DonationProcessor.STRIPE
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> DonationProcessor.STRIPE
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> DonationProcessor.PAYPAL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,31 +7,29 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
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.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
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.jobs.InAppPaymentOneTimeContextJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OneTimeDonationRepository(private val donationsService: DonationsService) {
|
||||
class OneTimeInAppPaymentRepository(private val donationsService: DonationsService) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(OneTimeDonationRepository::class.java)
|
||||
private val TAG = Log.tag(OneTimeInAppPaymentRepository::class.java)
|
||||
|
||||
fun <T : Any> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: PaymentSourceType): Single<T> {
|
||||
return if (throwable is DonationError) {
|
||||
@@ -50,7 +48,7 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
|
||||
if (recipient.isSelf) {
|
||||
Log.d(TAG, "Cannot send a gift to self.", true)
|
||||
throw DonationError.GiftRecipientVerificationError.SelectedRecipientDoesNotSupportGifts
|
||||
throw DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid
|
||||
}
|
||||
|
||||
if (recipient.isGroup || recipient.isDistributionList || recipient.registered != RecipientTable.RegisteredState.REGISTERED) {
|
||||
@@ -93,20 +91,25 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
|
||||
fun waitForOneTimeRedemption(
|
||||
gatewayRequest: GatewayRequest,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
paymentIntentId: String,
|
||||
donationProcessor: DonationProcessor,
|
||||
paymentSourceType: PaymentSourceType
|
||||
): Completable {
|
||||
val isLongRunning = paymentSourceType == PaymentSourceType.Stripe.SEPADebit
|
||||
val isBoost = gatewayRequest.recipientId == Recipient.self().id
|
||||
val isBoost = inAppPayment.data.recipientId?.let { RecipientId.from(it) } == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.ONE_TIME else DonationErrorSource.GIFT
|
||||
|
||||
val waitOnRedemption = Completable.create {
|
||||
val timeoutError: DonationError = if (isLongRunning) {
|
||||
BadgeRedemptionError.DonationPending(donationErrorSource, inAppPayment)
|
||||
} else {
|
||||
BadgeRedemptionError.TimeoutWaitingForTokenError(donationErrorSource)
|
||||
}
|
||||
|
||||
return Single.fromCallable {
|
||||
val donationReceiptRecord = if (isBoost) {
|
||||
DonationReceiptRecord.createForBoost(gatewayRequest.fiat)
|
||||
DonationReceiptRecord.createForBoost(inAppPayment.data.amount!!.toFiatMoney())
|
||||
} else {
|
||||
DonationReceiptRecord.createForGift(gatewayRequest.fiat)
|
||||
DonationReceiptRecord.createForGift(inAppPayment.data.amount!!.toFiatMoney())
|
||||
}
|
||||
|
||||
val donationTypeLabel = donationReceiptRecord.type.code.replaceFirstChar { c -> if (c.isLowerCase()) c.titlecase(Locale.US) else c.toString() }
|
||||
@@ -114,66 +117,30 @@ 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(
|
||||
DonationSerializationHelper.createPendingOneTimeDonationProto(
|
||||
gatewayRequest.badge,
|
||||
paymentSourceType,
|
||||
gatewayRequest.fiat
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment = inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT,
|
||||
paymentIntentId = paymentIntentId
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val terminalDonation = TerminalDonationQueue.TerminalDonation(
|
||||
level = gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = isLongRunning
|
||||
)
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val chain = if (isBoost) {
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor, gatewayRequest.uiSessionKey, terminalDonation)
|
||||
} else {
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, gatewayRequest.recipientId, gatewayRequest.additionalMessage, gatewayRequest.level, donationProcessor, gatewayRequest.uiSessionKey, terminalDonation)
|
||||
InAppPaymentOneTimeContextJob.createJobChain(inAppPayment).enqueue()
|
||||
inAppPayment.id
|
||||
}.flatMap { inAppPaymentId ->
|
||||
Log.d(TAG, "Awaiting completion of redemption chain for up to 10 seconds.", true)
|
||||
InAppPaymentsRepository.observeUpdates(inAppPaymentId).filter {
|
||||
it.state == InAppPaymentTable.State.END
|
||||
}.take(1).firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError))
|
||||
}.map {
|
||||
if (it.data.error != null) {
|
||||
Log.d(TAG, "Failure during redemption chain.", true)
|
||||
throw DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
|
||||
chain.enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
val timeoutError: DonationError = if (isLongRunning) {
|
||||
DonationError.donationPending(donationErrorSource, gatewayRequest)
|
||||
} else {
|
||||
DonationError.timeoutWaitingForToken(donationErrorSource)
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.d(TAG, "$donationTypeLabel request response job chain succeeded.", true)
|
||||
it.onComplete()
|
||||
}
|
||||
JobTracker.JobState.FAILURE -> {
|
||||
Log.d(TAG, "$donationTypeLabel request response job chain failed permanently.", true)
|
||||
it.onError(DonationError.genericBadgeRedemptionFailure(donationErrorSource))
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "$donationTypeLabel request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "$donationTypeLabel job chain timed out waiting for job completion.", true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "$donationTypeLabel job chain interrupted", e, true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
|
||||
return waitOnRedemption
|
||||
it
|
||||
}.ignoreElement()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationResult
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse
|
||||
@@ -29,7 +31,7 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
private val TAG = Log.tag(PayPalRepository::class.java)
|
||||
}
|
||||
|
||||
private val monthlyDonationRepository = MonthlyDonationRepository(donationsService)
|
||||
private val recurringInAppPaymentRepository = RecurringInAppPaymentRepository(donationsService)
|
||||
|
||||
fun createOneTimePaymentIntent(
|
||||
amount: FiatMoney,
|
||||
@@ -48,7 +50,7 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
)
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.onErrorResumeNext { OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, PaymentSourceType.PayPal) }
|
||||
.onErrorResumeNext { OneTimeInAppPaymentRepository.handleCreatePaymentIntentError(it, badgeRecipient, PaymentSourceType.PayPal) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -76,34 +78,42 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
* it means that the PaymentMethod is already tied to a Stripe account. We can retry in this
|
||||
* situation by simply deleting the old subscriber id on the service and replacing it.
|
||||
*/
|
||||
fun createPaymentMethod(retryOn409: Boolean = true): Single<PayPalCreatePaymentMethodResponse> {
|
||||
fun createPaymentMethod(subscriberType: InAppPaymentSubscriberRecord.Type, retryOn409: Boolean = true): Single<PayPalCreatePaymentMethodResponse> {
|
||||
return Single.fromCallable {
|
||||
donationsService.createPayPalPaymentMethod(
|
||||
Locale.getDefault(),
|
||||
SignalStore.donationsValues().requireSubscriber().subscriberId,
|
||||
InAppPaymentsRepository.requireSubscriber(subscriberType).subscriberId,
|
||||
MONTHLY_RETURN_URL,
|
||||
CANCEL_URL
|
||||
)
|
||||
}.flatMap { serviceResponse ->
|
||||
if (retryOn409 && serviceResponse.status == 409) {
|
||||
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(retryOn409 = false))
|
||||
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(subscriberType, retryOn409 = false))
|
||||
} else {
|
||||
serviceResponse.flattenResult()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Setting default payment method...", true)
|
||||
donationsService.setDefaultPayPalPaymentMethod(
|
||||
SignalStore.donationsValues().requireSubscriber().subscriberId,
|
||||
paymentMethodId
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method.", true)
|
||||
Log.d(TAG, "Storing the subscription payment source type locally.", true)
|
||||
SignalStore.donationsValues().setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
fun setDefaultPaymentMethod(subscriberType: InAppPaymentSubscriberRecord.Type, paymentMethodId: String): Completable {
|
||||
return Single
|
||||
.fromCallable { InAppPaymentsRepository.requireSubscriber(subscriberType) }
|
||||
.flatMapCompletable { subscriberRecord ->
|
||||
Single.fromCallable {
|
||||
Log.d(TAG, "Setting default payment method...", true)
|
||||
donationsService.setDefaultPayPalPaymentMethod(
|
||||
subscriberRecord.subscriberId,
|
||||
paymentMethodId
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method.", true)
|
||||
Log.d(TAG, "Storing the subscription payment source type locally.", true)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.setPaymentMethod(
|
||||
subscriberRecord.subscriberId,
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,24 @@ import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.BadgeRedemptionError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.TerminalDonationQueue
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentRecurringContextJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
@@ -29,26 +30,24 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Repository which can query for the user's active subscription as well as a list of available subscriptions,
|
||||
* in the currency indicated.
|
||||
*/
|
||||
class MonthlyDonationRepository(private val donationsService: DonationsService) {
|
||||
class RecurringInAppPaymentRepository(private val donationsService: DonationsService) {
|
||||
|
||||
private val TAG = Log.tag(MonthlyDonationRepository::class.java)
|
||||
|
||||
fun getActiveSubscription(): Single<ActiveSubscription> {
|
||||
val localSubscription = SignalStore.donationsValues().getSubscriber()
|
||||
fun getActiveSubscription(type: InAppPaymentSubscriberRecord.Type): Single<ActiveSubscription> {
|
||||
val localSubscription = InAppPaymentsRepository.getSubscriber(type)
|
||||
return if (localSubscription != null) {
|
||||
Single.fromCallable { donationsService.getSubscription(localSubscription.subscriberId) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<ActiveSubscription>::flattenResult)
|
||||
.doOnSuccess { activeSubscription ->
|
||||
if (activeSubscription.isActive && activeSubscription.activeSubscription.endOfCurrentPeriod > SignalStore.donationsValues().getLastEndOfPeriod()) {
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis())
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -85,23 +84,23 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
* Since PayPal and Stripe can't interoperate, we need to be able to rotate the subscriber ID
|
||||
* in case of failures.
|
||||
*/
|
||||
fun rotateSubscriberId(): Completable {
|
||||
fun rotateSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
|
||||
Log.d(TAG, "Rotating SubscriberId due to alternate payment processor...", true)
|
||||
val cancelCompletable: Completable = if (SignalStore.donationsValues().getSubscriber() != null) {
|
||||
cancelActiveSubscription().andThen(updateLocalSubscriptionStateAndScheduleDataSync())
|
||||
val cancelCompletable: Completable = if (InAppPaymentsRepository.getSubscriber(subscriberType) != null) {
|
||||
cancelActiveSubscription(subscriberType).andThen(updateLocalSubscriptionStateAndScheduleDataSync(subscriberType))
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
return cancelCompletable.andThen(ensureSubscriberId(isRotation = true))
|
||||
return cancelCompletable.andThen(ensureSubscriberId(subscriberType, isRotation = true))
|
||||
}
|
||||
|
||||
fun ensureSubscriberId(isRotation: Boolean = false): Completable {
|
||||
fun ensureSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type, isRotation: Boolean = false): Completable {
|
||||
Log.d(TAG, "Ensuring SubscriberId exists on Signal service {isRotation?$isRotation}...", true)
|
||||
val subscriberId: SubscriberId = if (isRotation) {
|
||||
SubscriberId.generate()
|
||||
} else {
|
||||
SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate()
|
||||
InAppPaymentsRepository.getSubscriber(subscriberType)?.subscriberId ?: SubscriberId.generate()
|
||||
}
|
||||
|
||||
return Single
|
||||
@@ -113,20 +112,27 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
.doOnComplete {
|
||||
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
|
||||
|
||||
SignalStore
|
||||
.donationsValues()
|
||||
.setSubscriber(Subscriber(subscriberId, SignalStore.donationsValues().getSubscriptionCurrency().currencyCode))
|
||||
InAppPaymentsRepository.setSubscriber(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = subscriberId,
|
||||
currencyCode = SignalStore.donationsValues().getSubscriptionCurrency(subscriberType).currencyCode,
|
||||
type = subscriberType,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
)
|
||||
)
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelActiveSubscription(): Completable {
|
||||
fun cancelActiveSubscription(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
|
||||
Log.d(TAG, "Canceling active subscription...", true)
|
||||
val localSubscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
return Single
|
||||
.fromCallable {
|
||||
val localSubscriber = InAppPaymentsRepository.requireSubscriber(subscriberType)
|
||||
|
||||
donationsService.cancelSubscription(localSubscriber.subscriberId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -135,12 +141,12 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
.doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) }
|
||||
}
|
||||
|
||||
fun cancelActiveSubscriptionIfNecessary(): Completable {
|
||||
return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable {
|
||||
fun cancelActiveSubscriptionIfNecessary(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
|
||||
return Single.fromCallable { InAppPaymentsRepository.getShouldCancelSubscriptionBeforeNextSubscribeAttempt(subscriberType) }.flatMapCompletable {
|
||||
if (it) {
|
||||
Log.d(TAG, "Cancelling active subscription...", true)
|
||||
cancelActiveSubscription().doOnComplete {
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
cancelActiveSubscription(subscriberType).doOnComplete {
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
}
|
||||
} else {
|
||||
@@ -149,13 +155,37 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(gatewayRequest: GatewayRequest, isLongRunning: Boolean): Completable {
|
||||
val subscriptionLevel = gatewayRequest.level.toString()
|
||||
val uiSessionKey = gatewayRequest.uiSessionKey
|
||||
fun getPaymentSourceTypeOfLatestSubscription(subscriberType: InAppPaymentSubscriberRecord.Type): Single<PaymentSourceType> {
|
||||
return Single.fromCallable {
|
||||
InAppPaymentsRepository.getLatestPaymentMethodType(subscriberType).toPaymentSourceType()
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceType: PaymentSourceType): Completable {
|
||||
val subscriptionLevel = inAppPayment.data.level.toString()
|
||||
val isLongRunning = paymentSourceType.isBankTransfer
|
||||
val subscriberType = inAppPayment.type.requireSubscriberType()
|
||||
val errorSource = subscriberType.inAppPaymentType.toErrorSource()
|
||||
|
||||
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||
.flatMapCompletable { levelUpdateOperation ->
|
||||
val subscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
val subscriber = InAppPaymentsRepository.requireSubscriber(subscriberType)
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment = inAppPayment.copy(
|
||||
subscriberId = subscriber.subscriberId,
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val timeoutError = if (isLongRunning) {
|
||||
BadgeRedemptionError.DonationPending(errorSource, inAppPayment)
|
||||
} else {
|
||||
BadgeRedemptionError.TimeoutWaitingForTokenError(errorSource)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true)
|
||||
Single
|
||||
@@ -165,13 +195,13 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
subscriptionLevel,
|
||||
subscriber.currencyCode,
|
||||
levelUpdateOperation.idempotencyKey.serialize(),
|
||||
SubscriptionReceiptRequestResponseJob.MUTEX
|
||||
subscriberType
|
||||
)
|
||||
}
|
||||
.flatMapCompletable {
|
||||
if (it.status == 200 || it.status == 204) {
|
||||
Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true)
|
||||
SignalStore.donationsValues().updateLocalStateForLocalSubscribe()
|
||||
SignalStore.donationsValues().updateLocalStateForLocalSubscribe(subscriberType)
|
||||
syncAccountRecord().subscribe()
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
Completable.complete()
|
||||
@@ -186,54 +216,24 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
it.flattenResult().ignoreElement()
|
||||
}
|
||||
}.andThen {
|
||||
Log.d(TAG, "Enqueuing request response job chain.", true)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
|
||||
val terminalDonation = TerminalDonationQueue.TerminalDonation(
|
||||
level = gatewayRequest.level,
|
||||
isLongRunningPaymentMethod = isLongRunning
|
||||
)
|
||||
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain(uiSessionKey, terminalDonation).enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
val timeoutError: DonationError = if (isLongRunning) {
|
||||
DonationError.donationPending(DonationErrorSource.MONTHLY, gatewayRequest)
|
||||
} else {
|
||||
DonationError.timeoutWaitingForToken(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.d(TAG, "Subscription request response job chain succeeded.", true)
|
||||
it.onComplete()
|
||||
}
|
||||
JobTracker.JobState.FAILURE -> {
|
||||
Log.d(TAG, "Subscription request response job chain failed permanently.", true)
|
||||
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY))
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}.andThen(
|
||||
Single.fromCallable {
|
||||
Log.d(TAG, "Enqueuing request response job chain.", true)
|
||||
val freshPayment = SignalDatabase.inAppPayments.getById(inAppPayment.id)!!
|
||||
InAppPaymentRecurringContextJob.createJobChain(freshPayment).enqueue()
|
||||
}.flatMap {
|
||||
Log.d(TAG, "Awaiting completion of redemption chain for up to 10 seconds.", true)
|
||||
InAppPaymentsRepository.observeUpdates(inAppPayment.id).filter {
|
||||
it.state == InAppPaymentTable.State.END
|
||||
}.take(1).map {
|
||||
if (it.data.error != null) {
|
||||
Log.d(TAG, "Failure during redemption chain.", true)
|
||||
throw DonationError.genericBadgeRedemptionFailure(errorSource)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Subscription request response job timed out.", true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Subscription request response interrupted.", e, true)
|
||||
it.onError(timeoutError)
|
||||
}
|
||||
}
|
||||
it
|
||||
}.firstOrError()
|
||||
}.timeout(10, TimeUnit.SECONDS, Single.error(timeoutError)).ignoreElement()
|
||||
)
|
||||
}.doOnError {
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
@@ -244,6 +244,8 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RecurringInAppPaymentRepository::class.java)
|
||||
|
||||
fun getOrCreateLevelUpdateOperation(tag: String, subscriptionLevel: String): LevelUpdateOperation {
|
||||
Log.d(tag, "Retrieving level update operation for $subscriptionLevel")
|
||||
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel)
|
||||
@@ -269,10 +271,10 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
* Update local state information and schedule a storage sync for the change. This method
|
||||
* assumes you've already properly called the DELETE method for the stored ID on the server.
|
||||
*/
|
||||
private fun updateLocalSubscriptionStateAndScheduleDataSync(): Completable {
|
||||
private fun updateLocalSubscriptionStateAndScheduleDataSync(subscriberType: InAppPaymentSubscriberRecord.Type): Completable {
|
||||
return Completable.fromAction {
|
||||
Log.d(TAG, "Marking subscription cancelled...", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
@@ -13,11 +13,13 @@ import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.signal.donations.json.StripeIntentStatus
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError.OneTimeDonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
@@ -43,11 +45,14 @@ import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
* 1. Create a PaymentMethod vai the Stripe API, utilizing the token from Google Pay
|
||||
* 1. Confirm the PaymentIntent via the Stripe API
|
||||
*/
|
||||
class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||
class StripeRepository(
|
||||
activity: Activity,
|
||||
private val subscriberType: InAppPaymentSubscriberRecord.Type = InAppPaymentSubscriberRecord.Type.DONATION
|
||||
) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||
|
||||
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
||||
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
|
||||
private val monthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
private val recurringInAppPaymentRepository = RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService())
|
||||
|
||||
fun isGooglePayAvailable(): Completable {
|
||||
return googlePayApi.queryIsReadyToPay()
|
||||
@@ -96,7 +101,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
|
||||
return stripeApi.createPaymentIntent(price, badgeLevel, paymentSourceType)
|
||||
.onErrorResumeNext {
|
||||
OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
|
||||
OneTimeInAppPaymentRepository.handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
|
||||
}
|
||||
.flatMap { result ->
|
||||
val recipient = Recipient.resolved(badgeRecipient)
|
||||
@@ -104,9 +109,9 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
|
||||
Log.d(TAG, "Created payment intent for $price.", true)
|
||||
when (result) {
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Single.error(DonationError.oneTimeDonationAmountTooSmall(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Single.error(DonationError.oneTimeDonationAmountTooLarge(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Single.error(DonationError.invalidCurrencyForOneTimeDonation(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Single.error(OneTimeDonationError.AmountTooSmallError(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Single.error(OneTimeDonationError.AmountTooLargeError(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Single.error(OneTimeDonationError.InvalidCurrencyError(errorSource))
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> Single.just(result.paymentIntent)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
@@ -165,7 +170,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
* situation by simply deleting the old subscriber id on the service and replacing it.
|
||||
*/
|
||||
private fun createPaymentMethod(paymentSourceType: PaymentSourceType.Stripe, retryOn409: Boolean = true): Single<StripeClientSecret> {
|
||||
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
|
||||
return Single.fromCallable { InAppPaymentsRepository.requireSubscriber(subscriberType) }
|
||||
.flatMap {
|
||||
Single.fromCallable {
|
||||
ApplicationDependencies
|
||||
@@ -175,7 +180,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
}
|
||||
.flatMap { serviceResponse ->
|
||||
if (retryOn409 && serviceResponse.status == 409) {
|
||||
monthlyDonationRepository.rotateSubscriberId().andThen(createPaymentMethod(paymentSourceType, retryOn409 = false))
|
||||
recurringInAppPaymentRepository.rotateSubscriberId(subscriberType).andThen(createPaymentMethod(paymentSourceType, retryOn409 = false))
|
||||
} else {
|
||||
serviceResponse.flattenResult()
|
||||
}
|
||||
@@ -230,24 +235,24 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Getting the subscriber...")
|
||||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
InAppPaymentsRepository.requireSubscriber(subscriberType)
|
||||
}.flatMapCompletable { subscriberRecord ->
|
||||
Log.d(TAG, "Setting default payment method via Signal service...")
|
||||
Single.fromCallable {
|
||||
if (paymentSourceType == PaymentSourceType.Stripe.IDEAL) {
|
||||
ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.setDefaultIdealPaymentMethod(it.subscriberId, setupIntentId)
|
||||
.setDefaultIdealPaymentMethod(subscriberRecord.subscriberId, setupIntentId)
|
||||
} else {
|
||||
ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.setDefaultStripePaymentMethod(it.subscriberId, paymentMethodId)
|
||||
.setDefaultStripePaymentMethod(subscriberRecord.subscriberId, paymentMethodId)
|
||||
}
|
||||
}.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method via Signal service!")
|
||||
Log.d(TAG, "Storing the subscription payment source type locally.")
|
||||
SignalDatabase.inAppPaymentSubscribers.setPaymentMethod(subscriberRecord.subscriberId, paymentSourceType.toPaymentMethodType())
|
||||
}
|
||||
}.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method via Signal service!")
|
||||
Log.d(TAG, "Storing the subscription payment source type locally.")
|
||||
SignalStore.donationsValues().setSubscriptionPaymentSourceType(paymentSourceType)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,21 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment
|
||||
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.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
/**
|
||||
@@ -52,8 +60,30 @@ class TerminalDonationDelegate(
|
||||
val verifiedMonthlyDonation: Stripe3DSData? = SignalStore.donationsValues().consumeVerifiedSubscription3DSData()
|
||||
if (verifiedMonthlyDonation != null) {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(verifiedMonthlyDonation.gatewayRequest).build().toBundle()
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(verifiedMonthlyDonation.inAppPayment).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
}
|
||||
|
||||
handleInAppPaymentSheets()
|
||||
}
|
||||
|
||||
private fun handleInAppPaymentSheets() {
|
||||
lifecycleDisposable += Single.fromCallable {
|
||||
SignalDatabase.inAppPayments.consumeInAppPaymentsToNotifyUser()
|
||||
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeBy { inAppPayments ->
|
||||
for (payment in inAppPayments) {
|
||||
if (payment.data.error == null && payment.state == InAppPaymentTable.State.END) {
|
||||
ThanksForYourSupportBottomSheetDialogFragment()
|
||||
.apply { arguments = ThanksForYourSupportBottomSheetDialogFragmentArgs.Builder(Badges.fromDatabaseBadge(payment.data.badge!!)).build().toBundle() }
|
||||
.show(fragmentManager, null)
|
||||
} else if (payment.data.error != null && payment.state == InAppPaymentTable.State.PENDING) {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(payment).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
} else if (payment.data.error != null && payment.data.cancellation != null && payment.data.cancellation.reason != InAppPaymentData.Cancellation.Reason.MANUAL && SignalStore.donationsValues().showMonthlyDonationCanceledDialog) {
|
||||
MonthlyDonationCanceledBottomSheetDialogFragment.show(fragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
|
||||
private val viewModel: SetCurrencyViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
val args = SetCurrencyFragmentArgs.fromBundle(requireArguments())
|
||||
SetCurrencyViewModel.Factory(args.isBoost, args.supportedCurrencyCodes.toList())
|
||||
SetCurrencyViewModel.Factory(args.inAppPaymentType, args.supportedCurrencyCodes.toList())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,24 +5,27 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
class SetCurrencyViewModel(
|
||||
private val isOneTime: Boolean,
|
||||
private val inAppPaymentType: InAppPaymentTable.Type,
|
||||
supportedCurrencyCodes: List<String>
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(
|
||||
SetCurrencyState(
|
||||
selectedCurrencyCode = if (isOneTime) {
|
||||
SignalStore.donationsValues().getOneTimeCurrency().currencyCode
|
||||
selectedCurrencyCode = if (inAppPaymentType.recurring) {
|
||||
SignalStore.donationsValues().getSubscriptionCurrency(inAppPaymentType.requireSubscriberType()).currencyCode
|
||||
} else {
|
||||
SignalStore.donationsValues().getSubscriptionCurrency().currencyCode
|
||||
SignalStore.donationsValues().getOneTimeCurrency().currencyCode
|
||||
},
|
||||
currencies = supportedCurrencyCodes
|
||||
.map(Currency::getInstance)
|
||||
@@ -35,19 +38,22 @@ class SetCurrencyViewModel(
|
||||
fun setSelectedCurrency(selectedCurrencyCode: String) {
|
||||
store.update { it.copy(selectedCurrencyCode = selectedCurrencyCode) }
|
||||
|
||||
if (isOneTime) {
|
||||
if (!inAppPaymentType.recurring) {
|
||||
SignalStore.donationsValues().setOneTimeCurrency(Currency.getInstance(selectedCurrencyCode))
|
||||
} else {
|
||||
val currency = Currency.getInstance(selectedCurrencyCode)
|
||||
val subscriber = SignalStore.donationsValues().getSubscriber(currency)
|
||||
val subscriber = InAppPaymentsRepository.getSubscriber(currency, inAppPaymentType.requireSubscriberType())
|
||||
|
||||
if (subscriber != null) {
|
||||
SignalStore.donationsValues().setSubscriber(subscriber)
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
} else {
|
||||
SignalStore.donationsValues().setSubscriber(
|
||||
Subscriber(
|
||||
InAppPaymentsRepository.setSubscriber(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currencyCode = currency.currencyCode
|
||||
currencyCode = currency.currencyCode,
|
||||
type = inAppPaymentType.requireSubscriberType(),
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -83,9 +89,9 @@ class SetCurrencyViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val isOneTime: Boolean, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||
class Factory(private val inAppPaymentType: InAppPaymentTable.Type, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(SetCurrencyViewModel(isOneTime, supportedCurrencyCodes))!!
|
||||
return modelClass.cast(SetCurrencyViewModel(inAppPaymentType, supportedCurrencyCodes))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
sealed class DonateToSignalAction {
|
||||
data class DisplayCurrencySelectionDialog(val donateToSignalType: DonateToSignalType, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayGatewaySelectorDialog(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class CancelSubscription(val gatewayRequest: GatewayRequest) : DonateToSignalAction()
|
||||
data class UpdateSubscription(val gatewayRequest: GatewayRequest, val isLongRunning: Boolean) : DonateToSignalAction()
|
||||
data class DisplayCurrencySelectionDialog(val inAppPaymentType: InAppPaymentTable.Type, val supportedCurrencies: List<String>) : DonateToSignalAction()
|
||||
data class DisplayGatewaySelectorDialog(val inAppPayment: InAppPaymentTable.InAppPayment) : DonateToSignalAction()
|
||||
object CancelSubscription : DonateToSignalAction()
|
||||
data class UpdateSubscription(val inAppPayment: InAppPaymentTable.InAppPayment, val isLongRunning: Boolean) : DonateToSignalAction()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
/**
|
||||
* Activity wrapper for donate to signal screen. An activity is needed because Google Pay uses the
|
||||
@@ -20,7 +21,7 @@ class DonateToSignalActivity : FragmentWrapperActivity(), DonationPaymentCompone
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun getFragment(): Fragment {
|
||||
return NavHostFragment.create(R.navigation.donate_to_signal, DonateToSignalFragmentArgs.Builder(DonateToSignalType.ONE_TIME).build().toBundle())
|
||||
return NavHostFragment.create(R.navigation.donate_to_signal, DonateToSignalFragmentArgs.Builder(InAppPaymentTable.Type.ONE_TIME_DONATION).build().toBundle())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.signal.core.util.dp
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -27,12 +28,10 @@ import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.DonateToSignalFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
@@ -68,9 +67,9 @@ class DonateToSignalFragment :
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(donateToSignalType: DonateToSignalType): DialogFragment {
|
||||
fun create(inAppPaymentType: InAppPaymentTable.Type): DialogFragment {
|
||||
return Dialog().apply {
|
||||
arguments = DonateToSignalFragmentArgs.Builder(donateToSignalType).build().toBundle()
|
||||
arguments = DonateToSignalFragmentArgs.Builder(inAppPaymentType).build().toBundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +107,11 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, viewModel.uiSessionKey, DonationErrorSource.ONE_TIME, DonationErrorSource.MONTHLY)
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(
|
||||
this,
|
||||
this,
|
||||
viewModel.inAppPaymentId
|
||||
)
|
||||
|
||||
val recyclerView = this.recyclerView!!
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
@@ -137,7 +140,7 @@ class DonateToSignalFragment :
|
||||
when (action) {
|
||||
is DonateToSignalAction.DisplayCurrencySelectionDialog -> {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToSetDonationCurrencyFragment(
|
||||
action.donateToSignalType == DonateToSignalType.ONE_TIME,
|
||||
action.inAppPaymentType,
|
||||
action.supportedCurrencies.toTypedArray()
|
||||
)
|
||||
|
||||
@@ -145,8 +148,8 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
is DonateToSignalAction.DisplayGatewaySelectorDialog -> {
|
||||
Log.d(TAG, "Presenting gateway selector for ${action.gatewayRequest}")
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.gatewayRequest)
|
||||
Log.d(TAG, "Presenting gateway selector for ${action.inAppPayment}")
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.inAppPayment)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
}
|
||||
@@ -155,7 +158,8 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION,
|
||||
action.gatewayRequest
|
||||
null,
|
||||
InAppPaymentTable.Type.RECURRING_DONATION
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -164,7 +168,8 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION,
|
||||
action.gatewayRequest
|
||||
action.inAppPayment,
|
||||
action.inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -234,7 +239,7 @@ class DonateToSignalFragment :
|
||||
|
||||
customPref(
|
||||
DonationPillToggle.Model(
|
||||
selected = state.donateToSignalType,
|
||||
selected = state.inAppPaymentType,
|
||||
onClick = {
|
||||
viewModel.toggleDonationType()
|
||||
}
|
||||
@@ -243,15 +248,15 @@ class DonateToSignalFragment :
|
||||
|
||||
space(10.dp)
|
||||
|
||||
when (state.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||
DonateToSignalType.GIFT -> error("This fragment does not support gifts.")
|
||||
when (state.inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||
else -> error("This fragment does not support ${state.inAppPaymentType}.")
|
||||
}
|
||||
|
||||
space(20.dp)
|
||||
|
||||
if (state.donateToSignalType == DonateToSignalType.MONTHLY && state.monthlyDonationState.isSubscriptionActive) {
|
||||
if (state.inAppPaymentType == InAppPaymentTable.Type.RECURRING_DONATION && state.monthlyDonationState.isSubscriptionActive) {
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
||||
isEnabled = state.canUpdate,
|
||||
@@ -317,7 +322,7 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
private fun showDonationPendingDialog(state: DonateToSignalState) {
|
||||
val message = if (state.donateToSignalType == DonateToSignalType.ONE_TIME) {
|
||||
val message = if (state.inAppPaymentType == InAppPaymentTable.Type.ONE_TIME_DONATION) {
|
||||
if (state.oneTimeDonationState.isOneTimeDonationLongRunning) {
|
||||
R.string.DonateToSignalFragment__bank_transfers_usually_take_1_business_day_to_process_onetime
|
||||
} else if (state.oneTimeDonationState.isNonVerifiedIdeal) {
|
||||
@@ -437,33 +442,34 @@ class DonateToSignalFragment :
|
||||
}
|
||||
}
|
||||
|
||||
override fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
|
||||
override fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, inAppPayment, inAppPayment.type))
|
||||
}
|
||||
|
||||
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
|
||||
override fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
gatewayRequest
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment))
|
||||
}
|
||||
|
||||
override fun navigateToIdealDetailsFragment(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(gatewayRequest))
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment))
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(gatewayResponse))
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment))
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(gatewayRequest.badge))
|
||||
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToThanksForYourSupportBottomSheetDialog(Badges.fromDatabaseBadge(inAppPayment.data.badge!!)))
|
||||
}
|
||||
|
||||
override fun onProcessorActionProcessed() {
|
||||
@@ -481,7 +487,7 @@ class DonateToSignalFragment :
|
||||
|
||||
override fun onUserLaunchedAnExternalApplication() = Unit
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(gatewayRequest))
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ 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.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.isLongRunning
|
||||
import org.thoughtcrime.securesms.database.model.isPending
|
||||
@@ -16,72 +18,72 @@ import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class DonateToSignalState(
|
||||
val donateToSignalType: DonateToSignalType,
|
||||
val inAppPaymentType: InAppPaymentTable.Type,
|
||||
val oneTimeDonationState: OneTimeDonationState = OneTimeDonationState(),
|
||||
val monthlyDonationState: MonthlyDonationState = MonthlyDonationState()
|
||||
) {
|
||||
|
||||
val areFieldsEnabled: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.donationStage == DonationStage.READY
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.donationStage == DonationStage.READY
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.donationStage == DonationStage.READY
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val badge: Badge?
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.badge
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription?.badge
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.badge
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription?.badge
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canSetCurrency: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && !monthlyDonationState.isSubscriptionActive
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val selectedCurrency: Currency
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectedCurrency
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedCurrency
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectedCurrency
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedCurrency
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val selectableCurrencyCodes: List<String>
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> oneTimeDonationState.selectableCurrencyCodes
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectableCurrencyCodes
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> oneTimeDonationState.selectableCurrencyCodes
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectableCurrencyCodes
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val level: Int
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> 1
|
||||
DonateToSignalType.MONTHLY -> monthlyDonationState.selectedSubscription!!.level
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> 1
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> monthlyDonationState.selectedSubscription!!.level
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val continueEnabled: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> areFieldsEnabled && oneTimeDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid && InAppDonations.hasAtLeastOnePaymentMethodAvailable()
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canContinue: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
DonateToSignalType.MONTHLY -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> continueEnabled && !oneTimeDonationState.isOneTimeDonationPending
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> continueEnabled && !monthlyDonationState.isSubscriptionActive && !monthlyDonationState.transactionState.isInProgress
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val canUpdate: Boolean
|
||||
get() = when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> false
|
||||
DonateToSignalType.MONTHLY -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||
DonateToSignalType.GIFT -> error("This flow does not support gifts")
|
||||
get() = when (inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> false
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> areFieldsEnabled && monthlyDonationState.isSelectionValid
|
||||
else -> error("This flow does not support $inAppPaymentType")
|
||||
}
|
||||
|
||||
val isUpdateLongRunning: Boolean
|
||||
@@ -112,7 +114,7 @@ data class DonateToSignalState(
|
||||
}
|
||||
|
||||
data class MonthlyDonationState(
|
||||
val selectedCurrency: Currency = SignalStore.donationsValues().getSubscriptionCurrency(),
|
||||
val selectedCurrency: Currency = SignalStore.donationsValues().getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION),
|
||||
val subscriptions: List<Subscription> = emptyList(),
|
||||
private val _activeSubscription: ActiveSubscription? = null,
|
||||
val selectedSubscription: Subscription? = null,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
|
||||
@Parcelize
|
||||
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
|
||||
ONE_TIME(16141),
|
||||
MONTHLY(16142),
|
||||
GIFT(16143);
|
||||
|
||||
fun toErrorSource(): DonationErrorSource {
|
||||
return when (this) {
|
||||
ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
MONTHLY -> DonationErrorSource.MONTHLY
|
||||
GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,30 +3,36 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.money.PlatformCurrencyUtil
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
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.DonationRedemptionJobStatus
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.DonationRedemptionJobWatcher
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.isExpired
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
@@ -44,28 +50,30 @@ import java.util.Optional
|
||||
* only in charge of rendering our "current view of the world."
|
||||
*/
|
||||
class DonateToSignalViewModel(
|
||||
startType: DonateToSignalType,
|
||||
private val subscriptionsRepository: MonthlyDonationRepository,
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
||||
startType: InAppPaymentTable.Type,
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository,
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonateToSignalViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(DonateToSignalState(donateToSignalType = startType))
|
||||
private val store = RxStore(DonateToSignalState(inAppPaymentType = startType))
|
||||
private val oneTimeDonationDisposables = CompositeDisposable()
|
||||
private val monthlyDonationDisposables = CompositeDisposable()
|
||||
private val networkDisposable = CompositeDisposable()
|
||||
private val actionDisposable = CompositeDisposable()
|
||||
private val _actions = PublishSubject.create<DonateToSignalAction>()
|
||||
private val _activeSubscription = PublishSubject.create<ActiveSubscription>()
|
||||
private val _inAppPaymentId = BehaviorProcessor.create<InAppPaymentTable.InAppPaymentId>()
|
||||
|
||||
val state = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
val actions: Observable<DonateToSignalAction> = _actions.observeOn(AndroidSchedulers.mainThread())
|
||||
val uiSessionKey: Long = System.currentTimeMillis()
|
||||
val inAppPaymentId: Flowable<InAppPaymentTable.InAppPaymentId> = _inAppPaymentId.onBackpressureLatest().distinctUntilChanged()
|
||||
|
||||
init {
|
||||
initializeOneTimeDonationState(oneTimeDonationRepository)
|
||||
initializeOneTimeDonationState(oneTimeInAppPaymentRepository)
|
||||
initializeMonthlyDonationState(subscriptionsRepository)
|
||||
|
||||
networkDisposable += InternetConnectionObserver
|
||||
@@ -89,45 +97,49 @@ class DonateToSignalViewModel(
|
||||
fun retryOneTimeDonationState() {
|
||||
if (!oneTimeDonationDisposables.isDisposed && store.state.oneTimeDonationState.donationStage == DonateToSignalState.DonationStage.FAILURE) {
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(donationStage = DonateToSignalState.DonationStage.INIT)) }
|
||||
initializeOneTimeDonationState(oneTimeDonationRepository)
|
||||
initializeOneTimeDonationState(oneTimeInAppPaymentRepository)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestChangeCurrency() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.canSetCurrency) {
|
||||
_actions.onNext(DonateToSignalAction.DisplayCurrencySelectionDialog(snapshot.donateToSignalType, snapshot.selectableCurrencyCodes))
|
||||
_actions.onNext(DonateToSignalAction.DisplayCurrencySelectionDialog(snapshot.inAppPaymentType, snapshot.selectableCurrencyCodes))
|
||||
}
|
||||
}
|
||||
|
||||
fun requestSelectGateway() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.DisplayGatewaySelectorDialog(createGatewayRequest(snapshot)))
|
||||
actionDisposable += createInAppPayment(snapshot).subscribeBy {
|
||||
_actions.onNext(DonateToSignalAction.DisplayGatewaySelectorDialog(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscription() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.UpdateSubscription(createGatewayRequest(snapshot), snapshot.isUpdateLongRunning))
|
||||
actionDisposable += createInAppPayment(snapshot).subscribeBy {
|
||||
_actions.onNext(DonateToSignalAction.UpdateSubscription(it, snapshot.isUpdateLongRunning))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
val snapshot = store.state
|
||||
if (snapshot.areFieldsEnabled) {
|
||||
_actions.onNext(DonateToSignalAction.CancelSubscription(createGatewayRequest(snapshot)))
|
||||
_actions.onNext(DonateToSignalAction.CancelSubscription)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDonationType() {
|
||||
store.update {
|
||||
it.copy(
|
||||
donateToSignalType = when (it.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonateToSignalType.MONTHLY
|
||||
DonateToSignalType.MONTHLY -> DonateToSignalType.ONE_TIME
|
||||
DonateToSignalType.GIFT -> error("We are in an illegal state")
|
||||
inAppPaymentType = when (it.inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> InAppPaymentTable.Type.RECURRING_DONATION
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> InAppPaymentTable.Type.ONE_TIME_DONATION
|
||||
else -> error("Should never get here.")
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -169,7 +181,7 @@ class DonateToSignalViewModel(
|
||||
|
||||
fun refreshActiveSubscription() {
|
||||
subscriptionsRepository
|
||||
.getActiveSubscription()
|
||||
.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
.subscribeBy(
|
||||
onSuccess = {
|
||||
_activeSubscription.onNext(it)
|
||||
@@ -180,25 +192,39 @@ class DonateToSignalViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGatewayRequest(snapshot: DonateToSignalState): GatewayRequest {
|
||||
private fun createInAppPayment(snapshot: DonateToSignalState): Single<InAppPaymentTable.InAppPayment> {
|
||||
val amount = getAmount(snapshot)
|
||||
return GatewayRequest(
|
||||
uiSessionKey = uiSessionKey,
|
||||
donateToSignalType = snapshot.donateToSignalType,
|
||||
badge = snapshot.badge!!,
|
||||
label = snapshot.badge!!.description,
|
||||
price = amount.amount,
|
||||
currencyCode = amount.currency.currencyCode,
|
||||
level = snapshot.level.toLong(),
|
||||
recipientId = Recipient.self().id
|
||||
)
|
||||
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.inAppPayments.clearCreated()
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = snapshot.inAppPaymentType,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
badge = snapshot.badge?.let { Badges.toDatabaseBadge(it) },
|
||||
label = snapshot.badge?.description ?: "",
|
||||
amount = amount.toFiatValue(),
|
||||
level = snapshot.level.toLong(),
|
||||
recipientId = Recipient.self().id.serialize(),
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN,
|
||||
redemption = InAppPaymentData.RedemptionState(
|
||||
stage = InAppPaymentData.RedemptionState.Stage.INIT
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
_inAppPaymentId.onNext(id)
|
||||
SignalDatabase.inAppPayments.getById(id)!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAmount(snapshot: DonateToSignalState): FiatMoney {
|
||||
return when (snapshot.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> getSelectedSubscriptionCost()
|
||||
DonateToSignalType.GIFT -> error("This ViewModel does not support gifts.")
|
||||
return when (snapshot.inAppPaymentType) {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> getOneTimeAmount(snapshot.oneTimeDonationState)
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> getSelectedSubscriptionCost()
|
||||
else -> error("This ViewModel does not support ${snapshot.inAppPaymentType}.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +236,8 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOneTimeDonationState(oneTimeDonationRepository: OneTimeDonationRepository) {
|
||||
val oneTimeDonationFromJob: Observable<Optional<PendingOneTimeDonation>> = DonationRedemptionJobWatcher.watchOneTimeRedemption().map {
|
||||
private fun initializeOneTimeDonationState(oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository) {
|
||||
val oneTimeDonationFromJob: Observable<Optional<PendingOneTimeDonation>> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION).map {
|
||||
when (it) {
|
||||
is DonationRedemptionJobStatus.PendingExternalVerification -> Optional.ofNullable(it.pendingOneTimeDonation)
|
||||
|
||||
@@ -238,7 +264,7 @@ class DonateToSignalViewModel(
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(pendingOneTimeDonation = pendingOneTimeDonation.orNull())) }
|
||||
}
|
||||
|
||||
oneTimeDonationDisposables += oneTimeDonationRepository.getBoostBadge().subscribeBy(
|
||||
oneTimeDonationDisposables += oneTimeInAppPaymentRepository.getBoostBadge().subscribeBy(
|
||||
onSuccess = { badge ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(badge = badge)) }
|
||||
},
|
||||
@@ -247,7 +273,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
)
|
||||
|
||||
oneTimeDonationDisposables += oneTimeDonationRepository.getMinimumDonationAmounts().subscribeBy(
|
||||
oneTimeDonationDisposables += oneTimeInAppPaymentRepository.getMinimumDonationAmounts().subscribeBy(
|
||||
onSuccess = { amountMap ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(minimumDonationAmounts = amountMap)) }
|
||||
},
|
||||
@@ -256,7 +282,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
)
|
||||
|
||||
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeDonationRepository.getBoosts().toObservable()
|
||||
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeInAppPaymentRepository.getBoosts().toObservable()
|
||||
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
||||
|
||||
oneTimeDonationDisposables += Observable.combineLatest(boosts, oneTimeCurrency) { boostMap, currency ->
|
||||
@@ -294,7 +320,7 @@ class DonateToSignalViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private fun initializeMonthlyDonationState(subscriptionsRepository: MonthlyDonationRepository) {
|
||||
private fun initializeMonthlyDonationState(subscriptionsRepository: RecurringInAppPaymentRepository) {
|
||||
monitorLevelUpdateProcessing()
|
||||
|
||||
val allSubscriptions = subscriptionsRepository.getSubscriptions()
|
||||
@@ -305,7 +331,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun monitorLevelUpdateProcessing() {
|
||||
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = DonationRedemptionJobWatcher.watchSubscriptionRedemption()
|
||||
val redemptionJobStatus: Observable<DonationRedemptionJobStatus> = InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION)
|
||||
|
||||
monthlyDonationDisposables += Observable
|
||||
.combineLatest(redemptionJobStatus, LevelUpdate.isProcessing, ::Pair)
|
||||
@@ -361,13 +387,13 @@ class DonateToSignalViewModel(
|
||||
onSuccess = { subscriptions ->
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
||||
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
|
||||
if (selectedCurrency !in priceCurrencies) {
|
||||
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $selectedCurrency isn't supported.")
|
||||
val usd = PlatformCurrencyUtil.USD
|
||||
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
||||
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
||||
val newSubscriber = InAppPaymentsRepository.getSubscriber(usd, InAppPaymentSubscriberRecord.Type.DONATION) ?: InAppPaymentSubscriberRecord(SubscriberId.generate(), usd.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN)
|
||||
InAppPaymentsRepository.setSubscriber(newSubscriber)
|
||||
subscriptionsRepository.syncAccountRecord().subscribe()
|
||||
}
|
||||
}
|
||||
@@ -377,7 +403,7 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
|
||||
private fun monitorSubscriptionCurrency() {
|
||||
monthlyDonationDisposables += SignalStore.donationsValues().observableSubscriptionCurrency.subscribe {
|
||||
monthlyDonationDisposables += SignalStore.donationsValues().observableRecurringDonationCurrency.subscribe {
|
||||
store.update { state ->
|
||||
state.copy(monthlyDonationState = state.monthlyDonationState.copy(selectedCurrency = it))
|
||||
}
|
||||
@@ -389,16 +415,17 @@ class DonateToSignalViewModel(
|
||||
oneTimeDonationDisposables.clear()
|
||||
monthlyDonationDisposables.clear()
|
||||
networkDisposable.clear()
|
||||
actionDisposable.clear()
|
||||
store.dispose()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val startType: DonateToSignalType,
|
||||
private val subscriptionsRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
private val startType: InAppPaymentTable.Type,
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, oneTimeDonationRepository)) as T
|
||||
return modelClass.cast(DonateToSignalViewModel(startType, subscriptionsRepository, oneTimeInAppPaymentRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import com.google.android.gms.wallet.PaymentData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
@@ -22,10 +24,11 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
@@ -34,12 +37,12 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.tr
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* Abstracts out some common UI-level interactions between gift flow and normal donate flow.
|
||||
@@ -47,9 +50,7 @@ import java.util.Currency
|
||||
class DonationCheckoutDelegate(
|
||||
private val fragment: Fragment,
|
||||
private val callback: Callback,
|
||||
private val uiSessionKey: Long,
|
||||
errorSource: DonationErrorSource,
|
||||
vararg additionalSources: DonationErrorSource
|
||||
inAppPaymentIdSource: Flowable<InAppPaymentTable.InAppPaymentId>
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
@@ -70,7 +71,7 @@ class DonationCheckoutDelegate(
|
||||
|
||||
init {
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
ErrorHandler().attach(fragment, callback, uiSessionKey, errorSource, *additionalSources)
|
||||
ErrorHandler().attach(fragment, callback, inAppPaymentIdSource)
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
@@ -82,8 +83,8 @@ class DonationCheckoutDelegate(
|
||||
if (bundle.containsKey(GatewaySelectorBottomSheet.FAILURE_KEY)) {
|
||||
callback.showSepaEuroMaximumDialog(FiatMoney(bundle.getSerializable(GatewaySelectorBottomSheet.SEPA_EURO_MAX) as BigDecimal, CurrencyUtil.EURO))
|
||||
} else {
|
||||
val response: GatewayResponse = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, GatewayResponse::class.java)!!
|
||||
handleGatewaySelectionResponse(response)
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(GatewaySelectorBottomSheet.REQUEST_KEY, InAppPaymentTable.InAppPayment::class.java)!!
|
||||
handleGatewaySelectionResponse(inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +104,8 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(BankTransferRequestKeys.PENDING_KEY) { _, bundle ->
|
||||
val request: GatewayRequest = bundle.getParcelableCompat(BankTransferRequestKeys.PENDING_KEY, GatewayRequest::class.java)!!
|
||||
callback.navigateToDonationPending(gatewayRequest = request)
|
||||
val request: InAppPaymentTable.InAppPayment = bundle.getParcelableCompat(BankTransferRequestKeys.PENDING_KEY, InAppPaymentTable.InAppPayment::class.java)!!
|
||||
callback.navigateToDonationPending(inAppPayment = request)
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
@@ -113,17 +114,18 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
|
||||
if (InAppDonations.isPaymentSourceAvailable(gatewayResponse.gateway.toPaymentSourceType(), gatewayResponse.request.donateToSignalType)) {
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> launchBankTransfer(gatewayResponse)
|
||||
GatewayResponse.Gateway.IDEAL -> launchBankTransfer(gatewayResponse)
|
||||
private fun handleGatewaySelectionResponse(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
if (InAppDonations.isPaymentSourceAvailable(inAppPayment.data.paymentMethodType.toPaymentSourceType(), inAppPayment.type)) {
|
||||
when (inAppPayment.data.paymentMethodType) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> launchGooglePay(inAppPayment)
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> launchPayPal(inAppPayment)
|
||||
InAppPaymentData.PaymentMethodType.CARD -> launchCreditCard(inAppPayment)
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> launchBankTransfer(inAppPayment)
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> launchBankTransfer(inAppPayment)
|
||||
else -> error("Unsupported payment method type")
|
||||
}
|
||||
} else {
|
||||
error("Unsupported combination! ${gatewayResponse.gateway} ${gatewayResponse.request.donateToSignalType}")
|
||||
error("Unsupported combination! ${inAppPayment.data.paymentMethodType} ${inAppPayment.type}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,8 +142,7 @@ class DonationCheckoutDelegate(
|
||||
if (result.action == DonationProcessorAction.CANCEL_SUBSCRIPTION) {
|
||||
Snackbar.make(fragment.requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
SignalStore.donationsValues().removeTerminalDonation(result.request.level)
|
||||
callback.onPaymentComplete(result.request)
|
||||
callback.onPaymentComplete(result.inAppPayment!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,28 +160,28 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchPayPal(gatewayResponse: GatewayResponse) {
|
||||
callback.navigateToPayPalPaymentInProgress(gatewayResponse.request)
|
||||
private fun launchPayPal(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
callback.navigateToPayPalPaymentInProgress(inAppPayment)
|
||||
}
|
||||
|
||||
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
||||
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
|
||||
private fun launchGooglePay(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
viewModel.provideGatewayRequestForGooglePay(inAppPayment)
|
||||
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
|
||||
price = FiatMoney(gatewayResponse.request.price, Currency.getInstance(gatewayResponse.request.currencyCode)),
|
||||
label = gatewayResponse.request.label,
|
||||
requestCode = gatewayResponse.request.donateToSignalType.requestCode.toInt()
|
||||
price = inAppPayment.data.amount!!.toFiatMoney(),
|
||||
label = inAppPayment.data.label,
|
||||
requestCode = InAppPaymentsRepository.getGooglePayRequestCode(inAppPayment.type)
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchCreditCard(gatewayResponse: GatewayResponse) {
|
||||
callback.navigateToCreditCardForm(gatewayResponse.request)
|
||||
private fun launchCreditCard(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
callback.navigateToCreditCardForm(inAppPayment)
|
||||
}
|
||||
|
||||
private fun launchBankTransfer(gatewayResponse: GatewayResponse) {
|
||||
if (gatewayResponse.request.donateToSignalType != DonateToSignalType.MONTHLY && gatewayResponse.gateway == GatewayResponse.Gateway.IDEAL) {
|
||||
callback.navigateToIdealDetailsFragment(gatewayResponse.request)
|
||||
private fun launchBankTransfer(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
if (!inAppPayment.type.recurring && inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) {
|
||||
callback.navigateToIdealDetailsFragment(inAppPayment)
|
||||
} else {
|
||||
callback.navigateToBankTransferMandate(gatewayResponse)
|
||||
callback.navigateToBankTransferMandate(inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,26 +201,26 @@ class DonationCheckoutDelegate(
|
||||
)
|
||||
}
|
||||
|
||||
inner class GooglePayRequestCallback(private val request: GatewayRequest) : GooglePayApi.PaymentRequestCallback {
|
||||
inner class GooglePayRequestCallback(private val inAppPayment: InAppPaymentTable.InAppPayment) : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
Log.d(TAG, "Successfully retrieved payment data from Google Pay", true)
|
||||
stripePaymentViewModel.providePaymentData(paymentData)
|
||||
callback.navigateToStripePaymentInProgress(request)
|
||||
callback.navigateToStripePaymentInProgress(inAppPayment)
|
||||
}
|
||||
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
Log.w(TAG, "Failed to retrieve payment data from Google Pay", googlePayException, true)
|
||||
|
||||
val error = DonationError.getGooglePayRequestTokenError(
|
||||
source = when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
},
|
||||
throwable = googlePayException
|
||||
)
|
||||
|
||||
DonationError.routeDonationError(fragment.requireContext(), error)
|
||||
InAppPaymentsRepository.updateInAppPayment(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
data = inAppPayment.data.copy(
|
||||
error = InAppPaymentData.Error(
|
||||
type = InAppPaymentData.Error.Type.GOOGLE_PAY_REQUEST_TOKEN
|
||||
)
|
||||
)
|
||||
)
|
||||
).subscribe()
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
@@ -236,7 +237,19 @@ class DonationCheckoutDelegate(
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private var errorHandlerCallback: ErrorHandlerCallback? = null
|
||||
|
||||
fun attach(fragment: Fragment, errorHandlerCallback: ErrorHandlerCallback?, uiSessionKey: Long, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) {
|
||||
fun attach(
|
||||
fragment: Fragment,
|
||||
errorHandlerCallback: ErrorHandlerCallback?,
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) {
|
||||
attach(fragment, errorHandlerCallback, Flowable.just(inAppPaymentId))
|
||||
}
|
||||
|
||||
fun attach(
|
||||
fragment: Fragment,
|
||||
errorHandlerCallback: ErrorHandlerCallback?,
|
||||
inAppPaymentIdSource: Flowable<InAppPaymentTable.InAppPaymentId>
|
||||
) {
|
||||
this.fragment = fragment
|
||||
this.errorHandlerCallback = errorHandlerCallback
|
||||
|
||||
@@ -244,12 +257,26 @@ class DonationCheckoutDelegate(
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
|
||||
disposables.bindTo(fragment.viewLifecycleOwner)
|
||||
disposables += registerErrorSource(errorSource)
|
||||
additionalSources.forEach { source ->
|
||||
disposables += registerErrorSource(source)
|
||||
}
|
||||
disposables += inAppPaymentIdSource
|
||||
.switchMap { filterUnnotifiedErrors(it) }
|
||||
.doOnNext {
|
||||
SignalDatabase.inAppPayments.update(it.copy(notified = true))
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
showErrorDialog(it)
|
||||
}
|
||||
|
||||
disposables += registerUiSession(uiSessionKey)
|
||||
disposables += inAppPaymentIdSource
|
||||
.switchMap { InAppPaymentsRepository.observeTemporaryErrors(it) }
|
||||
.onBackpressureLatest()
|
||||
.concatMapSingle { (id, err) -> Single.fromCallable { SignalDatabase.inAppPayments.getById(id)!! to err } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy { (inAppPayment, error) ->
|
||||
handleTemporaryError(inAppPayment, error)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
@@ -258,95 +285,105 @@ class DonationCheckoutDelegate(
|
||||
errorHandlerCallback = null
|
||||
}
|
||||
|
||||
private fun registerErrorSource(errorSource: DonationErrorSource): Disposable {
|
||||
return DonationError.getErrorsForSource(errorSource)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
private fun filterUnnotifiedErrors(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Flowable<InAppPaymentTable.InAppPayment> {
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPaymentId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.filter {
|
||||
!it.notified && it.data.error != null
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerUiSession(uiSessionKey: Long): Disposable {
|
||||
return DonationError.getErrorsForUiSessionKey(uiSessionKey)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
showErrorDialog(it)
|
||||
private fun handleTemporaryError(inAppPayment: InAppPaymentTable.InAppPayment, throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is DonationError.UserCancelledPaymentError -> {
|
||||
Log.d(TAG, "User cancelled out of payment flow.", true)
|
||||
}
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> {
|
||||
Log.d(TAG, "User launched an external application.", true)
|
||||
errorHandlerCallback?.onUserLaunchedAnExternalApplication()
|
||||
}
|
||||
is DonationError.UserLaunchedExternalApplication -> {
|
||||
Log.d(TAG, "Long-running donation is still pending.", true)
|
||||
errorHandlerCallback?.navigateToDonationPending(inAppPayment)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
fragment!!.requireContext(),
|
||||
throwable,
|
||||
DialogHandler()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(throwable: Throwable) {
|
||||
private fun showErrorDialog(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
if (errorDialog != null) {
|
||||
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
|
||||
Log.d(TAG, "Already displaying an error dialog. Skipping. ${inAppPayment.data.error}", true)
|
||||
return
|
||||
}
|
||||
|
||||
if (throwable is DonationError.UserCancelledPaymentError) {
|
||||
Log.d(TAG, "User cancelled out of payment flow.", true)
|
||||
|
||||
val error = inAppPayment.data.error
|
||||
if (error == null) {
|
||||
Log.d(TAG, "InAppPayment does not contain an error. Skipping.", true)
|
||||
return
|
||||
}
|
||||
|
||||
if (throwable is DonationError.UserLaunchedExternalApplication) {
|
||||
Log.d(TAG, "User launched an external application.", true)
|
||||
errorHandlerCallback?.onUserLaunchedAnExternalApplication()
|
||||
return
|
||||
}
|
||||
|
||||
if (throwable is DonationError.BadgeRedemptionError.DonationPending) {
|
||||
Log.d(TAG, "Long-running donation is still pending.", true)
|
||||
errorHandlerCallback?.navigateToDonationPending(throwable.gatewayRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
fragment!!.requireContext(),
|
||||
throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
var tryAgain = false
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogDismissed() {
|
||||
errorDialog = null
|
||||
if (!tryAgain) {
|
||||
tryAgain = false
|
||||
fragment?.findNavController()?.popBackStack()
|
||||
}
|
||||
}
|
||||
when (error.type) {
|
||||
else -> {
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
fragment!!.requireContext(),
|
||||
inAppPayment,
|
||||
DialogHandler()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DialogHandler : DonationErrorDialogs.DialogCallback() {
|
||||
var tryAgain = false
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTryBankTransferAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogDismissed() {
|
||||
errorDialog = null
|
||||
if (!tryAgain) {
|
||||
tryAgain = false
|
||||
fragment?.findNavController()?.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorHandlerCallback {
|
||||
fun onUserLaunchedAnExternalApplication()
|
||||
fun navigateToDonationPending(gatewayRequest: GatewayRequest)
|
||||
fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
}
|
||||
|
||||
interface Callback : ErrorHandlerCallback {
|
||||
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
|
||||
fun navigateToIdealDetailsFragment(gatewayRequest: GatewayRequest)
|
||||
fun navigateToBankTransferMandate(gatewayResponse: GatewayResponse)
|
||||
fun onPaymentComplete(gatewayRequest: GatewayRequest)
|
||||
fun navigateToStripePaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun navigateToPayPalPaymentInProgress(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment)
|
||||
fun onProcessorActionProcessed()
|
||||
fun showSepaEuroMaximumDialog(sepaEuroMaximum: FiatMoney)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
/**
|
||||
@@ -14,17 +14,17 @@ class DonationCheckoutViewModel : ViewModel() {
|
||||
private val TAG = Log.tag(DonationCheckoutViewModel::class.java)
|
||||
}
|
||||
|
||||
private var gatewayRequest: GatewayRequest? = null
|
||||
private var inAppPayment: InAppPaymentTable.InAppPayment? = null
|
||||
|
||||
fun provideGatewayRequestForGooglePay(request: GatewayRequest) {
|
||||
fun provideGatewayRequestForGooglePay(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Log.d(TAG, "Provided with a gateway request.")
|
||||
Preconditions.checkState(gatewayRequest == null)
|
||||
gatewayRequest = request
|
||||
Preconditions.checkState(this.inAppPayment == null)
|
||||
this.inAppPayment = inAppPayment
|
||||
}
|
||||
|
||||
fun consumeGatewayRequestForGooglePay(): GatewayRequest? {
|
||||
val request = gatewayRequest
|
||||
gatewayRequest = null
|
||||
fun consumeGatewayRequestForGooglePay(): InAppPaymentTable.InAppPayment? {
|
||||
val request = inAppPayment
|
||||
inAppPayment = null
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.DonationPillToggleBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
@@ -15,7 +16,7 @@ object DonationPillToggle {
|
||||
}
|
||||
|
||||
class Model(
|
||||
val selected: DonateToSignalType,
|
||||
val selected: InAppPaymentTable.Type,
|
||||
val onClick: () -> Unit
|
||||
) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
@@ -28,13 +29,13 @@ object DonationPillToggle {
|
||||
private class ViewHolder(binding: DonationPillToggleBinding) : BindingViewHolder<Model, DonationPillToggleBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
when (model.selected) {
|
||||
DonateToSignalType.ONE_TIME -> {
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> {
|
||||
presentButtons(model, binding.oneTime, binding.monthly)
|
||||
}
|
||||
DonateToSignalType.MONTHLY -> {
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> {
|
||||
presentButtons(model, binding.monthly, binding.oneTime)
|
||||
}
|
||||
DonateToSignalType.GIFT -> {
|
||||
else -> {
|
||||
error("Unsupported donation type.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
@Parcelize
|
||||
class DonationProcessorActionResult(
|
||||
val action: DonationProcessorAction,
|
||||
val request: GatewayRequest,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment?,
|
||||
val status: Status
|
||||
) : Parcelable {
|
||||
enum class Status {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
|
||||
/**
|
||||
* Wraps an InAppPaymentData.Error in a throwable.
|
||||
*/
|
||||
class InAppPaymentError(
|
||||
val inAppPaymentDataError: InAppPaymentData.Error
|
||||
) : Exception() {
|
||||
companion object {
|
||||
fun fromDonationError(donationError: DonationError): InAppPaymentError? {
|
||||
val inAppPaymentDataError: InAppPaymentData.Error? = when (donationError) {
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> null
|
||||
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.CREDENTIAL_VALIDATION)
|
||||
is DonationError.BadgeRedemptionError.GenericError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.REDEMPTION)
|
||||
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> null
|
||||
is DonationError.PaymentProcessingError.GenericError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYMENT_PROCESSING)
|
||||
DonationError.GiftRecipientVerificationError.SelectedRecipientIsInvalid -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.INVALID_GIFT_RECIPIENT)
|
||||
is DonationError.GooglePayError.RequestTokenError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.GOOGLE_PAY_REQUEST_TOKEN)
|
||||
is DonationError.OneTimeDonationError.AmountTooLargeError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.ONE_TIME_AMOUNT_TOO_LARGE)
|
||||
is DonationError.OneTimeDonationError.AmountTooSmallError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.ONE_TIME_AMOUNT_TOO_SMALL)
|
||||
is DonationError.OneTimeDonationError.InvalidCurrencyError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.INVALID_CURRENCY)
|
||||
is DonationError.PaymentSetupError.GenericError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYMENT_SETUP)
|
||||
is DonationError.PaymentSetupError.PayPalCodedError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYPAL_CODED_ERROR, data_ = donationError.errorCode.toString())
|
||||
is DonationError.PaymentSetupError.PayPalDeclinedError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.PAYPAL_DECLINED_ERROR, data_ = donationError.code.code.toString())
|
||||
is DonationError.PaymentSetupError.StripeCodedError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.STRIPE_CODED_ERROR, data_ = donationError.errorCode)
|
||||
is DonationError.PaymentSetupError.StripeDeclinedError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.STRIPE_DECLINED_ERROR, data_ = donationError.declineCode.rawCode)
|
||||
is DonationError.PaymentSetupError.StripeFailureCodeError -> InAppPaymentData.Error(type = InAppPaymentData.Error.Type.STRIPE_FAILURE, data_ = donationError.failureCode.rawCode)
|
||||
is DonationError.UserCancelledPaymentError -> null
|
||||
is DonationError.UserLaunchedExternalApplication -> null
|
||||
}
|
||||
|
||||
return inAppPaymentDataError?.let { InAppPaymentError(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,13 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
@@ -48,14 +48,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, null, args.request.uiSessionKey, errorSource)
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, null, args.inAppPayment.id)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
@@ -65,13 +58,14 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
binding.continueButton.text = if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
// TODO [message-backups] Copy for this button in backups checkout flow.
|
||||
binding.continueButton.text = if (args.inAppPayment.type == InAppPaymentTable.Type.RECURRING_DONATION) {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, args.request.fiat))
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney()))
|
||||
}
|
||||
|
||||
binding.description.setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary))
|
||||
@@ -122,7 +116,8 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
findNavController().safeNavigate(
|
||||
CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
args.request
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.c
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
/**
|
||||
* Encapsulates data returned from the credit card form that can be used
|
||||
@@ -11,6 +11,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.ga
|
||||
*/
|
||||
@Parcelize
|
||||
data class CreditCardResult(
|
||||
val gatewayRequest: GatewayRequest,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val creditCardData: StripeApi.CardData
|
||||
) : Parcelable
|
||||
|
||||
@@ -8,39 +8,40 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
sealed interface GatewayOrderStrategy {
|
||||
|
||||
val orderedGateways: Set<GatewayResponse.Gateway>
|
||||
val orderedGateways: Set<InAppPaymentData.PaymentMethodType>
|
||||
|
||||
private object Default : GatewayOrderStrategy {
|
||||
override val orderedGateways: Set<GatewayResponse.Gateway> = setOf(
|
||||
GatewayResponse.Gateway.CREDIT_CARD,
|
||||
GatewayResponse.Gateway.PAYPAL,
|
||||
GatewayResponse.Gateway.GOOGLE_PAY,
|
||||
GatewayResponse.Gateway.SEPA_DEBIT,
|
||||
GatewayResponse.Gateway.IDEAL
|
||||
override val orderedGateways: Set<InAppPaymentData.PaymentMethodType> = setOf(
|
||||
InAppPaymentData.PaymentMethodType.CARD,
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL,
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY,
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT,
|
||||
InAppPaymentData.PaymentMethodType.IDEAL
|
||||
)
|
||||
}
|
||||
|
||||
private object NorthAmerica : GatewayOrderStrategy {
|
||||
override val orderedGateways: Set<GatewayResponse.Gateway> = setOf(
|
||||
GatewayResponse.Gateway.GOOGLE_PAY,
|
||||
GatewayResponse.Gateway.PAYPAL,
|
||||
GatewayResponse.Gateway.CREDIT_CARD,
|
||||
GatewayResponse.Gateway.SEPA_DEBIT,
|
||||
GatewayResponse.Gateway.IDEAL
|
||||
override val orderedGateways: Set<InAppPaymentData.PaymentMethodType> = setOf(
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY,
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL,
|
||||
InAppPaymentData.PaymentMethodType.CARD,
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT,
|
||||
InAppPaymentData.PaymentMethodType.IDEAL
|
||||
)
|
||||
}
|
||||
|
||||
private object Netherlands : GatewayOrderStrategy {
|
||||
override val orderedGateways: Set<GatewayResponse.Gateway> = setOf(
|
||||
GatewayResponse.Gateway.IDEAL,
|
||||
GatewayResponse.Gateway.PAYPAL,
|
||||
GatewayResponse.Gateway.GOOGLE_PAY,
|
||||
GatewayResponse.Gateway.CREDIT_CARD,
|
||||
GatewayResponse.Gateway.SEPA_DEBIT
|
||||
override val orderedGateways: Set<InAppPaymentData.PaymentMethodType> = setOf(
|
||||
InAppPaymentData.PaymentMethodType.IDEAL,
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL,
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY,
|
||||
InAppPaymentData.PaymentMethodType.CARD,
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@Parcelize
|
||||
data class GatewayRequest(
|
||||
val uiSessionKey: Long,
|
||||
val donateToSignalType: DonateToSignalType,
|
||||
val badge: Badge,
|
||||
val label: String,
|
||||
val price: BigDecimal,
|
||||
val currencyCode: String,
|
||||
val level: Long,
|
||||
val recipientId: RecipientId,
|
||||
val additionalMessage: String? = null
|
||||
) : Parcelable {
|
||||
@IgnoredOnParcel
|
||||
val fiat: FiatMoney = FiatMoney(price, Currency.getInstance(currencyCode))
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.PaymentSourceType
|
||||
|
||||
@Parcelize
|
||||
data class GatewayResponse(val gateway: Gateway, val request: GatewayRequest) : Parcelable {
|
||||
enum class Gateway {
|
||||
GOOGLE_PAY,
|
||||
PAYPAL,
|
||||
CREDIT_CARD,
|
||||
SEPA_DEBIT,
|
||||
IDEAL;
|
||||
|
||||
fun toPaymentSourceType(): PaymentSourceType {
|
||||
return when (this) {
|
||||
GOOGLE_PAY -> PaymentSourceType.Stripe.GooglePay
|
||||
PAYPAL -> PaymentSourceType.PayPal
|
||||
CREDIT_CARD -> PaymentSourceType.Stripe.CreditCard
|
||||
SEPA_DEBIT -> PaymentSourceType.Stripe.SEPADebit
|
||||
IDEAL -> PaymentSourceType.Stripe.IDEAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
@@ -18,11 +20,13 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -55,16 +59,17 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private fun getConfiguration(state: GatewaySelectorState): DSLConfiguration {
|
||||
return configure {
|
||||
// TODO [message-backups] -- No badge on message backups.
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.badge,
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), args.request)
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
@@ -75,13 +80,14 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
return@configure
|
||||
}
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEachIndexed { index, gateway ->
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> renderGooglePayButton(state)
|
||||
GatewayResponse.Gateway.PAYPAL -> renderPayPalButton(state)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> renderCreditCardButton(state)
|
||||
GatewayResponse.Gateway.SEPA_DEBIT -> renderSEPADebitButton(state)
|
||||
GatewayResponse.Gateway.IDEAL -> renderIDEALButton(state)
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PAY -> renderGooglePayButton(state)
|
||||
InAppPaymentData.PaymentMethodType.PAYPAL -> renderPayPalButton(state)
|
||||
InAppPaymentData.PaymentMethodType.CARD -> renderCreditCardButton(state)
|
||||
InAppPaymentData.PaymentMethodType.SEPA_DEBIT -> renderSEPADebitButton(state)
|
||||
InAppPaymentData.PaymentMethodType.IDEAL -> renderIDEALButton(state)
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN -> error("Unsupported payment method.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +103,11 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
GooglePayButton.Model(
|
||||
isEnabled = true,
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.GOOGLE_PAY, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.GOOGLE_PAY)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -113,9 +121,11 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
customPref(
|
||||
PayPalButton.Model(
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.PAYPAL, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.PAYPAL)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
},
|
||||
isEnabled = true
|
||||
)
|
||||
@@ -130,10 +140,13 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),
|
||||
icon = DSLSettingsIcon.from(R.drawable.credit_card, R.color.signal_colorOnCustom),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.CREDIT_CARD, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.CARD)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -146,18 +159,21 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
tonalButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__bank_transfer),
|
||||
icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
val price = args.inAppPayment.data.amount!!.toFiatMoney()
|
||||
if (state.sepaEuroMaximum != null &&
|
||||
args.request.fiat.currency == CurrencyUtil.EURO &&
|
||||
args.request.fiat.amount > state.sepaEuroMaximum.amount
|
||||
price.currency == CurrencyUtil.EURO &&
|
||||
price.amount > state.sepaEuroMaximum.amount
|
||||
) {
|
||||
findNavController().popBackStack()
|
||||
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(FAILURE_KEY to true, SEPA_EURO_MAX to state.sepaEuroMaximum.amount))
|
||||
} else {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.SEPA_DEBIT, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.SEPA_DEBIT)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -171,10 +187,13 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
tonalButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__ideal),
|
||||
icon = DSLSettingsIcon.from(R.drawable.logo_ideal, NO_TINT),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.IDEAL, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
lifecycleDisposable += viewModel.updateInAppPaymentMethod(InAppPaymentData.PaymentMethodType.IDEAL)
|
||||
.subscribeBy {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to it))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -185,18 +204,20 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
const val FAILURE_KEY = "gateway_failure"
|
||||
const val SEPA_EURO_MAX = "sepa_euro_max"
|
||||
|
||||
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, request: GatewayRequest) {
|
||||
when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> presentMonthlyText(context, request)
|
||||
DonateToSignalType.ONE_TIME -> presentOneTimeText(context, request)
|
||||
DonateToSignalType.GIFT -> presentGiftText(context, request)
|
||||
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> error("This type is not supported") // TODO [message-backups] necessary?
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> presentMonthlyText(context, inAppPayment)
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> presentOneTimeText(context, inAppPayment)
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> presentGiftText(context, inAppPayment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentMonthlyText(context: Context, request: GatewayRequest) {
|
||||
private fun DSLConfiguration.presentMonthlyText(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, FiatMoneyUtil.format(context.resources, inAppPayment.data.amount!!.toFiatMoney())),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
@@ -204,7 +225,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge, request.badge.name),
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge, inAppPayment.data.badge!!.name),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant))
|
||||
@@ -212,10 +233,10 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentOneTimeText(context: Context, request: GatewayRequest) {
|
||||
private fun DSLConfiguration.presentOneTimeText(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, inAppPayment.data.amount!!.toFiatMoney())),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
@@ -223,7 +244,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.resources.getQuantityString(R.plurals.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, 30, request.badge.name, 30),
|
||||
context.resources.getQuantityString(R.plurals.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, 30, inAppPayment.data.badge!!.name, 30),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant))
|
||||
@@ -231,10 +252,10 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentGiftText(context: Context, request: GatewayRequest) {
|
||||
private fun DSLConfiguration.presentGiftText(context: Context, inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, inAppPayment.data.amount!!.toFiatMoney())),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
|
||||
@@ -2,7 +2,11 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
@@ -18,10 +22,10 @@ class GatewaySelectorRepository(
|
||||
.map { configuration ->
|
||||
val available = configuration.getAvailablePaymentMethods(currencyCode).map {
|
||||
when (it) {
|
||||
SubscriptionsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL)
|
||||
SubscriptionsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
|
||||
SubscriptionsConfiguration.SEPA_DEBIT -> listOf(GatewayResponse.Gateway.SEPA_DEBIT)
|
||||
SubscriptionsConfiguration.IDEAL -> listOf(GatewayResponse.Gateway.IDEAL)
|
||||
SubscriptionsConfiguration.PAYPAL -> listOf(InAppPaymentData.PaymentMethodType.PAYPAL)
|
||||
SubscriptionsConfiguration.CARD -> listOf(InAppPaymentData.PaymentMethodType.CARD, InAppPaymentData.PaymentMethodType.GOOGLE_PAY)
|
||||
SubscriptionsConfiguration.SEPA_DEBIT -> listOf(InAppPaymentData.PaymentMethodType.SEPA_DEBIT)
|
||||
SubscriptionsConfiguration.IDEAL -> listOf(InAppPaymentData.PaymentMethodType.IDEAL)
|
||||
else -> listOf()
|
||||
}
|
||||
}.flatten().toSet()
|
||||
@@ -33,8 +37,20 @@ class GatewaySelectorRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun setInAppPaymentMethodType(inAppPayment: InAppPaymentTable.InAppPayment, paymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
paymentMethodType = paymentMethodType
|
||||
)
|
||||
)
|
||||
)
|
||||
}.flatMap { InAppPaymentsRepository.requireInAppPayment(inAppPayment.id) }
|
||||
}
|
||||
|
||||
data class GatewayConfiguration(
|
||||
val availableGateways: Set<GatewayResponse.Gateway>,
|
||||
val availableGateways: Set<InAppPaymentData.PaymentMethodType>,
|
||||
val sepaEuroMaximum: FiatMoney?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val gatewayOrderStrategy: GatewayOrderStrategy,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val loading: Boolean = true,
|
||||
val badge: Badge,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
@@ -9,6 +10,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
@@ -16,18 +19,18 @@ import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
class GatewaySelectorViewModel(
|
||||
args: GatewaySelectorBottomSheetArgs,
|
||||
repository: StripeRepository,
|
||||
gatewaySelectorRepository: GatewaySelectorRepository
|
||||
private val gatewaySelectorRepository: GatewaySelectorRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(
|
||||
GatewaySelectorState(
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
|
||||
badge = args.request.badge,
|
||||
isGooglePayAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.request.donateToSignalType),
|
||||
isCreditCardAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.request.donateToSignalType),
|
||||
isPayPalAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.PayPal, args.request.donateToSignalType),
|
||||
isSEPADebitAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.request.donateToSignalType),
|
||||
isIDEALAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.request.donateToSignalType)
|
||||
inAppPayment = args.inAppPayment,
|
||||
isCreditCardAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.inAppPayment.type),
|
||||
isGooglePayAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.inAppPayment.type),
|
||||
isPayPalAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.PayPal, args.inAppPayment.type),
|
||||
isSEPADebitAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.inAppPayment.type),
|
||||
isIDEALAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.inAppPayment.type)
|
||||
)
|
||||
)
|
||||
private val disposables = CompositeDisposable()
|
||||
@@ -36,17 +39,18 @@ class GatewaySelectorViewModel(
|
||||
|
||||
init {
|
||||
val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false)
|
||||
val gatewayConfiguration = gatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = args.request.currencyCode)
|
||||
val gatewayConfiguration = gatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = args.inAppPayment.data.amount!!.currencyCode)
|
||||
|
||||
disposables += Single.zip(isGooglePayAvailable, gatewayConfiguration, ::Pair).subscribeBy { (googlePayAvailable, gatewayConfiguration) ->
|
||||
SignalStore.donationsValues().isGooglePayReady = googlePayAvailable
|
||||
store.update {
|
||||
it.copy(
|
||||
loading = false,
|
||||
isCreditCardAvailable = it.isCreditCardAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.CREDIT_CARD),
|
||||
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.GOOGLE_PAY),
|
||||
isPayPalAvailable = it.isPayPalAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.PAYPAL),
|
||||
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.SEPA_DEBIT),
|
||||
isIDEALAvailable = it.isIDEALAvailable && gatewayConfiguration.availableGateways.contains(GatewayResponse.Gateway.IDEAL),
|
||||
isCreditCardAvailable = it.isCreditCardAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.CARD),
|
||||
isGooglePayAvailable = it.isGooglePayAvailable && googlePayAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.GOOGLE_PAY),
|
||||
isPayPalAvailable = it.isPayPalAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.PAYPAL),
|
||||
isSEPADebitAvailable = it.isSEPADebitAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.SEPA_DEBIT),
|
||||
isIDEALAvailable = it.isIDEALAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.IDEAL),
|
||||
sepaEuroMaximum = gatewayConfiguration.sepaEuroMaximum
|
||||
)
|
||||
}
|
||||
@@ -58,6 +62,10 @@ class GatewaySelectorViewModel(
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun updateInAppPaymentMethod(inAppPaymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
return gatewaySelectorRepository.setInAppPaymentMethodType(store.state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val args: GatewaySelectorBottomSheetArgs,
|
||||
private val repository: StripeRepository,
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
@@ -43,14 +44,14 @@ class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = args.request.badge,
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), args.request)
|
||||
presentTitleAndSubtitle(requireContext(), args.inAppPayment)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
|
||||
@@ -59,15 +60,14 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION -> {
|
||||
viewModel.processNewDonation(args.request, this::oneTimeConfirmationPipeline, this::monthlyConfirmationPipeline)
|
||||
viewModel.processNewDonation(args.inAppPayment!!, this::oneTimeConfirmationPipeline, this::monthlyConfirmationPipeline)
|
||||
}
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request)
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
}
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription()
|
||||
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION) // TODO [message-backups] Remove hardcode
|
||||
}
|
||||
else -> error("Unsupported action: ${args.action}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
inAppPayment = args.inAppPayment,
|
||||
status = DonationProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -103,7 +103,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
inAppPayment = args.inAppPayment,
|
||||
status = DonationProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -128,7 +128,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result.copy(paymentId = createPaymentIntentResponse.paymentId))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result) {
|
||||
emitter.onSuccess(PayPalPaymentMethodId(createPaymentIntentResponse.token))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,29 +12,30 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.toDonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
class PayPalPaymentInProgressViewModel(
|
||||
private val payPalRepository: PayPalRepository,
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository,
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
||||
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository,
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -66,24 +67,24 @@ class PayPalPaymentInProgressViewModel(
|
||||
}
|
||||
|
||||
fun processNewDonation(
|
||||
request: GatewayRequest,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
routeToOneTimeConfirmation: (PayPalCreatePaymentIntentResponse) -> Single<PayPalConfirmationResult>,
|
||||
routeToMonthlyConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>
|
||||
) {
|
||||
Log.d(TAG, "Proceeding with donation...", true)
|
||||
|
||||
return when (request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> proceedOneTime(request, routeToOneTimeConfirmation)
|
||||
DonateToSignalType.MONTHLY -> proceedMonthly(request, routeToMonthlyConfirmation)
|
||||
DonateToSignalType.GIFT -> proceedOneTime(request, routeToOneTimeConfirmation)
|
||||
return if (inAppPayment.type.recurring) {
|
||||
proceedMonthly(inAppPayment, routeToMonthlyConfirmation)
|
||||
} else {
|
||||
proceedOneTime(inAppPayment, routeToOneTimeConfirmation)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscription(request: GatewayRequest) {
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, false))
|
||||
disposables += recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
@@ -91,28 +92,22 @@ class PayPalPaymentInProgressViewModel(
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.MONTHLY, PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, PaymentSourceType.PayPal, throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
fun cancelSubscription(subscriberType: InAppPaymentSubscriberRecord.Type) {
|
||||
Log.d(TAG, "Beginning cancellation...", true)
|
||||
|
||||
store.update { DonationProcessorStage.CANCELLING }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscription().subscribeBy(
|
||||
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Cancellation succeeded", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
monthlyDonationRepository.syncAccountRecord().subscribe()
|
||||
recurringInAppPaymentRepository.syncAccountRecord().subscribe()
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
@@ -123,13 +118,13 @@ class PayPalPaymentInProgressViewModel(
|
||||
}
|
||||
|
||||
private fun proceedOneTime(
|
||||
request: GatewayRequest,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
routeToPaypalConfirmation: (PayPalCreatePaymentIntentResponse) -> Single<PayPalConfirmationResult>
|
||||
) {
|
||||
Log.d(TAG, "Proceeding with one-time payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
val verifyUser = if (request.donateToSignalType == DonateToSignalType.GIFT) {
|
||||
OneTimeDonationRepository.verifyRecipientIsAllowedToReceiveAGift(request.recipientId)
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
|
||||
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(RecipientId.from(inAppPayment.data.recipientId!!))
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
@@ -138,24 +133,23 @@ class PayPalPaymentInProgressViewModel(
|
||||
.andThen(
|
||||
payPalRepository
|
||||
.createOneTimePaymentIntent(
|
||||
amount = request.fiat,
|
||||
badgeRecipient = request.recipientId,
|
||||
badgeLevel = request.level
|
||||
amount = inAppPayment.data.amount!!.toFiatMoney(),
|
||||
badgeRecipient = inAppPayment.data.recipientId?.let { RecipientId.from(it) } ?: Recipient.self().id,
|
||||
badgeLevel = inAppPayment.data.level
|
||||
)
|
||||
)
|
||||
.flatMap(routeToPaypalConfirmation)
|
||||
.flatMap { result ->
|
||||
payPalRepository.confirmOneTimePaymentIntent(
|
||||
amount = request.fiat,
|
||||
badgeLevel = request.level,
|
||||
amount = inAppPayment.data.amount.toFiatMoney(),
|
||||
badgeLevel = inAppPayment.data.level,
|
||||
paypalConfirmationResult = result
|
||||
)
|
||||
}
|
||||
.flatMapCompletable { response ->
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
gatewayRequest = request,
|
||||
oneTimeInAppPaymentRepository.waitForOneTimeRedemption(
|
||||
inAppPayment = inAppPayment,
|
||||
paymentIntentId = response.paymentId,
|
||||
donationProcessor = DonationProcessor.PAYPAL,
|
||||
paymentSourceType = PaymentSourceType.PayPal
|
||||
)
|
||||
}
|
||||
@@ -164,13 +158,7 @@ class PayPalPaymentInProgressViewModel(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(request.donateToSignalType.toErrorSource(), PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(request.donateToSignalType.toErrorSource())
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.ONE_TIME, PaymentSourceType.PayPal, throwable)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished one-time payment pipeline...", true)
|
||||
@@ -179,28 +167,22 @@ class PayPalPaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedMonthly(request: GatewayRequest, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>) {
|
||||
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>) {
|
||||
Log.d(TAG, "Proceeding with monthly payment pipeline...")
|
||||
|
||||
val setup = monthlyDonationRepository.ensureSubscriberId()
|
||||
.andThen(monthlyDonationRepository.cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(payPalRepository.createPaymentMethod())
|
||||
val setup = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
|
||||
.andThen(payPalRepository.createPaymentMethod(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMap(routeToPaypalConfirmation)
|
||||
.flatMapCompletable { payPalRepository.setDefaultPaymentMethod(it.paymentId) }
|
||||
.flatMapCompletable { payPalRepository.setDefaultPaymentMethod(inAppPayment.type.requireSubscriberType(), it.paymentId) }
|
||||
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.MONTHLY, it, PaymentSourceType.PayPal)) }
|
||||
|
||||
disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request, false))
|
||||
disposables += setup.andThen(recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, PaymentSourceType.PayPal))
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.MONTHLY, PaymentSourceType.PayPal)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, PaymentSourceType.PayPal, throwable)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished subscription payment pipeline...", true)
|
||||
@@ -211,11 +193,11 @@ class PayPalPaymentInProgressViewModel(
|
||||
|
||||
class Factory(
|
||||
private val payPalRepository: PayPalRepository = PayPalRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository, monthlyDonationRepository, oneTimeDonationRepository)) as T
|
||||
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository, recurringInAppPaymentRepository, oneTimeInAppPaymentRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toBigDecimal
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentMethodType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ExternalLaunchTransactionState
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Encapsulates the data required to complete a pending external transaction
|
||||
@@ -23,14 +23,14 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@Parcelize
|
||||
data class Stripe3DSData(
|
||||
val stripeIntentAccessor: StripeIntentAccessor,
|
||||
val gatewayRequest: GatewayRequest,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
private val rawPaymentSourceType: String
|
||||
) : Parcelable {
|
||||
@IgnoredOnParcel
|
||||
val paymentSourceType: PaymentSourceType = PaymentSourceType.fromCode(rawPaymentSourceType)
|
||||
|
||||
@IgnoredOnParcel
|
||||
val isLongRunning: Boolean = paymentSourceType == PaymentSourceType.Stripe.SEPADebit || (gatewayRequest.donateToSignalType == DonateToSignalType.MONTHLY && paymentSourceType.isBankTransfer)
|
||||
val isLongRunning: Boolean = paymentSourceType == PaymentSourceType.Stripe.SEPADebit || (inAppPayment.type.recurring && paymentSourceType.isBankTransfer)
|
||||
|
||||
fun toProtoBytes(): ByteArray {
|
||||
return ExternalLaunchTransactionState(
|
||||
@@ -43,25 +43,27 @@ data class Stripe3DSData(
|
||||
intentClientSecret = stripeIntentAccessor.intentClientSecret
|
||||
),
|
||||
gatewayRequest = ExternalLaunchTransactionState.GatewayRequest(
|
||||
donateToSignalType = when (gatewayRequest.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY
|
||||
DonateToSignalType.GIFT -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT
|
||||
donateToSignalType = when (inAppPayment.type) {
|
||||
InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN")
|
||||
InAppPaymentTable.Type.ONE_TIME_DONATION -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME
|
||||
InAppPaymentTable.Type.RECURRING_DONATION -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT
|
||||
InAppPaymentTable.Type.RECURRING_BACKUP -> error("Unimplemented") // TODO [message-backups] do we still need this?
|
||||
},
|
||||
badge = Badges.toDatabaseBadge(gatewayRequest.badge),
|
||||
label = gatewayRequest.label,
|
||||
price = gatewayRequest.price.toDecimalValue(),
|
||||
currencyCode = gatewayRequest.currencyCode,
|
||||
level = gatewayRequest.level,
|
||||
recipient_id = gatewayRequest.recipientId.toLong(),
|
||||
additionalMessage = gatewayRequest.additionalMessage ?: ""
|
||||
badge = inAppPayment.data.badge,
|
||||
label = inAppPayment.data.label,
|
||||
price = inAppPayment.data.amount!!.amount,
|
||||
currencyCode = inAppPayment.data.amount.currencyCode,
|
||||
level = inAppPayment.data.level,
|
||||
recipient_id = inAppPayment.data.recipientId?.toLong() ?: Recipient.self().id.toLong(),
|
||||
additionalMessage = inAppPayment.data.additionalMessage ?: ""
|
||||
),
|
||||
paymentSourceType = paymentSourceType.code
|
||||
).encode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromProtoBytes(byteArray: ByteArray, uiSessionKey: Long): Stripe3DSData {
|
||||
fun fromProtoBytes(byteArray: ByteArray): Stripe3DSData {
|
||||
val proto = ExternalLaunchTransactionState.ADAPTER.decode(byteArray)
|
||||
return Stripe3DSData(
|
||||
stripeIntentAccessor = StripeIntentAccessor(
|
||||
@@ -72,20 +74,33 @@ data class Stripe3DSData(
|
||||
intentId = proto.stripeIntentAccessor.intentId,
|
||||
intentClientSecret = proto.stripeIntentAccessor.intentClientSecret
|
||||
),
|
||||
gatewayRequest = GatewayRequest(
|
||||
uiSessionKey = uiSessionKey,
|
||||
donateToSignalType = when (proto.gatewayRequest!!.donateToSignalType) {
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY -> DonateToSignalType.MONTHLY
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME -> DonateToSignalType.ONE_TIME
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT -> DonateToSignalType.GIFT
|
||||
inAppPayment = InAppPaymentTable.InAppPayment(
|
||||
id = InAppPaymentTable.InAppPaymentId(-1), // TODO [alex] -- can we start writing this in for new transactions?
|
||||
type = when (proto.gatewayRequest!!.donateToSignalType) {
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.MONTHLY -> InAppPaymentTable.Type.RECURRING_DONATION
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.ONE_TIME -> InAppPaymentTable.Type.ONE_TIME_DONATION
|
||||
ExternalLaunchTransactionState.GatewayRequest.DonateToSignalType.GIFT -> InAppPaymentTable.Type.ONE_TIME_GIFT
|
||||
// TODO [message-backups] -- Backups?
|
||||
},
|
||||
badge = Badges.fromDatabaseBadge(proto.gatewayRequest.badge!!),
|
||||
label = proto.gatewayRequest.label,
|
||||
price = proto.gatewayRequest.price!!.toBigDecimal(),
|
||||
currencyCode = proto.gatewayRequest.currencyCode,
|
||||
level = proto.gatewayRequest.level,
|
||||
recipientId = RecipientId.from(proto.gatewayRequest.recipient_id),
|
||||
additionalMessage = proto.gatewayRequest.additionalMessage.takeIf { it.isNotBlank() }
|
||||
endOfPeriod = 0.milliseconds,
|
||||
updatedAt = 0.milliseconds,
|
||||
insertedAt = 0.milliseconds,
|
||||
notified = true,
|
||||
state = InAppPaymentTable.State.WAITING_FOR_AUTHORIZATION,
|
||||
subscriberId = null,
|
||||
data = InAppPaymentData(
|
||||
paymentMethodType = PaymentSourceType.fromCode(proto.paymentSourceType).toPaymentMethodType(),
|
||||
badge = proto.gatewayRequest.badge,
|
||||
label = proto.gatewayRequest.label,
|
||||
amount = FiatValue(amount = proto.gatewayRequest.price, currencyCode = proto.gatewayRequest.currencyCode),
|
||||
level = proto.gatewayRequest.level,
|
||||
recipientId = null,
|
||||
additionalMessage = "",
|
||||
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(
|
||||
stripeClientSecret = proto.stripeIntentAccessor.intentClientSecret,
|
||||
stripeIntentId = proto.stripeIntentAccessor.intentId
|
||||
)
|
||||
)
|
||||
),
|
||||
rawPaymentSourceType = proto.paymentSourceType
|
||||
)
|
||||
|
||||
@@ -20,13 +20,17 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationWebViewOnBackPressedCallback
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
@@ -50,6 +54,8 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
|
||||
var result: Bundle? = null
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
@@ -57,6 +63,8 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
dialog!!.window!!.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
|
||||
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
|
||||
@@ -76,7 +84,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
)
|
||||
)
|
||||
|
||||
if (FeatureFlags.internalUser() && args.stripe3DSData.paymentSourceType == PaymentSourceType.Stripe.IDEAL) {
|
||||
if (FeatureFlags.internalUser() && args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) {
|
||||
val openApp = MaterialButton(requireContext()).apply {
|
||||
text = "Open App"
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
@@ -98,14 +106,20 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
}
|
||||
|
||||
private fun handleLaunchExternal(intent: Intent) {
|
||||
SignalStore.donationsValues().setPending3DSData(args.stripe3DSData)
|
||||
lifecycleDisposable += Completable
|
||||
.fromAction {
|
||||
SignalDatabase.inAppPayments.update(args.inAppPayment)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
result = bundleOf(
|
||||
LAUNCHED_EXTERNAL to true
|
||||
)
|
||||
|
||||
result = bundleOf(
|
||||
LAUNCHED_EXTERNAL to true
|
||||
)
|
||||
|
||||
startActivity(intent)
|
||||
dismissAllowingStateLoss()
|
||||
startActivity(intent)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Stripe3DSWebClient : WebViewClient() {
|
||||
|
||||
@@ -8,10 +8,11 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.s
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
fun interface StripeNextActionHandler {
|
||||
fun handle(
|
||||
action: StripeApi.Secure3DSAction,
|
||||
stripe3DSData: Stripe3DSData
|
||||
inAppPayment: InAppPaymentTable.InAppPayment
|
||||
): Single<StripeIntentAccessor>
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -63,13 +64,13 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION -> {
|
||||
viewModel.processNewDonation(args.request, this::handleSecure3dsAction)
|
||||
viewModel.processNewDonation(args.inAppPayment!!, this::handleSecure3dsAction)
|
||||
}
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request, args.isLongRunning)
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
}
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription()
|
||||
viewModel.cancelSubscription(args.inAppPaymentType.requireSubscriberType())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
inAppPayment = args.inAppPayment,
|
||||
status = DonationProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -106,7 +107,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
inAppPayment = args.inAppPayment,
|
||||
status = DonationProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -116,7 +117,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSecure3dsAction(secure3dsAction: StripeApi.Secure3DSAction, stripe3DSData: Stripe3DSData): Single<StripeIntentAccessor> {
|
||||
private fun handleSecure3dsAction(secure3dsAction: StripeApi.Secure3DSAction, inAppPayment: InAppPaymentTable.InAppPayment): Single<StripeIntentAccessor> {
|
||||
return when (secure3dsAction) {
|
||||
is StripeApi.Secure3DSAction.NotNeeded -> {
|
||||
Log.d(TAG, "No 3DS action required.")
|
||||
@@ -132,16 +133,16 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
} else {
|
||||
val didLaunchExternal = bundle.getBoolean(Stripe3DSDialogFragment.LAUNCHED_EXTERNAL, false)
|
||||
if (didLaunchExternal) {
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(args.request.donateToSignalType.toErrorSource()))
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(args.inAppPaymentType.toErrorSource()))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(Stripe3DSDialogFragment.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(StripePaymentInProgressFragmentDirections.actionStripePaymentInProgressFragmentToStripe3dsDialogFragment(secure3dsAction.uri, secure3dsAction.returnUri, stripe3DSData))
|
||||
findNavController().safeNavigate(StripePaymentInProgressFragmentDirections.actionStripePaymentInProgressFragmentToStripe3dsDialogFragment(secure3dsAction.uri, secure3dsAction.returnUri, inAppPayment))
|
||||
|
||||
emitter.setCancellable {
|
||||
parentFragmentManager.clearFragmentResultListener(Stripe3DSDialogFragment.REQUEST_KEY)
|
||||
|
||||
@@ -10,32 +10,38 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.toDonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError
|
||||
|
||||
class StripePaymentInProgressViewModel(
|
||||
private val stripeRepository: StripeRepository,
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository,
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
||||
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository,
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
@@ -69,21 +75,15 @@ class StripePaymentInProgressViewModel(
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun processNewDonation(request: GatewayRequest, nextActionHandler: StripeNextActionHandler) {
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, nextActionHandler: StripeNextActionHandler) {
|
||||
Log.d(TAG, "Proceeding with donation...", true)
|
||||
|
||||
val errorSource = when (request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(inAppPayment.type.toErrorSource())
|
||||
|
||||
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(errorSource)
|
||||
|
||||
return when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> proceedMonthly(request, paymentSourceProvider, nextActionHandler)
|
||||
DonateToSignalType.ONE_TIME -> proceedOneTime(request, paymentSourceProvider, nextActionHandler)
|
||||
DonateToSignalType.GIFT -> proceedOneTime(request, paymentSourceProvider, nextActionHandler)
|
||||
return if (inAppPayment.type.recurring) {
|
||||
proceedMonthly(inAppPayment, paymentSourceProvider, nextActionHandler)
|
||||
} else {
|
||||
proceedOneTime(inAppPayment, paymentSourceProvider, nextActionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,27 +142,31 @@ class StripePaymentInProgressViewModel(
|
||||
stripePaymentData = null
|
||||
}
|
||||
|
||||
private fun proceedMonthly(request: GatewayRequest, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: StripeNextActionHandler) {
|
||||
val ensureSubscriberId: Completable = monthlyDonationRepository.ensureSubscriberId()
|
||||
private fun proceedMonthly(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceProvider: PaymentSourceProvider, nextActionHandler: StripeNextActionHandler) {
|
||||
val ensureSubscriberId: Completable = recurringInAppPaymentRepository.ensureSubscriberId(inAppPayment.type.requireSubscriberType())
|
||||
val createAndConfirmSetupIntent: Single<StripeApi.Secure3DSAction> = paymentSourceProvider.paymentSource.flatMap {
|
||||
stripeRepository.createAndConfirmSetupIntent(it, paymentSourceProvider.paymentSourceType as PaymentSourceType.Stripe)
|
||||
}
|
||||
|
||||
val setLevel: Completable = monthlyDonationRepository.setSubscriptionLevel(request, paymentSourceProvider.paymentSourceType.isBankTransfer)
|
||||
val setLevel: Completable = recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceProvider.paymentSourceType)
|
||||
|
||||
Log.d(TAG, "Starting subscription payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
|
||||
val setup: Completable = ensureSubscriberId
|
||||
.andThen(monthlyDonationRepository.cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(recurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()))
|
||||
.andThen(createAndConfirmSetupIntent)
|
||||
.flatMap { secure3DSAction ->
|
||||
nextActionHandler.handle(
|
||||
action = secure3DSAction,
|
||||
Stripe3DSData(
|
||||
secure3DSAction.stripeIntentAccessor,
|
||||
request,
|
||||
paymentSourceProvider.paymentSourceType.code
|
||||
inAppPayment = inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = null,
|
||||
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(
|
||||
stripeIntentId = secure3DSAction.stripeIntentAccessor.intentId,
|
||||
stripeClientSecret = secure3DSAction.stripeIntentAccessor.intentClientSecret
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flatMap { secure3DSResult -> stripeRepository.getStatusAndPaymentMethodId(secure3DSResult, secure3DSAction.paymentMethodId) }
|
||||
@@ -180,13 +184,7 @@ class StripePaymentInProgressViewModel(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in subscription payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, paymentSourceProvider.paymentSourceType, throwable)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished subscription payment pipeline...", true)
|
||||
@@ -196,41 +194,45 @@ class StripePaymentInProgressViewModel(
|
||||
}
|
||||
|
||||
private fun proceedOneTime(
|
||||
request: GatewayRequest,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
paymentSourceProvider: PaymentSourceProvider,
|
||||
nextActionHandler: StripeNextActionHandler
|
||||
) {
|
||||
Log.w(TAG, "Beginning one-time payment pipeline...", true)
|
||||
|
||||
val amount = request.fiat
|
||||
val verifyUser = if (request.donateToSignalType == DonateToSignalType.GIFT) {
|
||||
OneTimeDonationRepository.verifyRecipientIsAllowedToReceiveAGift(request.recipientId)
|
||||
val amount = inAppPayment.data.amount!!.toFiatMoney()
|
||||
val recipientId = inAppPayment.data.recipientId?.let { RecipientId.from(it) } ?: Recipient.self().id
|
||||
val verifyUser = if (inAppPayment.type == InAppPaymentTable.Type.ONE_TIME_GIFT) {
|
||||
OneTimeInAppPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(recipientId)
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
val continuePayment: Single<StripeIntentAccessor> = verifyUser.andThen(stripeRepository.continuePayment(amount, request.recipientId, request.level, paymentSourceProvider.paymentSourceType))
|
||||
val continuePayment: Single<StripeIntentAccessor> = verifyUser.andThen(stripeRepository.continuePayment(amount, recipientId, inAppPayment.data.level, paymentSourceProvider.paymentSourceType))
|
||||
val intentAndSource: Single<Pair<StripeIntentAccessor, StripeApi.PaymentSource>> = Single.zip(continuePayment, paymentSourceProvider.paymentSource, ::Pair)
|
||||
|
||||
disposables += intentAndSource.flatMapCompletable { (paymentIntent, paymentSource) ->
|
||||
stripeRepository.confirmPayment(paymentSource, paymentIntent, request.recipientId)
|
||||
stripeRepository.confirmPayment(paymentSource, paymentIntent, recipientId)
|
||||
.flatMap { action ->
|
||||
nextActionHandler
|
||||
.handle(
|
||||
action,
|
||||
Stripe3DSData(
|
||||
action.stripeIntentAccessor,
|
||||
request,
|
||||
paymentSourceProvider.paymentSourceType.code
|
||||
action = action,
|
||||
inAppPayment = inAppPayment.copy(
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = null,
|
||||
waitForAuth = InAppPaymentData.WaitingForAuthorizationState(
|
||||
stripeIntentId = action.stripeIntentAccessor.intentId,
|
||||
stripeClientSecret = action.stripeIntentAccessor.intentClientSecret
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it, action.paymentMethodId) }
|
||||
}
|
||||
.flatMapCompletable {
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
gatewayRequest = request,
|
||||
oneTimeInAppPaymentRepository.waitForOneTimeRedemption(
|
||||
inAppPayment = inAppPayment,
|
||||
paymentIntentId = paymentIntent.intentId,
|
||||
donationProcessor = DonationProcessor.STRIPE,
|
||||
paymentSourceType = paymentSource.type
|
||||
)
|
||||
}
|
||||
@@ -238,13 +240,7 @@ class StripePaymentInProgressViewModel(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(request.donateToSignalType.toErrorSource(), paymentSourceProvider.paymentSourceType)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(request.donateToSignalType.toErrorSource())
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.ONE_TIME, paymentSourceProvider.paymentSourceType, throwable)
|
||||
},
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed one-time payment pipeline...", true)
|
||||
@@ -253,14 +249,14 @@ class StripePaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
fun cancelSubscription(subscriberType: InAppPaymentSubscriberRecord.Type) {
|
||||
Log.d(TAG, "Beginning cancellation...", true)
|
||||
|
||||
store.update { DonationProcessorStage.CANCELLING }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscription().subscribeBy(
|
||||
disposables += recurringInAppPaymentRepository.cancelActiveSubscription(subscriberType).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Cancellation succeeded", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation(subscriberType)
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
stripeRepository.scheduleSyncForAccountRecordChange()
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
@@ -272,10 +268,13 @@ class StripePaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription(request: GatewayRequest, isLongRunning: Boolean) {
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request, isLongRunning))
|
||||
disposables += recurringInAppPaymentRepository
|
||||
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(recurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMapCompletable { paymentSourceType -> recurringInAppPaymentRepository.setSubscriptionLevel(inAppPayment, paymentSourceType) }
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
@@ -283,14 +282,11 @@ class StripePaymentInProgressViewModel(
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = when (throwable) {
|
||||
is DonationError -> throwable
|
||||
is DonationProcessorError -> throwable.toDonationError(DonationErrorSource.MONTHLY, PaymentSourceType.Stripe.GooglePay)
|
||||
else -> DonationError.genericBadgeRedemptionFailure(DonationErrorSource.MONTHLY)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
val paymentSourceType = InAppPaymentsRepository.getLatestPaymentMethodType(inAppPayment.type.requireSubscriberType()).toPaymentSourceType()
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, paymentSourceType, throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -309,11 +305,11 @@ class StripePaymentInProgressViewModel(
|
||||
|
||||
class Factory(
|
||||
private val stripeRepository: StripeRepository,
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
private val recurringInAppPaymentRepository: RecurringInAppPaymentRepository = RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeInAppPaymentRepository: OneTimeInAppPaymentRepository = OneTimeInAppPaymentRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(StripePaymentInProgressViewModel(stripeRepository, monthlyDonationRepository, oneTimeDonationRepository)) as T
|
||||
return modelClass.cast(StripePaymentInProgressViewModel(stripeRepository, recurringInAppPaymentRepository, oneTimeInAppPaymentRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,17 +56,16 @@ import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.details.BankTransferDetailsViewModel.Field
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -90,13 +89,7 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, this, args.request.uiSessionKey, errorSource)
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPayment.id)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
@@ -111,16 +104,16 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
override fun FragmentContent() {
|
||||
val state: BankTransferDetailsState by viewModel.state
|
||||
|
||||
val donateLabel = remember(args.request) {
|
||||
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
val donateLabel = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-requests] backups copy
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat)
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -154,15 +147,16 @@ class BankTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate.
|
||||
findNavController().safeNavigate(
|
||||
BankTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
args.request
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onUserLaunchedAnExternalApplication() = Unit
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to gatewayRequest))
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to inAppPayment))
|
||||
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
|
||||
@@ -57,17 +57,16 @@ import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal.IdealTransferDetailsViewModel.Field
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -81,7 +80,7 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
|
||||
private val args: IdealTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: IdealTransferDetailsViewModel by viewModel {
|
||||
IdealTransferDetailsViewModel(args.request.donateToSignalType == DonateToSignalType.MONTHLY)
|
||||
IdealTransferDetailsViewModel(args.inAppPayment.type.recurring)
|
||||
}
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
@@ -94,13 +93,7 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.ONE_TIME
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.MONTHLY
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, this, args.request.uiSessionKey, errorSource)
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPayment.id)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, DonationProcessorActionResult::class.java)!!
|
||||
@@ -120,22 +113,22 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
|
||||
val donateLabel = remember(args.request) {
|
||||
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
val donateLabel = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.request.fiat)
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val idealDirections = remember(args.request) {
|
||||
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
val idealDirections = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
R.string.IdealTransferDetailsFragment__enter_your_bank
|
||||
} else {
|
||||
R.string.IdealTransferDetailsFragment__enter_your_bank_details_one_time
|
||||
@@ -164,12 +157,13 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
findNavController().safeNavigate(
|
||||
IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
args.request
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-requests] -- handle backup
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.IdealTransferDetailsFragment__confirm_your_donation_with_s, getString(state.idealBank!!.getUIValues().name)))
|
||||
.setMessage(R.string.IdealTransferDetailsFragment__monthly_ideal_warning)
|
||||
@@ -191,8 +185,8 @@ class IdealTransferDetailsFragment : ComposeFragment(), DonationCheckoutDelegate
|
||||
})
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(gatewayRequest: GatewayRequest) {
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to gatewayRequest))
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
setFragmentResult(BankTransferRequestKeys.PENDING_KEY, bundleOf(BankTransferRequestKeys.PENDING_KEY to inAppPayment))
|
||||
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
|
||||
@@ -58,9 +58,9 @@ import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorAnimator
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
@@ -112,13 +112,13 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
if (args.response.gateway == GatewayResponse.Gateway.SEPA_DEBIT) {
|
||||
if (args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT) {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.response.request)
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.inAppPayment)
|
||||
)
|
||||
} else {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToIdealTransferDetailsFragment(args.response.request)
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToIdealTransferDetailsFragment(args.inAppPayment)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,20 @@ import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeError
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
|
||||
/**
|
||||
* @deprecated Replaced with InAppDonationData.Error
|
||||
*
|
||||
* This needs to remain until all the old jobs are through people's systems (90 days from release + timeout)
|
||||
*/
|
||||
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
|
||||
|
||||
/**
|
||||
* Google Pay errors, which happen well before a user would ever be charged.
|
||||
*/
|
||||
sealed class GooglePayError(source: DonationErrorSource, cause: Throwable) : DonationError(source, cause) {
|
||||
class NotAvailableError(source: DonationErrorSource, cause: Throwable) : GooglePayError(source, cause)
|
||||
class RequestTokenError(source: DonationErrorSource, cause: Throwable) : GooglePayError(source, cause)
|
||||
}
|
||||
|
||||
@@ -38,8 +42,6 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
*/
|
||||
sealed class GiftRecipientVerificationError(cause: Throwable) : DonationError(DonationErrorSource.GIFT, cause) {
|
||||
object SelectedRecipientIsInvalid : GiftRecipientVerificationError(Exception("Selected recipient is invalid."))
|
||||
object SelectedRecipientDoesNotSupportGifts : GiftRecipientVerificationError(Exception("Selected recipient does not support gifts."))
|
||||
class FailedToFetchProfile(cause: Throwable) : GiftRecipientVerificationError(Exception("Failed to fetch recipient profile.", cause))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +108,7 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
* Timeout elapsed while the user was waiting for badge redemption to complete for a long-running payment.
|
||||
* This is not an indication that redemption failed, just that it could take a few days to process the payment.
|
||||
*/
|
||||
class DonationPending(source: DonationErrorSource, val gatewayRequest: GatewayRequest) : BadgeRedemptionError(source, Exception("Long-running donation is still pending."))
|
||||
class DonationPending(source: DonationErrorSource, val inAppPayment: InAppPaymentTable.InAppPayment) : BadgeRedemptionError(source, Exception("Long-running donation is still pending."))
|
||||
|
||||
/**
|
||||
* Timeout elapsed while the user was waiting for badge redemption to complete. This is not an indication that
|
||||
@@ -133,20 +135,11 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
source to PublishSubject.create()
|
||||
}
|
||||
|
||||
private val donationErrorsSubjectUiSessionMap: MutableMap<Long, Subject<DonationError>> = mutableMapOf()
|
||||
|
||||
@JvmStatic
|
||||
fun getErrorsForSource(donationErrorSource: DonationErrorSource): Observable<DonationError> {
|
||||
return donationErrorSubjectSourceMap[donationErrorSource]!!
|
||||
}
|
||||
|
||||
fun getErrorsForUiSessionKey(uiSessionKey: Long): Observable<DonationError> {
|
||||
val subject: Subject<DonationError> = donationErrorsSubjectUiSessionMap[uiSessionKey] ?: PublishSubject.create()
|
||||
donationErrorsSubjectUiSessionMap[uiSessionKey] = subject
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun DonationError.toDonationErrorValue(): DonationErrorValue {
|
||||
return when (this) {
|
||||
@@ -182,7 +175,6 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
@JvmOverloads
|
||||
fun routeBackgroundError(
|
||||
context: Context,
|
||||
uiSessionKey: Long,
|
||||
error: DonationError,
|
||||
suppressNotification: Boolean = true
|
||||
) {
|
||||
@@ -191,17 +183,12 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
return
|
||||
}
|
||||
|
||||
val subject: Subject<DonationError>? = donationErrorsSubjectUiSessionMap[uiSessionKey]
|
||||
when {
|
||||
subject != null && subject.hasObservers() -> {
|
||||
Log.i(TAG, "Routing background donation error to uiSessionKey $uiSessionKey dialog", error)
|
||||
subject.onNext(error)
|
||||
}
|
||||
suppressNotification -> {
|
||||
Log.i(TAG, "Suppressing notification for error.", error)
|
||||
}
|
||||
else -> {
|
||||
Log.i(TAG, "Routing background donation error to uiSessionKey $uiSessionKey notification", error)
|
||||
Log.i(TAG, "Routing background donation error to notification", error)
|
||||
DonationErrorNotifications.displayErrorNotification(context, error)
|
||||
}
|
||||
}
|
||||
@@ -211,8 +198,7 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
* Route a given donation error, which will either pipe it out to an appropriate subject
|
||||
* or, if the subject has no observers, post it as a notification.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun routeDonationError(context: Context, error: DonationError) {
|
||||
private fun routeDonationError(context: Context, error: DonationError) {
|
||||
val subject: Subject<DonationError> = donationErrorSubjectSourceMap[error.source]!!
|
||||
when {
|
||||
subject.hasObservers() -> {
|
||||
@@ -226,11 +212,6 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getGooglePayRequestTokenError(source: DonationErrorSource, throwable: Throwable): DonationError {
|
||||
return GooglePayError.RequestTokenError(source, throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a throwable into a payment setup error. This should only be used when
|
||||
* handling errors handed back via the Stripe API or via PayPal, when we know for sure that no
|
||||
@@ -260,21 +241,6 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun oneTimeDonationAmountTooSmall(source: DonationErrorSource): DonationError = OneTimeDonationError.AmountTooSmallError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun oneTimeDonationAmountTooLarge(source: DonationErrorSource): DonationError = OneTimeDonationError.AmountTooLargeError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun invalidCurrencyForOneTimeDonation(source: DonationErrorSource): DonationError = OneTimeDonationError.InvalidCurrencyError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun timeoutWaitingForToken(source: DonationErrorSource): DonationError = BadgeRedemptionError.TimeoutWaitingForTokenError(source)
|
||||
|
||||
@JvmStatic
|
||||
fun donationPending(source: DonationErrorSource, gatewayRequest: GatewayRequest) = BadgeRedemptionError.DonationPending(source, gatewayRequest)
|
||||
|
||||
@JvmStatic
|
||||
fun genericBadgeRedemptionFailure(source: DonationErrorSource): DonationError = BadgeRedemptionError.GenericError(source)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.DialogInterface
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
|
||||
@@ -12,6 +13,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
* Donation Error Dialogs.
|
||||
*/
|
||||
object DonationErrorDialogs {
|
||||
|
||||
/**
|
||||
* Displays a dialog, and returns a handle to it for dismissal.
|
||||
*/
|
||||
@@ -36,6 +38,30 @@ object DonationErrorDialogs {
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog, and returns a handle to it for dismissal.
|
||||
*/
|
||||
fun show(context: Context, inAppPayment: InAppPaymentTable.InAppPayment, callback: DialogCallback): DialogInterface {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
|
||||
builder.setOnDismissListener { callback.onDialogDismissed() }
|
||||
|
||||
val params = DonationErrorParams.create(context, inAppPayment, callback)
|
||||
|
||||
builder.setTitle(params.title)
|
||||
.setMessage(params.message)
|
||||
|
||||
if (params.positiveAction != null) {
|
||||
builder.setPositiveButton(params.positiveAction.label) { _, _ -> params.positiveAction.action() }
|
||||
}
|
||||
|
||||
if (params.negativeAction != null) {
|
||||
builder.setNegativeButton(params.negativeAction.label) { _, _ -> params.negativeAction.action() }
|
||||
}
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
abstract class DialogCallback : DonationErrorParams.Callback<Unit> {
|
||||
|
||||
override fun onCancel(context: Context): DonationErrorParams.ErrorAction<Unit>? {
|
||||
|
||||
@@ -6,6 +6,10 @@ import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeFailureCode
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
|
||||
class DonationErrorParams<V> private constructor(
|
||||
@StringRes val title: Int,
|
||||
@@ -25,46 +29,61 @@ class DonationErrorParams<V> private constructor(
|
||||
callback: Callback<V>
|
||||
): DonationErrorParams<V> {
|
||||
return when (throwable) {
|
||||
is DonationError.GiftRecipientVerificationError -> getVerificationErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.StripeDeclinedError -> getStripeDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.StripeFailureCodeError -> getStripeFailureCodeErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.PayPalDeclinedError -> getPayPalDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.GiftRecipientVerificationError -> getVerificationErrorParams(context, callback)
|
||||
is DonationError.PaymentSetupError.StripeDeclinedError -> getStripeDeclinedErrorParams(context, throwable.method, throwable.declineCode, callback)
|
||||
is DonationError.PaymentSetupError.StripeFailureCodeError -> getStripeFailureCodeErrorParams(context, throwable.method, throwable.failureCode, callback)
|
||||
is DonationError.PaymentSetupError.PayPalDeclinedError -> getPayPalDeclinedErrorParams(context, throwable.code, callback)
|
||||
is DonationError.PaymentSetupError -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__failed_to_validate_badge,
|
||||
message = R.string.DonationsErrors__could_not_validate,
|
||||
positiveAction = callback.onContactSupport(context),
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
is DonationError.BadgeRedemptionError.GenericError -> getGenericRedemptionError(context, throwable, callback)
|
||||
else -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__couldnt_add_badge,
|
||||
message = R.string.DonationsErrors__your_badge_could_not,
|
||||
positiveAction = callback.onContactSupport(context),
|
||||
negativeAction = null
|
||||
)
|
||||
is DonationError.BadgeRedemptionError.DonationPending -> getStillProcessingErrorParams(context, callback)
|
||||
is DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError -> getStillProcessingErrorParams(context, callback)
|
||||
is DonationError.BadgeRedemptionError.FailedToValidateCredentialError -> getBadgeCredentialValidationErrorParams(context, callback)
|
||||
is DonationError.BadgeRedemptionError.GenericError -> getGenericRedemptionError(context, throwable.source.toInAppPaymentType(), callback)
|
||||
else -> getGenericRedemptionError(context, InAppPaymentTable.Type.ONE_TIME_DONATION, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getGenericRedemptionError(context: Context, genericError: DonationError.BadgeRedemptionError.GenericError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return when (genericError.source) {
|
||||
DonationErrorSource.GIFT -> DonationErrorParams(
|
||||
fun <V> create(
|
||||
context: Context,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
callback: Callback<V>
|
||||
): DonationErrorParams<V> {
|
||||
return when (inAppPayment.data.error?.type) {
|
||||
InAppPaymentData.Error.Type.UNKNOWN -> getGenericRedemptionError(context, inAppPayment.type, callback)
|
||||
InAppPaymentData.Error.Type.GOOGLE_PAY_REQUEST_TOKEN -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.INVALID_GIFT_RECIPIENT -> getVerificationErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.ONE_TIME_AMOUNT_TOO_SMALL -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.ONE_TIME_AMOUNT_TOO_LARGE -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.INVALID_CURRENCY -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.PAYMENT_SETUP -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.STRIPE_CODED_ERROR -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.STRIPE_DECLINED_ERROR -> getStripeDeclinedErrorParams(
|
||||
context = context,
|
||||
paymentSourceType = inAppPayment.data.paymentMethodType.toPaymentSourceType() as PaymentSourceType.Stripe,
|
||||
declineCode = StripeDeclineCode.getFromCode(inAppPayment.data.error.data_),
|
||||
callback = callback
|
||||
)
|
||||
InAppPaymentData.Error.Type.STRIPE_FAILURE -> getStripeFailureCodeErrorParams(
|
||||
context = context,
|
||||
paymentSourceType = inAppPayment.data.paymentMethodType.toPaymentSourceType() as PaymentSourceType.Stripe,
|
||||
failureCode = StripeFailureCode.getFromCode(inAppPayment.data.error.data_),
|
||||
callback = callback
|
||||
)
|
||||
InAppPaymentData.Error.Type.PAYPAL_CODED_ERROR -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.PAYPAL_DECLINED_ERROR -> getPayPalDeclinedErrorParams(
|
||||
context = context,
|
||||
payPalDeclineCode = PayPalDeclineCode.KnownCode.fromCode(inAppPayment.data.error.data_!!.toInt())!!,
|
||||
callback = callback
|
||||
)
|
||||
InAppPaymentData.Error.Type.PAYMENT_PROCESSING -> getGenericRedemptionError(context, inAppPayment.type, callback)
|
||||
InAppPaymentData.Error.Type.CREDENTIAL_VALIDATION -> getBadgeCredentialValidationErrorParams(context, callback)
|
||||
InAppPaymentData.Error.Type.REDEMPTION -> getGenericRedemptionError(context, inAppPayment.type, callback)
|
||||
null -> error("No error in data!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getGenericRedemptionError(context: Context, type: InAppPaymentTable.Type, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return when (type) {
|
||||
InAppPaymentTable.Type.ONE_TIME_GIFT -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__donation_failed,
|
||||
message = R.string.DonationsErrors__your_payment_was_processed_but,
|
||||
positiveAction = callback.onContactSupport(context),
|
||||
@@ -80,65 +99,65 @@ class DonationErrorParams<V> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return when (verificationError) {
|
||||
is DonationError.GiftRecipientVerificationError.FailedToFetchProfile -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__cannot_send_donation,
|
||||
message = R.string.DonationsErrors__your_donation_could_not_be_sent,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
|
||||
else -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__cannot_send_donation,
|
||||
message = R.string.DonationsErrors__this_user_cant_receive_donations_until,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
}
|
||||
private fun <V> getVerificationErrorParams(context: Context, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__cannot_send_donation,
|
||||
message = R.string.DonationsErrors__this_user_cant_receive_donations_until,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun <V> getPayPalDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.PayPalDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return when (declinedError.code) {
|
||||
private fun <V> getPayPalDeclinedErrorParams(
|
||||
context: Context,
|
||||
payPalDeclineCode: PayPalDeclineCode.KnownCode,
|
||||
callback: Callback<V>
|
||||
): DonationErrorParams<V> {
|
||||
return when (payPalDeclineCode) {
|
||||
PayPalDeclineCode.KnownCode.DECLINED -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank_for_more_information_if_this_was_a_paypal)
|
||||
else -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_another_payment_method_or_contact_your_bank)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getStripeDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
if (!declinedError.method.hasDeclineCodeSupport()) {
|
||||
private fun <V> getStripeDeclinedErrorParams(
|
||||
context: Context,
|
||||
paymentSourceType: PaymentSourceType.Stripe,
|
||||
declineCode: StripeDeclineCode,
|
||||
callback: Callback<V>
|
||||
): DonationErrorParams<V> {
|
||||
if (!paymentSourceType.hasDeclineCodeSupport()) {
|
||||
return getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
|
||||
fun unexpectedDeclinedError(declinedError: DonationError.PaymentSetupError.StripeDeclinedError): Nothing {
|
||||
error("Unexpected declined error: ${declinedError.declineCode} during ${declinedError.method} processing.")
|
||||
fun unexpectedDeclinedError(declineCode: StripeDeclineCode, paymentSourceType: PaymentSourceType.Stripe): Nothing {
|
||||
error("Unexpected declined error: $declineCode during $paymentSourceType processing.")
|
||||
}
|
||||
|
||||
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
|
||||
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> this::getTryCreditCardAgainParams
|
||||
PaymentSourceType.Stripe.GooglePay -> this::getGoToGooglePayParams
|
||||
else -> this::getLearnMoreParams
|
||||
}
|
||||
|
||||
return when (declinedError.declineCode) {
|
||||
is StripeDeclineCode.Known -> when (declinedError.declineCode.code) {
|
||||
return when (declineCode) {
|
||||
is StripeDeclineCode.Known -> when (declineCode.code) {
|
||||
StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -146,30 +165,30 @@ class DonationErrorParams<V> private constructor(
|
||||
StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_has_expired
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -177,40 +196,40 @@ class DonationErrorParams<V> private constructor(
|
||||
StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_month
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_year
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams(
|
||||
context,
|
||||
callback,
|
||||
when (declinedError.method) {
|
||||
when (paymentSourceType) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
else -> unexpectedDeclinedError(declinedError)
|
||||
else -> unexpectedDeclinedError(declineCode, paymentSourceType)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -224,15 +243,20 @@ class DonationErrorParams<V> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getStripeFailureCodeErrorParams(context: Context, failureCodeError: DonationError.PaymentSetupError.StripeFailureCodeError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
if (!failureCodeError.method.hasFailureCodeSupport()) {
|
||||
private fun <V> getStripeFailureCodeErrorParams(
|
||||
context: Context,
|
||||
paymentSourceType: PaymentSourceType.Stripe,
|
||||
failureCode: StripeFailureCode,
|
||||
callback: Callback<V>
|
||||
): DonationErrorParams<V> {
|
||||
if (!paymentSourceType.hasFailureCodeSupport()) {
|
||||
return getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
|
||||
return when (failureCodeError.failureCode) {
|
||||
return when (failureCode) {
|
||||
is StripeFailureCode.Known -> {
|
||||
val errorText = failureCodeError.failureCode.mapToErrorStringResource()
|
||||
when (failureCodeError.failureCode.code) {
|
||||
val errorText = failureCode.mapToErrorStringResource()
|
||||
when (failureCode.code) {
|
||||
StripeFailureCode.Code.REFER_TO_CUSTOMER -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, errorText)
|
||||
StripeFailureCode.Code.DEBIT_DISPUTED -> getLearnMoreParams(context, callback, errorText)
|
||||
@@ -248,10 +272,29 @@ class DonationErrorParams<V> private constructor(
|
||||
StripeFailureCode.Code.GENERIC_COULD_NOT_PROCESS -> getTryBankTransferAgainParams(context, callback, errorText)
|
||||
}
|
||||
}
|
||||
|
||||
is StripeFailureCode.Unknown -> getGenericPaymentSetupErrorParams(context, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <V> getStillProcessingErrorParams(context: Context, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__still_processing,
|
||||
message = R.string.DonationsErrors__your_payment_is_still,
|
||||
positiveAction = callback.onOk(context),
|
||||
negativeAction = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun <V> getBadgeCredentialValidationErrorParams(context: Context, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__failed_to_validate_badge,
|
||||
message = R.string.DonationsErrors__could_not_validate,
|
||||
positiveAction = callback.onContactSupport(context),
|
||||
negativeAction = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun <V> getGenericPaymentSetupErrorParams(context: Context, callback: Callback<V>): DonationErrorParams<V> {
|
||||
return DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -33,6 +32,7 @@ object ActiveSubscriptionPreference {
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.RedemptionState,
|
||||
val activeSubscription: ActiveSubscription.Subscription?,
|
||||
val subscriberRequiresCancel: Boolean,
|
||||
val onContactSupport: () -> Unit,
|
||||
val onPendingClick: (FiatMoney) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
@@ -104,7 +104,7 @@ object ActiveSubscriptionPreference {
|
||||
}
|
||||
|
||||
private fun presentFailureState(model: Model) {
|
||||
if (model.activeSubscription?.isFailedPayment == true || SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) {
|
||||
if (model.activeSubscription?.isFailedPayment == true || model.subscriberRequiresCancel) {
|
||||
presentPaymentFailureState(model)
|
||||
} else {
|
||||
presentRedemptionFailureState(model)
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
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
|
||||
@@ -16,6 +18,8 @@ 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.
|
||||
*
|
||||
* @deprecated This object is deprecated and will be removed once we are sure all jobs have drained.
|
||||
*/
|
||||
object DonationRedemptionJobWatcher {
|
||||
|
||||
@@ -24,7 +28,6 @@ object DonationRedemptionJobWatcher {
|
||||
ONE_TIME
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun hasPendingRedemptionJob(): Boolean {
|
||||
return getDonationRedemptionJobStatus(RedemptionType.SUBSCRIPTION).isInProgress() || getDonationRedemptionJobStatus(RedemptionType.ONE_TIME).isInProgress()
|
||||
@@ -113,9 +116,9 @@ object DonationRedemptionJobWatcher {
|
||||
}
|
||||
|
||||
return DonationSerializationHelper.createPendingOneTimeDonationProto(
|
||||
badge = stripe3DSData.gatewayRequest.badge,
|
||||
badge = Badges.fromDatabaseBadge(stripe3DSData.inAppPayment.data.badge!!),
|
||||
paymentSourceType = stripe3DSData.paymentSourceType,
|
||||
amount = stripe3DSData.gatewayRequest.fiat
|
||||
amount = stripe3DSData.inAppPayment.data.amount!!.toFiatMoney()
|
||||
).copy(
|
||||
timestamp = createTime,
|
||||
pendingVerification = true,
|
||||
@@ -130,8 +133,8 @@ object DonationRedemptionJobWatcher {
|
||||
|
||||
return NonVerifiedMonthlyDonation(
|
||||
timestamp = createTime,
|
||||
price = stripe3DSData.gatewayRequest.fiat,
|
||||
level = stripe3DSData.gatewayRequest.level.toInt(),
|
||||
price = stripe3DSData.inAppPayment.data.amount!!.toFiatMoney(),
|
||||
level = stripe3DSData.inAppPayment.data.level.toInt(),
|
||||
checkedVerification = runAttempt > 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.TerminalDonationDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -70,7 +70,7 @@ class ManageDonationsFragment :
|
||||
|
||||
private val viewModel: ManageDonationsViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
ManageDonationsViewModel.Factory(MonthlyDonationRepository(ApplicationDependencies.getDonationsService()))
|
||||
ManageDonationsViewModel.Factory(RecurringInAppPaymentRepository(ApplicationDependencies.getDonationsService()))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -167,7 +167,7 @@ class ManageDonationsFragment :
|
||||
primaryWrappedButton(
|
||||
text = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_to_signal),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.ONE_TIME))
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.ONE_TIME_DONATION))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -274,6 +274,7 @@ class ManageDonationsFragment :
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
},
|
||||
activeSubscription = activeSubscription,
|
||||
subscriberRequiresCancel = state.subscriberRequiresCancel,
|
||||
onPendingClick = {
|
||||
displayPendingDialog(it)
|
||||
}
|
||||
@@ -295,6 +296,7 @@ class ManageDonationsFragment :
|
||||
redemptionState = ManageDonationsState.RedemptionState.IN_PROGRESS,
|
||||
onContactSupport = {},
|
||||
activeSubscription = null,
|
||||
subscriberRequiresCancel = state.subscriberRequiresCancel,
|
||||
onPendingClick = {}
|
||||
)
|
||||
)
|
||||
@@ -318,7 +320,7 @@ class ManageDonationsFragment :
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_person_24),
|
||||
isEnabled = state.getMonthlyDonorRedemptionState() != ManageDonationsState.RedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.RECURRING_DONATION))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -422,6 +424,7 @@ class ManageDonationsFragment :
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
val message = if (isIdeal) {
|
||||
R.string.DonationsErrors__your_ideal_couldnt_be_processed
|
||||
@@ -445,6 +448,6 @@ class ManageDonationsFragment :
|
||||
}
|
||||
|
||||
override fun onMakeAMonthlyDonation() {
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(DonateToSignalType.MONTHLY))
|
||||
findNavController().safeNavigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToDonateToSignalFragment(InAppPaymentTable.Type.RECURRING_DONATION))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ data class ManageDonationsState(
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
val pendingOneTimeDonation: PendingOneTimeDonation? = null,
|
||||
val nonVerifiedMonthlyDonation: NonVerifiedMonthlyDonation? = null,
|
||||
val subscriberRequiresCancel: Boolean = false,
|
||||
private val subscriptionRedemptionState: RedemptionState = RedemptionState.NONE
|
||||
) {
|
||||
|
||||
|
||||
@@ -12,8 +12,11 @@ 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.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
@@ -23,7 +26,7 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Optional
|
||||
|
||||
class ManageDonationsViewModel(
|
||||
private val subscriptionsRepository: MonthlyDonationRepository
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(ManageDonationsState())
|
||||
@@ -62,7 +65,15 @@ class ManageDonationsViewModel(
|
||||
disposables.clear()
|
||||
|
||||
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
|
||||
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription()
|
||||
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
|
||||
disposables += Single.fromCallable {
|
||||
InAppPaymentsRepository.getShouldCancelSubscriptionBeforeNextSubscribeAttempt(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}.subscribeBy { requiresCancel ->
|
||||
store.update {
|
||||
it.copy(subscriberRequiresCancel = requiresCancel)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Recipient.observable(Recipient.self().id).map { it.badges }.subscribeBy { badges ->
|
||||
store.update { state ->
|
||||
@@ -76,7 +87,7 @@ class ManageDonationsViewModel(
|
||||
store.update { it.copy(hasReceipts = hasReceipts) }
|
||||
}
|
||||
|
||||
disposables += DonationRedemptionJobWatcher.watchSubscriptionRedemption().subscribeBy { redemptionStatus ->
|
||||
disposables += InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.RECURRING_DONATION).subscribeBy { redemptionStatus ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
nonVerifiedMonthlyDonation = if (redemptionStatus is DonationRedemptionJobStatus.PendingExternalVerification) redemptionStatus.nonVerifiedMonthlyDonation else null,
|
||||
@@ -87,7 +98,7 @@ class ManageDonationsViewModel(
|
||||
|
||||
disposables += Observable.combineLatest(
|
||||
SignalStore.donationsValues().observablePendingOneTimeDonation,
|
||||
DonationRedemptionJobWatcher.watchOneTimeRedemption()
|
||||
InAppPaymentsRepository.observeInAppPaymentRedemption(InAppPaymentTable.Type.ONE_TIME_DONATION)
|
||||
) { pendingFromStore, pendingFromJob ->
|
||||
if (pendingFromStore.isPresent) {
|
||||
pendingFromStore
|
||||
@@ -145,7 +156,7 @@ class ManageDonationsViewModel(
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val subscriptionsRepository: MonthlyDonationRepository
|
||||
private val subscriptionsRepository: RecurringInAppPaymentRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ManageDonationsViewModel(subscriptionsRepository))!!
|
||||
|
||||
@@ -19,7 +19,10 @@ object PayPalButton {
|
||||
class ViewHolder(binding: PaypalButtonBinding) : BindingViewHolder<Model, PaypalButtonBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.paypalButton.isEnabled = model.isEnabled
|
||||
binding.paypalButton.setOnClickListener { model.onClick() }
|
||||
binding.paypalButton.setOnClickListener {
|
||||
binding.paypalButton.isEnabled = false
|
||||
model.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
@@ -28,7 +30,6 @@ import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -236,7 +237,8 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
if (recipient.isSelf) {
|
||||
sectionHeaderPref(DSLSettingsText.from("Donations"))
|
||||
|
||||
val subscriber: Subscriber? = SignalStore.donationsValues().getSubscriber()
|
||||
// TODO [alex] - DB on main thread!
|
||||
val subscriber: InAppPaymentSubscriberRecord? = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val summary = if (subscriber != null) {
|
||||
"""currency code: ${subscriber.currencyCode}
|
||||
|subscriber id: ${subscriber.subscriberId.serialize()}
|
||||
|
||||
@@ -160,18 +160,20 @@ class DSLConfiguration {
|
||||
text: DSLSettingsText,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
disableOnClick: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.Primary(text, icon, isEnabled, onClick)
|
||||
val preference = Button.Model.Primary(text, icon, isEnabled, disableOnClick, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun primaryWrappedButton(
|
||||
text: DSLSettingsText,
|
||||
isEnabled: Boolean = true,
|
||||
disableOnClick: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.PrimaryWrapped(text, null, isEnabled, onClick)
|
||||
val preference = Button.Model.PrimaryWrapped(text, null, isEnabled, disableOnClick, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -179,9 +181,10 @@ class DSLConfiguration {
|
||||
text: DSLSettingsText,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
disableOnClick: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.Tonal(text, icon, isEnabled, onClick)
|
||||
val preference = Button.Model.Tonal(text, icon, isEnabled, disableOnClick, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -189,9 +192,10 @@ class DSLConfiguration {
|
||||
text: DSLSettingsText,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
disableOnClick: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.TonalWrapped(text, icon, isEnabled, onClick)
|
||||
val preference = Button.Model.TonalWrapped(text, icon, isEnabled, disableOnClick, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
@@ -199,9 +203,10 @@ class DSLConfiguration {
|
||||
text: DSLSettingsText,
|
||||
icon: DSLSettingsIcon? = null,
|
||||
isEnabled: Boolean = true,
|
||||
disableOnClick: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = Button.Model.SecondaryNoOutline(text, icon, isEnabled, onClick)
|
||||
val preference = Button.Model.SecondaryNoOutline(text, icon, isEnabled, disableOnClick, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ object Button {
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
val disableOnClick: Boolean,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<T>(
|
||||
title = title,
|
||||
@@ -37,8 +38,9 @@ object Button {
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
disableOnClick: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<Primary>(title, icon, isEnabled, onClick)
|
||||
) : Model<Primary>(title, icon, isEnabled, disableOnClick, onClick)
|
||||
|
||||
/**
|
||||
* Large primary button with width set to wrap_content
|
||||
@@ -47,29 +49,33 @@ object Button {
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
disableOnClick: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<PrimaryWrapped>(title, icon, isEnabled, onClick)
|
||||
) : Model<PrimaryWrapped>(title, icon, isEnabled, disableOnClick, onClick)
|
||||
|
||||
class Tonal(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
disableOnClick: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<Tonal>(title, icon, isEnabled, onClick)
|
||||
) : Model<Tonal>(title, icon, isEnabled, disableOnClick, onClick)
|
||||
|
||||
class TonalWrapped(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
disableOnClick: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<TonalWrapped>(title, icon, isEnabled, onClick)
|
||||
) : Model<TonalWrapped>(title, icon, isEnabled, disableOnClick, onClick)
|
||||
|
||||
class SecondaryNoOutline(
|
||||
title: DSLSettingsText?,
|
||||
icon: DSLSettingsIcon?,
|
||||
isEnabled: Boolean,
|
||||
disableOnClick: Boolean,
|
||||
onClick: () -> Unit
|
||||
) : Model<SecondaryNoOutline>(title, icon, isEnabled, onClick)
|
||||
) : Model<SecondaryNoOutline>(title, icon, isEnabled, disableOnClick, onClick)
|
||||
}
|
||||
|
||||
class ViewHolder<T : Model<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
@@ -79,6 +85,7 @@ object Button {
|
||||
override fun bind(model: T) {
|
||||
button.text = model.title?.resolve(context)
|
||||
button.setOnClickListener {
|
||||
button.isEnabled = model.isEnabled && !model.disableOnClick
|
||||
model.onClick()
|
||||
}
|
||||
button.icon = model.icon?.resolve(context)
|
||||
|
||||
Reference in New Issue
Block a user