mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Pass InAppPayments around by ID instead of passing the entire object.
This commit is contained in:
@@ -24,7 +24,6 @@ import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
@@ -138,12 +137,12 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to handle purchase.", e)
|
||||
InAppPaymentsRepository.handlePipelineError(
|
||||
inAppPaymentId = id,
|
||||
donationErrorSource = DonationErrorSource.BACKUPS,
|
||||
paymentSourceType = PaymentSourceType.GooglePlayBilling,
|
||||
error = e
|
||||
)
|
||||
withContext(SignalDispatchers.IO) {
|
||||
InAppPaymentsRepository.handlePipelineError(
|
||||
inAppPaymentId = id,
|
||||
error = e
|
||||
)
|
||||
}
|
||||
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
|
||||
@@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
@@ -58,10 +57,6 @@ class GiftFlowConfirmationFragment :
|
||||
EmojiSearchFragment.Callback,
|
||||
InAppPaymentCheckoutDelegate.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: GiftFlowViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
@@ -118,7 +113,7 @@ class GiftFlowConfirmationFragment :
|
||||
lifecycleDisposable += viewModel.insertInAppPayment().subscribe { inAppPayment ->
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
|
||||
inAppPayment
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -266,8 +261,7 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -276,15 +270,14 @@ class GiftFlowConfirmationFragment :
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment)
|
||||
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment.id)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
@@ -36,6 +38,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Displayed after the user completes the donation flow for a bank transfer.
|
||||
@@ -43,13 +46,19 @@ import org.thoughtcrime.securesms.util.SpanUtil
|
||||
class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
private val args: DonationPendingBottomSheetArgs by navArgs()
|
||||
private val viewModel: DonationPendingBottomSheetViewModel by viewModel {
|
||||
DonationPendingBottomSheetViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
DonationPendingBottomSheetContent(
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
onDoneClick = this::onDoneClick
|
||||
)
|
||||
val inAppPayment by viewModel.inAppPayment.collectAsStateWithLifecycle()
|
||||
|
||||
if (inAppPayment != null)
|
||||
DonationPendingBottomSheetContent(
|
||||
badge = Badges.fromDatabaseBadge(inAppPayment!!.data.badge!!),
|
||||
onDoneClick = this::onDoneClick
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDoneClick() {
|
||||
@@ -59,7 +68,8 @@ class DonationPendingBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
|
||||
if (!args.inAppPayment.type.recurring) {
|
||||
val iap = viewModel.inAppPayment.value
|
||||
if (iap != null && !iap.type.recurring) {
|
||||
findNavController().popBackStack()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
class DonationPendingBottomSheetViewModel(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
private val internalInAppPayment = MutableStateFlow<InAppPaymentTable.InAppPayment?>(null)
|
||||
val inAppPayment: StateFlow<InAppPaymentTable.InAppPayment?> = internalInAppPayment
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
}
|
||||
|
||||
internalInAppPayment.update { inAppPayment }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,12 +112,15 @@ object InAppPaymentsRepository {
|
||||
* 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.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun handlePipelineError(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
donationErrorSource: DonationErrorSource,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
error: Throwable
|
||||
) {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
val donationErrorSource = inAppPayment.type.toErrorSource()
|
||||
val paymentSourceType = inAppPayment.data.paymentMethodType.toPaymentSourceType()
|
||||
|
||||
if (error is InAppPaymentError) {
|
||||
setErrorIfNotPresent(inAppPaymentId, error.inAppPaymentDataError)
|
||||
return
|
||||
@@ -132,7 +135,7 @@ object InAppPaymentsRepository {
|
||||
val inAppPaymentError = InAppPaymentError.fromDonationError(donationError)?.inAppPaymentDataError
|
||||
if (inAppPaymentError != null) {
|
||||
Log.w(TAG, "Detected a terminal error.")
|
||||
setErrorIfNotPresent(inAppPaymentId, inAppPaymentError).subscribe()
|
||||
setErrorIfNotPresent(inAppPaymentId, inAppPaymentError)
|
||||
} else {
|
||||
Log.w(TAG, "Detected a temporary error.")
|
||||
temporaryErrorProcessor.onNext(inAppPaymentId to donationError)
|
||||
@@ -150,20 +153,19 @@ object InAppPaymentsRepository {
|
||||
/**
|
||||
* 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) {
|
||||
Log.d(TAG, "Setting error on InAppPayment[$inAppPaymentId]")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(error = error)
|
||||
)
|
||||
@WorkerThread
|
||||
private fun setErrorIfNotPresent(inAppPaymentId: InAppPaymentTable.InAppPaymentId, error: InAppPaymentData.Error?) {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
if (inAppPayment.data.error == null) {
|
||||
Log.d(TAG, "Setting error on InAppPayment[$inAppPaymentId]")
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
notified = false,
|
||||
state = InAppPaymentTable.State.END,
|
||||
data = inAppPayment.data.copy(error = error)
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,6 +524,7 @@ object InAppPaymentsRepository {
|
||||
nonVerifiedMonthlyDonation = inAppPayment.toNonVerifiedMonthlyDonation()
|
||||
)
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.PENDING, InAppPaymentTable.State.TRANSACTING, InAppPaymentTable.State.REQUIRED_ACTION_COMPLETED -> {
|
||||
if (inAppPayment.data.redemption?.keepAlive == true) {
|
||||
DonationRedemptionJobStatus.PendingKeepAlive
|
||||
@@ -531,6 +534,7 @@ object InAppPaymentsRepository {
|
||||
DonationRedemptionJobStatus.PendingReceiptRequest
|
||||
}
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.END -> {
|
||||
if (type.recurring && inAppPayment.data.error != null) {
|
||||
DonationRedemptionJobStatus.FailedSubscription
|
||||
|
||||
@@ -89,7 +89,7 @@ class InAppPaymentsBottomSheetDelegate(
|
||||
private fun handleLegacyVerifiedMonthlyDonationSheets() {
|
||||
SignalStore.inAppPayments.consumeVerifiedSubscription3DSData()?.also {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(it.inAppPayment).build().toBundle()
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(it.inAppPayment.id).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class InAppPaymentsBottomSheetDelegate(
|
||||
.show(fragmentManager, null)
|
||||
} else if (payment.data.error != null && payment.state == InAppPaymentTable.State.PENDING) {
|
||||
DonationPendingBottomSheet().apply {
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(payment).build().toBundle()
|
||||
arguments = DonationPendingBottomSheetArgs.Builder(payment.id).build().toBundle()
|
||||
}.show(fragmentManager, null)
|
||||
} else if (isUnexpectedCancellation(payment.state, payment.data) && SignalStore.inAppPayments.showMonthlyDonationCanceledDialog) {
|
||||
MonthlyDonationCanceledBottomSheetDialogFragment.show(fragmentManager)
|
||||
|
||||
@@ -165,7 +165,7 @@ class DonateToSignalFragment :
|
||||
|
||||
is DonateToSignalAction.DisplayGatewaySelectorDialog -> {
|
||||
Log.d(TAG, "Presenting gateway selector for ${action.inAppPayment.id}")
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.inAppPayment)
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToGatewaySelectorBottomSheetDialog(action.inAppPayment.id)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
}
|
||||
@@ -173,8 +173,7 @@ class DonateToSignalFragment :
|
||||
is DonateToSignalAction.CancelSubscription -> {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION,
|
||||
null,
|
||||
InAppPaymentType.RECURRING_DONATION
|
||||
null
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
@@ -184,16 +183,14 @@ class DonateToSignalFragment :
|
||||
if (action.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.PAYPAL) {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION,
|
||||
action.inAppPayment,
|
||||
action.inAppPayment.type
|
||||
action.inAppPayment.id
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
} else {
|
||||
val navAction = DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION,
|
||||
action.inAppPayment,
|
||||
action.inAppPayment.type
|
||||
action.inAppPayment.id
|
||||
)
|
||||
|
||||
findNavController().safeNavigate(navAction)
|
||||
@@ -477,8 +474,7 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -487,22 +483,21 @@ class DonateToSignalFragment :
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
inAppPayment,
|
||||
inAppPayment.type
|
||||
inAppPayment.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun navigateToIdealDetailsFragment(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToIdealTransferDetailsFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun navigateToBankTransferMandate(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToBankTransferMandateFragment(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun onPaymentComplete(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
@@ -523,7 +518,7 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment))
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToDonationPendingBottomSheet(inAppPayment.id))
|
||||
}
|
||||
|
||||
override fun exitCheckoutFlow() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -17,7 +18,10 @@ 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 kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayApi
|
||||
@@ -126,12 +130,17 @@ class InAppPaymentCheckoutDelegate(
|
||||
}
|
||||
|
||||
private fun handleSuccessfulDonationProcessorActionResult(result: InAppPaymentProcessorActionResult) {
|
||||
setActivityResult(result.action, result.inAppPaymentType)
|
||||
|
||||
if (result.action == InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION) {
|
||||
callback.onSubscriptionCancelled(result.inAppPaymentType)
|
||||
setActivityResult(result.action, InAppPaymentType.RECURRING_DONATION)
|
||||
callback.onSubscriptionCancelled(InAppPaymentType.RECURRING_DONATION)
|
||||
} else {
|
||||
callback.onPaymentComplete(result.inAppPayment!!)
|
||||
fragment.lifecycleScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(result.inAppPaymentId!!)!!
|
||||
}
|
||||
|
||||
callback.onPaymentComplete(inAppPayment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
@Parcelize
|
||||
class InAppPaymentProcessorActionResult(
|
||||
val action: InAppPaymentProcessorAction,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment?,
|
||||
val inAppPaymentType: InAppPaymentType,
|
||||
val inAppPaymentId: InAppPaymentTable.InAppPaymentId?,
|
||||
val status: Status
|
||||
) : Parcelable {
|
||||
enum class Status {
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.errors.Do
|
||||
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.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPayPalOneTimeSetupJob
|
||||
@@ -47,17 +48,17 @@ object SharedInAppPaymentPipeline {
|
||||
* This method will enqueue the proper setup job based off the type of [InAppPaymentTable.InAppPayment] and then
|
||||
* await for either [InAppPaymentTable.State.PENDING], [InAppPaymentTable.State.REQUIRES_ACTION] or [InAppPaymentTable.State.END]
|
||||
* before moving further, handling each state appropriately.
|
||||
*
|
||||
* @param requiredActionHandler Dispatch method for handling PayPal input, 3DS, iDEAL, etc.
|
||||
*/
|
||||
@CheckResult
|
||||
fun awaitTransaction(
|
||||
inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
paymentSource: PaymentSource,
|
||||
requiredActionHandler: RequiredActionHandler
|
||||
): Completable {
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPayment.id)
|
||||
oneTimeRequiredActionHandler: RequiredActionHandler,
|
||||
monthlyRequiredActionHandler: RequiredActionHandler
|
||||
): Single<InAppPaymentTable.InAppPayment> {
|
||||
return InAppPaymentsRepository.observeUpdates(inAppPaymentId)
|
||||
.doOnSubscribe {
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
val job = if (inAppPayment.type.recurring) {
|
||||
if (inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.PAYPAL) {
|
||||
InAppPaymentPayPalRecurringSetupJob.create(inAppPayment, paymentSource)
|
||||
@@ -76,25 +77,27 @@ object SharedInAppPaymentPipeline {
|
||||
}
|
||||
.skipWhile { it.state != InAppPaymentTable.State.PENDING && it.state != InAppPaymentTable.State.REQUIRES_ACTION && it.state != InAppPaymentTable.State.END }
|
||||
.firstOrError()
|
||||
.flatMapCompletable { iap ->
|
||||
.flatMap { iap ->
|
||||
when (iap.state) {
|
||||
InAppPaymentTable.State.PENDING -> {
|
||||
Log.w(TAG, "Payment of type ${inAppPayment.type} is pending. Awaiting completion.")
|
||||
Log.w(TAG, "Payment of type ${iap.type} is pending. Awaiting completion.")
|
||||
awaitRedemption(iap, paymentSource.type)
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.REQUIRES_ACTION -> {
|
||||
Log.d(TAG, "Payment of type ${inAppPayment.type} requires user action to set up.", true)
|
||||
requiredActionHandler(iap.id).andThen(awaitTransaction(iap, paymentSource, requiredActionHandler))
|
||||
Log.d(TAG, "Payment of type ${iap.type} requires user action to set up.", true)
|
||||
|
||||
val requiredActionHandler = if (iap.type.recurring) monthlyRequiredActionHandler else oneTimeRequiredActionHandler
|
||||
requiredActionHandler(iap.id).andThen(awaitTransaction(iap.id, paymentSource, oneTimeRequiredActionHandler, monthlyRequiredActionHandler))
|
||||
}
|
||||
|
||||
InAppPaymentTable.State.END -> {
|
||||
if (iap.data.error != null) {
|
||||
Log.d(TAG, "IAP error detected.", true)
|
||||
Completable.error(InAppPaymentError(iap.data.error))
|
||||
Single.error(InAppPaymentError(iap.data.error))
|
||||
} else {
|
||||
Log.d(TAG, "Unexpected early end state. Possible payment failure.", true)
|
||||
Completable.error(DonationError.genericPaymentFailure(inAppPayment.type.toErrorSource()))
|
||||
Single.error(DonationError.genericPaymentFailure(iap.type.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ object SharedInAppPaymentPipeline {
|
||||
* Waits 10 seconds for the redemption to complete, and fails with a temporary error afterwards.
|
||||
*/
|
||||
@CheckResult
|
||||
fun awaitRedemption(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceType: PaymentSourceType): Completable {
|
||||
fun awaitRedemption(inAppPayment: InAppPaymentTable.InAppPayment, paymentSourceType: PaymentSourceType): Single<InAppPaymentTable.InAppPayment> {
|
||||
val isLongRunning = paymentSourceType.isBankTransfer
|
||||
val errorSource = when (inAppPayment.type) {
|
||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||
@@ -131,19 +134,6 @@ object SharedInAppPaymentPipeline {
|
||||
throw InAppPaymentError(it.data.error)
|
||||
}
|
||||
it
|
||||
}.firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError)).ignoreElement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic error handling for donations.
|
||||
*/
|
||||
fun handleError(
|
||||
throwable: Throwable,
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
paymentSourceType: PaymentSourceType,
|
||||
donationErrorSource: DonationErrorSource
|
||||
) {
|
||||
Log.w(TAG, "Failure in $donationErrorSource payment pipeline...", throwable, true)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, donationErrorSource, paymentSourceType, throwable)
|
||||
}.firstOrError().timeout(10, TimeUnit.SECONDS, Single.error(timeoutError))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,14 @@ import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.signal.donations.InAppPaymentType
|
||||
@@ -30,12 +34,16 @@ import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
private val binding by ViewBinderDelegate(CreditCardFragmentBinding::bind)
|
||||
private val args: CreditCardFragmentArgs by navArgs()
|
||||
private val viewModel: CreditCardViewModel by viewModels()
|
||||
private val viewModel: CreditCardViewModel by viewModel {
|
||||
CreditCardViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.checkout_flow
|
||||
@@ -43,7 +51,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, null, args.inAppPayment.id)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, null, args.inAppPaymentId)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
@@ -53,21 +61,27 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
}
|
||||
}
|
||||
|
||||
binding.continueButton.text = when (args.inAppPayment.type) {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
InAppPaymentType.RECURRING_BACKUP -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__pay_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney()))
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel.inAppPayment.collectLatest { inAppPayment ->
|
||||
binding.continueButton.text = when (inAppPayment.type) {
|
||||
InAppPaymentType.RECURRING_DONATION -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
InAppPaymentType.RECURRING_BACKUP -> {
|
||||
getString(
|
||||
R.string.CreditCardFragment__pay_s_month,
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
getString(R.string.CreditCardFragment__donate_s, FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +133,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
findNavController().safeNavigate(
|
||||
CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
args.inAppPaymentId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.card
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.Calendar
|
||||
|
||||
class CreditCardViewModel : ViewModel() {
|
||||
class CreditCardViewModel(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
private val formStore = RxStore(CreditCardFormState())
|
||||
private val validationProcessor: BehaviorProcessor<CreditCardValidationState> = BehaviorProcessor.create()
|
||||
private val currentYear: Int
|
||||
private val currentMonth: Int
|
||||
|
||||
private val internalInAppPayment = MutableStateFlow<InAppPaymentTable.InAppPayment?>(null)
|
||||
val inAppPayment: Flow<InAppPaymentTable.InAppPayment> = internalInAppPayment.filterNotNull()
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
viewModelScope.launch {
|
||||
val inAppPayment = withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
}
|
||||
|
||||
internalInAppPayment.update { inAppPayment }
|
||||
}
|
||||
|
||||
currentYear = calendar.get(Calendar.YEAR)
|
||||
currentMonth = calendar.get(Calendar.MONTH) + 1
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
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
|
||||
@@ -31,6 +30,7 @@ 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
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Entry point to capturing the necessary payment token to pay for a donation
|
||||
@@ -41,9 +41,9 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private val args: GatewaySelectorBottomSheetArgs by navArgs()
|
||||
|
||||
private val viewModel: GatewaySelectorViewModel by viewModels(factoryProducer = {
|
||||
GatewaySelectorViewModel.Factory(args, requireListener<GooglePayComponent>().googlePayRepository)
|
||||
})
|
||||
private val viewModel: GatewaySelectorViewModel by viewModel {
|
||||
GatewaySelectorViewModel(args, requireListener<GooglePayComponent>().googlePayRepository)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
@@ -59,44 +59,48 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: GatewaySelectorState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
if (state.loading) {
|
||||
space(16.dp)
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
space(16.dp)
|
||||
return@configure
|
||||
}
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
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.")
|
||||
return when (state) {
|
||||
GatewaySelectorState.Loading -> {
|
||||
configure {
|
||||
space(16.dp)
|
||||
customPref(IndeterminateLoadingCircle)
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
is GatewaySelectorState.Ready -> {
|
||||
configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = state.inAppPayment.data.badge!!.let { Badges.fromDatabaseBadge(it) },
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(16.dp)
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), state.inAppPayment)
|
||||
|
||||
space(16.dp)
|
||||
|
||||
state.gatewayOrderStrategy.orderedGateways.forEach { gateway ->
|
||||
when (gateway) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING -> error("Unsupported payment method.")
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderGooglePayButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderGooglePayButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isGooglePayAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -115,7 +119,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderPayPalButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderPayPalButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isPayPalAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -134,7 +138,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderCreditCardButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderCreditCardButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isCreditCardAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -153,7 +157,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderSEPADebitButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderSEPADebitButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isSEPADebitAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
@@ -162,7 +166,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
icon = DSLSettingsIcon.from(R.drawable.bank_transfer),
|
||||
disableOnClick = true,
|
||||
onClick = {
|
||||
val price = args.inAppPayment.data.amount!!.toFiatMoney()
|
||||
val price = state.inAppPayment.data.amount!!.toFiatMoney()
|
||||
if (state.sepaEuroMaximum != null &&
|
||||
price.currency == CurrencyUtil.EURO &&
|
||||
price.amount > state.sepaEuroMaximum.amount
|
||||
@@ -181,7 +185,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState) {
|
||||
private fun DSLConfiguration.renderIDEALButton(state: GatewaySelectorState.Ready) {
|
||||
if (state.isIDEALAvailable) {
|
||||
space(16.dp)
|
||||
|
||||
|
||||
@@ -7,17 +7,15 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.getAvaila
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.util.Locale
|
||||
|
||||
class GatewaySelectorRepository(
|
||||
private val donationsService: DonationsService
|
||||
) {
|
||||
object GatewaySelectorRepository {
|
||||
fun getAvailableGatewayConfiguration(currencyCode: String): Single<GatewayConfiguration> {
|
||||
return Single.fromCallable {
|
||||
donationsService.getDonationsConfiguration(Locale.getDefault())
|
||||
AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault())
|
||||
}.flatMap { it.flattenResult() }
|
||||
.map { configuration ->
|
||||
val available = configuration.getAvailablePaymentMethods(currencyCode).map {
|
||||
|
||||
@@ -3,14 +3,17 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val gatewayOrderStrategy: GatewayOrderStrategy,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val loading: Boolean = true,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
val isSEPADebitAvailable: Boolean = false,
|
||||
val isIDEALAvailable: Boolean = false,
|
||||
val sepaEuroMaximum: FiatMoney? = null
|
||||
)
|
||||
sealed interface GatewaySelectorState {
|
||||
data object Loading : GatewaySelectorState
|
||||
|
||||
data class Ready(
|
||||
val gatewayOrderStrategy: GatewayOrderStrategy,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false,
|
||||
val isSEPADebitAvailable: Boolean = false,
|
||||
val isIDEALAvailable: Boolean = false,
|
||||
val sepaEuroMaximum: FiatMoney? = null
|
||||
) : GatewaySelectorState
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
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
|
||||
@@ -10,47 +9,38 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class GatewaySelectorViewModel(
|
||||
args: GatewaySelectorBottomSheetArgs,
|
||||
repository: GooglePayRepository,
|
||||
private val gatewaySelectorRepository: GatewaySelectorRepository
|
||||
repository: GooglePayRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(
|
||||
GatewaySelectorState(
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
|
||||
inAppPayment = args.inAppPayment,
|
||||
isCreditCardAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.inAppPayment.type),
|
||||
isGooglePayAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.inAppPayment.type),
|
||||
isPayPalAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.PayPal, args.inAppPayment.type),
|
||||
isSEPADebitAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, args.inAppPayment.type),
|
||||
isIDEALAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, args.inAppPayment.type)
|
||||
)
|
||||
)
|
||||
private val store = RxStore<GatewaySelectorState>(GatewaySelectorState.Loading)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state = store.stateFlowable
|
||||
|
||||
init {
|
||||
val inAppPayment = InAppPaymentsRepository.requireInAppPayment(args.inAppPaymentId)
|
||||
val isGooglePayAvailable = repository.isGooglePayAvailable().toSingleDefault(true).onErrorReturnItem(false)
|
||||
val gatewayConfiguration = gatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = args.inAppPayment.data.amount!!.currencyCode)
|
||||
val gatewayConfiguration = inAppPayment.flatMap { GatewaySelectorRepository.getAvailableGatewayConfiguration(currencyCode = it.data.amount!!.currencyCode) }
|
||||
|
||||
disposables += Single.zip(isGooglePayAvailable, gatewayConfiguration, ::Pair).subscribeBy { (googlePayAvailable, gatewayConfiguration) ->
|
||||
disposables += Single.zip(inAppPayment, isGooglePayAvailable, gatewayConfiguration, ::Triple).subscribeBy { (inAppPayment, googlePayAvailable, gatewayConfiguration) ->
|
||||
SignalStore.inAppPayments.isGooglePayReady = googlePayAvailable
|
||||
store.update {
|
||||
it.copy(
|
||||
loading = false,
|
||||
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),
|
||||
GatewaySelectorState.Ready(
|
||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(),
|
||||
inAppPayment = inAppPayment,
|
||||
isCreditCardAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.CARD),
|
||||
isGooglePayAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, inAppPayment.type) && googlePayAvailable && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.GOOGLE_PAY),
|
||||
isPayPalAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.PayPal, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.PAYPAL),
|
||||
isSEPADebitAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.SEPADebit, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.SEPA_DEBIT),
|
||||
isIDEALAvailable = InAppDonations.isDonationsPaymentSourceAvailable(PaymentSourceType.Stripe.IDEAL, inAppPayment.type) && gatewayConfiguration.availableGateways.contains(InAppPaymentData.PaymentMethodType.IDEAL),
|
||||
sepaEuroMaximum = gatewayConfiguration.sepaEuroMaximum
|
||||
)
|
||||
}
|
||||
@@ -63,16 +53,8 @@ class GatewaySelectorViewModel(
|
||||
}
|
||||
|
||||
fun updateInAppPaymentMethod(inAppPaymentMethodType: InAppPaymentData.PaymentMethodType): Single<InAppPaymentTable.InAppPayment> {
|
||||
return gatewaySelectorRepository.setInAppPaymentMethodType(store.state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
val state = store.state as GatewaySelectorState.Ready
|
||||
|
||||
class Factory(
|
||||
private val args: GatewaySelectorBottomSheetArgs,
|
||||
private val repository: GooglePayRepository,
|
||||
private val gatewaySelectorRepository: GatewaySelectorRepository = GatewaySelectorRepository(AppDependencies.donationsService)
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(GatewaySelectorViewModel(args, repository, gatewaySelectorRepository)) as T
|
||||
}
|
||||
return GatewaySelectorRepository.setInAppPaymentMethodType(state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.p
|
||||
import android.content.DialogInterface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
@@ -15,6 +19,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFrag
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet.Companion.presentTitleAndSubtitle
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
/**
|
||||
* Bottom sheet for final order confirmation from PayPal
|
||||
@@ -32,7 +38,13 @@ class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
BadgeDisplay112.register(adapter)
|
||||
PayPalCompleteOrderPaymentItem.register(adapter)
|
||||
|
||||
adapter.submitList(getConfiguration().toMappingModelList())
|
||||
lifecycleScope.launch {
|
||||
val inAppPayment = withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(args.inAppPaymentId)!!
|
||||
}
|
||||
|
||||
adapter.submitList(getConfiguration(inAppPayment).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
@@ -40,18 +52,18 @@ class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to didConfirmOrder))
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
private fun getConfiguration(inAppPayment: InAppPaymentTable.InAppPayment): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = Badges.fromDatabaseBadge(args.inAppPayment.data.badge!!),
|
||||
badge = Badges.fromDatabaseBadge(inAppPayment.data.badge!!),
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), args.inAppPayment)
|
||||
presentTitleAndSubtitle(requireContext(), inAppPayment)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ 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
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorActionResult
|
||||
@@ -32,6 +30,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.In
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
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.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -50,9 +49,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
|
||||
private val args: PayPalPaymentInProgressFragmentArgs by navArgs()
|
||||
|
||||
private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.checkout_flow, factoryProducer = {
|
||||
PayPalPaymentInProgressViewModel.Factory()
|
||||
})
|
||||
private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.checkout_flow)
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = false
|
||||
@@ -67,21 +64,18 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
when (args.action) {
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT -> {
|
||||
viewModel.processNewDonation(
|
||||
args.inAppPayment!!,
|
||||
if (args.inAppPaymentType.recurring) {
|
||||
this::monthlyConfirmationPipeline
|
||||
} else {
|
||||
this::oneTimeConfirmationPipeline
|
||||
}
|
||||
args.inAppPaymentId!!,
|
||||
this::oneTimeConfirmationPipeline,
|
||||
this::monthlyConfirmationPipeline
|
||||
)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
viewModel.updateSubscription(args.inAppPaymentId!!)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription(args.inAppPaymentType.requireSubscriberType())
|
||||
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +98,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -120,8 +113,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -133,11 +125,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
|
||||
private fun getProcessingStatus(): String {
|
||||
return if (args.inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_payment)
|
||||
} else {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
return getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
|
||||
private fun oneTimeConfirmationPipeline(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Completable {
|
||||
@@ -209,7 +197,9 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result.copy(paymentId = createPaymentIntentResponse.paymentId))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(it.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +227,9 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result) {
|
||||
emitter.onSuccess(PayPalPaymentMethodId(createPaymentIntentResponse.token))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(it.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
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.Single
|
||||
import io.reactivex.rxjava3.core.SingleSource
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PayPalPaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.RequiredActionHandler
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.SharedInAppPaymentPipeline
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
|
||||
class PayPalPaymentInProgressViewModel(
|
||||
private val payPalRepository: PayPalRepository
|
||||
) : ViewModel() {
|
||||
class PayPalPaymentInProgressViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(PayPalPaymentInProgressViewModel::class.java)
|
||||
@@ -63,26 +58,36 @@ class PayPalPaymentInProgressViewModel(
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
fun getInAppPaymentType(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Single<InAppPaymentType> {
|
||||
return InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).map { it.type }.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(
|
||||
SingleSource<InAppPaymentTable.InAppPayment> {
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType()).andThen(
|
||||
SingleSource<InAppPaymentTable.InAppPayment> {
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}
|
||||
).flatMap {
|
||||
SharedInAppPaymentPipeline.awaitRedemption(it, PaymentSourceType.PayPal)
|
||||
}
|
||||
).flatMapCompletable {
|
||||
SharedInAppPaymentPipeline.awaitRedemption(it, PaymentSourceType.PayPal)
|
||||
}.subscribeBy(
|
||||
onComplete = {
|
||||
onSuccess = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, PaymentSourceType.PayPal, throwable)
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -106,34 +111,29 @@ class PayPalPaymentInProgressViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, requiredActionHandler: RequiredActionHandler) {
|
||||
Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
check(inAppPayment.data.paymentMethodType.toPaymentSourceType() == PaymentSourceType.PayPal)
|
||||
|
||||
fun processNewDonation(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId,
|
||||
oneTimeActionHandler: RequiredActionHandler,
|
||||
monthlyActionHandler: RequiredActionHandler
|
||||
) {
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
|
||||
disposables += SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPaymentId,
|
||||
PayPalPaymentSource(),
|
||||
requiredActionHandler
|
||||
oneTimeActionHandler,
|
||||
monthlyActionHandler
|
||||
).subscribeOn(Schedulers.io()).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished ${inAppPayment.type} payment pipeline...", true)
|
||||
onSuccess = {
|
||||
Log.d(TAG, "Finished ${it.type} payment pipeline...", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = {
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SharedInAppPaymentPipeline.handleError(it, inAppPayment.id, PaymentSourceType.PayPal, inAppPayment.type.toErrorSource())
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val payPalRepository: PayPalRepository = PayPalRepository(AppDependencies.donationsService)
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
)
|
||||
)
|
||||
|
||||
if (RemoteConfig.internalUser && args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) {
|
||||
if (RemoteConfig.internalUser && args.waitingForAuthPayment.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 {
|
||||
@@ -119,7 +119,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
progress.show(parentFragmentManager, null)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.update(args.inAppPayment)
|
||||
SignalDatabase.inAppPayments.update(args.waitingForAuthPayment)
|
||||
}
|
||||
|
||||
progress.dismissAllowingStateLoss()
|
||||
|
||||
@@ -21,7 +21,6 @@ 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
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -35,6 +34,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.In
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
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.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -67,15 +67,15 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT -> {
|
||||
viewModel.processNewDonation(args.inAppPayment!!, this::handleRequiredAction)
|
||||
viewModel.processNewDonation(args.inAppPaymentId!!, this::handleRequiredAction, this::handleRequiredAction)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.inAppPayment!!)
|
||||
viewModel.updateSubscription(args.inAppPaymentId!!)
|
||||
}
|
||||
|
||||
InAppPaymentProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription(args.inAppPaymentType.requireSubscriberType())
|
||||
viewModel.cancelSubscription(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,8 +98,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
@@ -114,8 +113,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
bundleOf(
|
||||
REQUEST_KEY to InAppPaymentProcessorActionResult(
|
||||
action = args.action,
|
||||
inAppPayment = args.inAppPayment,
|
||||
inAppPaymentType = args.inAppPaymentType,
|
||||
inAppPaymentId = args.inAppPaymentId,
|
||||
status = InAppPaymentProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
@@ -127,11 +125,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
|
||||
private fun getProcessingStatus(): String {
|
||||
return if (args.inAppPaymentType == InAppPaymentType.RECURRING_BACKUP) {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_payment)
|
||||
} else {
|
||||
getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
return getString(R.string.InAppPaymentInProgressFragment__processing_donation)
|
||||
}
|
||||
|
||||
private fun handleRequiredAction(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Completable {
|
||||
@@ -183,11 +177,13 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result)
|
||||
} else {
|
||||
val didLaunchExternal = bundle.getBoolean(Stripe3DSDialogFragment.LAUNCHED_EXTERNAL, false)
|
||||
if (didLaunchExternal) {
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(args.inAppPaymentType.toErrorSource()))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.inAppPaymentType.toErrorSource()))
|
||||
disposables += viewModel.getInAppPaymentType(args.inAppPaymentId!!).subscribeBy { inAppPaymentType ->
|
||||
val didLaunchExternal = bundle.getBoolean(Stripe3DSDialogFragment.LAUNCHED_EXTERNAL, false)
|
||||
if (didLaunchExternal) {
|
||||
emitter.onError(DonationError.UserLaunchedExternalApplication(inAppPaymentType.toErrorSource()))
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(inAppPaymentType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.signal.donations.PaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
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.InAppPaymentProcessorStage
|
||||
@@ -64,29 +64,29 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun processNewDonation(inAppPayment: InAppPaymentTable.InAppPayment, requiredActionHandler: RequiredActionHandler) {
|
||||
Log.d(TAG, "Proceeding with InAppPayment::${inAppPayment.id} of type ${inAppPayment.type}...", true)
|
||||
|
||||
val paymentSourceProvider: PaymentSourceProvider = resolvePaymentSourceProvider(inAppPayment.type.toErrorSource())
|
||||
|
||||
check(inAppPayment.data.paymentMethodType.toPaymentSourceType() == paymentSourceProvider.paymentSourceType)
|
||||
|
||||
fun processNewDonation(inAppPaymentId: InAppPaymentTable.InAppPaymentId, oneTimeRequiredActionHandler: RequiredActionHandler, monthlyRequiredActionHandler: RequiredActionHandler) {
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
|
||||
disposables += paymentSourceProvider.paymentSource.flatMapCompletable { paymentSource ->
|
||||
SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
paymentSource,
|
||||
requiredActionHandler
|
||||
)
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
resolvePaymentSourceProvider(inAppPayment.type.toErrorSource()).paymentSource.flatMap { paymentSource ->
|
||||
SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPaymentId,
|
||||
paymentSource,
|
||||
oneTimeRequiredActionHandler,
|
||||
monthlyRequiredActionHandler
|
||||
)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io()).subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished ${inAppPayment.type} payment pipeline...", true)
|
||||
onSuccess = {
|
||||
Log.d(TAG, "Finished ${it.type} payment pipeline...", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = {
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SharedInAppPaymentPipeline.handleError(it, inAppPayment.id, paymentSourceProvider.paymentSourceType, inAppPayment.type.toErrorSource())
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -137,6 +137,10 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
this.stripePaymentData = StripePaymentData.IDEAL(bankData)
|
||||
}
|
||||
|
||||
fun getInAppPaymentType(inAppPaymentId: InAppPaymentTable.InAppPaymentId): Single<InAppPaymentType> {
|
||||
return InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).map { it.type }.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun requireNoPaymentInformation() {
|
||||
require(stripePaymentData == null)
|
||||
}
|
||||
@@ -162,22 +166,25 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSubscription(inAppPayment: InAppPaymentTable.InAppPayment) {
|
||||
fun updateSubscription(inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
store.update { InAppPaymentProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += RecurringInAppPaymentRepository
|
||||
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(RecurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMapCompletable { paymentSourceType ->
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
val iap = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
disposables += iap.flatMap { inAppPayment ->
|
||||
RecurringInAppPaymentRepository
|
||||
.cancelActiveSubscriptionIfNecessary(inAppPayment.type.requireSubscriberType())
|
||||
.andThen(RecurringInAppPaymentRepository.getPaymentSourceTypeOfLatestSubscription(inAppPayment.type.requireSubscriberType()))
|
||||
.flatMap { paymentSourceType ->
|
||||
val freshPayment = SignalDatabase.inAppPayments.moveToTransacting(inAppPayment.id)!!
|
||||
|
||||
Single.fromCallable {
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}.flatMapCompletable { SharedInAppPaymentPipeline.awaitRedemption(it, paymentSourceType) }
|
||||
}
|
||||
Single.fromCallable {
|
||||
RecurringInAppPaymentRepository.setSubscriptionLevelSync(freshPayment)
|
||||
}.flatMap { SharedInAppPaymentPipeline.awaitRedemption(it, paymentSourceType) }
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
onSuccess = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { InAppPaymentProcessorStage.COMPLETE }
|
||||
},
|
||||
@@ -185,8 +192,7 @@ class StripePaymentInProgressViewModel : ViewModel() {
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
store.update { InAppPaymentProcessorStage.FAILED }
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
val paymentSourceType = InAppPaymentsRepository.getLatestPaymentMethodType(inAppPayment.type.requireSubscriberType()).toPaymentSourceType()
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPayment.id, DonationErrorSource.MONTHLY, paymentSourceType, throwable)
|
||||
InAppPaymentsRepository.handlePipelineError(inAppPaymentId, throwable)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -42,9 +42,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
@@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Collects SEPA Debit bank transfer details from the user to proceed with donation.
|
||||
@@ -75,7 +76,10 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDelegate.ErrorHandlerCallback {
|
||||
|
||||
private val args: BankTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModels()
|
||||
|
||||
private val viewModel: BankTransferDetailsViewModel by viewModel {
|
||||
BankTransferDetailsViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.checkout_flow
|
||||
@@ -84,7 +88,7 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPayment.id)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPaymentId)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
@@ -98,17 +102,33 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state: BankTransferDetailsState by viewModel.state
|
||||
val inAppPayment by viewModel.inAppPayment.collectAsStateWithLifecycle(null)
|
||||
|
||||
val donateLabel = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-requests] backups copy
|
||||
if (inAppPayment != null) {
|
||||
ReadyContent(
|
||||
state,
|
||||
viewModel,
|
||||
inAppPayment!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReadyContent(
|
||||
state: BankTransferDetailsState,
|
||||
viewModel: BankTransferDetailsViewModel,
|
||||
inAppPayment: InAppPaymentTable.InAppPayment
|
||||
) {
|
||||
val donateLabel = remember(inAppPayment) {
|
||||
if (inAppPayment.type.recurring) { // TODO [message-requests] backups copy
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney())
|
||||
FiatMoneyUtil.format(resources, inAppPayment.data.amount!!.toFiatMoney())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -142,8 +162,7 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
||||
findNavController().safeNavigate(
|
||||
BankTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
args.inAppPaymentId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,15 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.t
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.details.BankTransferDetailsState.FocusState
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
class BankTransferDetailsViewModel : ViewModel() {
|
||||
class BankTransferDetailsViewModel(
|
||||
inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private const val IBAN_MAX_CHARACTER_COUNT = 34
|
||||
@@ -19,6 +25,8 @@ class BankTransferDetailsViewModel : ViewModel() {
|
||||
private val internalState = mutableStateOf(BankTransferDetailsState())
|
||||
val state: State<BankTransferDetailsState> = internalState
|
||||
|
||||
val inAppPayment: Flow<InAppPaymentTable.InAppPayment> = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).toFlowable().asFlow()
|
||||
|
||||
fun setDisplayFindAccountInfoSheet(displayFindAccountInfoSheet: Boolean) {
|
||||
internalState.value = internalState.value.copy(
|
||||
displayFindAccountInfoSheet = displayFindAccountInfoSheet
|
||||
|
||||
@@ -46,6 +46,7 @@ import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
@@ -78,7 +79,7 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
||||
|
||||
private val args: IdealTransferDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: IdealTransferDetailsViewModel by viewModel {
|
||||
IdealTransferDetailsViewModel(args.inAppPayment.type.recurring)
|
||||
IdealTransferDetailsViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
@@ -88,7 +89,7 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
|
||||
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPayment.id)
|
||||
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPaymentId)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
|
||||
@@ -106,24 +107,29 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.state
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val donateLabel = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
val iap = remember(state.inAppPayment) { state.inAppPayment }
|
||||
if (iap == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val donateLabel = remember(iap) {
|
||||
if (iap.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s_month,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
FiatMoneyUtil.format(resources, iap.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
)
|
||||
} else {
|
||||
getString(
|
||||
R.string.BankTransferDetailsFragment__donate_s,
|
||||
FiatMoneyUtil.format(resources, args.inAppPayment.data.amount!!.toFiatMoney())
|
||||
FiatMoneyUtil.format(resources, iap.data.amount!!.toFiatMoney())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val idealDirections = remember(args.inAppPayment) {
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
val idealDirections = remember(iap) {
|
||||
if (iap.type.recurring) { // TODO [message-request] -- Handle backups
|
||||
R.string.IdealTransferDetailsFragment__enter_your_bank
|
||||
} else {
|
||||
R.string.IdealTransferDetailsFragment__enter_your_bank_details_one_time
|
||||
@@ -152,14 +158,13 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
||||
findNavController().safeNavigate(
|
||||
IdealTransferDetailsFragmentDirections.actionBankTransferDetailsFragmentToStripePaymentInProgressFragment(
|
||||
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
|
||||
args.inAppPayment,
|
||||
args.inAppPayment.type
|
||||
args.inAppPaymentId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (args.inAppPayment.type.recurring) { // TODO [message-requests] -- handle backup
|
||||
val formattedMoney = FiatMoneyUtil.format(requireContext().resources, args.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
if (state.inAppPayment!!.type.recurring) { // TODO [message-requests] -- handle backup
|
||||
val formattedMoney = FiatMoneyUtil.format(requireContext().resources, state.inAppPayment.data.amount!!.toFiatMoney(), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.IdealTransferDetailsFragment__confirm_your_donation_with_s, getString(state.idealBank!!.getUIValues().name)))
|
||||
.setMessage(getString(R.string.IdealTransferDetailsFragment__to_setup_your_recurring_donation, formattedMoney))
|
||||
@@ -199,7 +204,7 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
||||
@Composable
|
||||
private fun IdealTransferDetailsContentPreview() {
|
||||
IdealTransferDetailsContent(
|
||||
state = IdealTransferDetailsState(isMonthly = true),
|
||||
state = IdealTransferDetailsState(),
|
||||
idealDirections = R.string.IdealTransferDetailsFragment__enter_your_bank,
|
||||
donateLabel = "Donate $5/month",
|
||||
onNavigationClick = {},
|
||||
@@ -294,7 +299,7 @@ private fun IdealTransferDetailsContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (state.isMonthly) {
|
||||
if (state.inAppPayment!!.type.recurring) {
|
||||
item {
|
||||
TextField(
|
||||
value = state.email,
|
||||
|
||||
@@ -7,9 +7,10 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.t
|
||||
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankDetailsValidator
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
|
||||
data class IdealTransferDetailsState(
|
||||
val isMonthly: Boolean,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||
val idealBank: IdealBank? = null,
|
||||
val name: String = "",
|
||||
val nameFocusState: FocusState = FocusState.NOT_FOCUSED,
|
||||
@@ -34,7 +35,7 @@ data class IdealTransferDetailsState(
|
||||
}
|
||||
|
||||
fun canProceed(): Boolean {
|
||||
return idealBank != null && BankDetailsValidator.validName(name) && (!isMonthly || BankDetailsValidator.validEmail(email))
|
||||
return idealBank != null && BankDetailsValidator.validName(name) && (inAppPayment?.type?.recurring != true || BankDetailsValidator.validEmail(email))
|
||||
}
|
||||
|
||||
enum class FocusState {
|
||||
|
||||
@@ -5,42 +5,67 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.ideal
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
||||
class IdealTransferDetailsViewModel(isMonthly: Boolean) : ViewModel() {
|
||||
class IdealTransferDetailsViewModel(inAppPaymentId: InAppPaymentTable.InAppPaymentId) : ViewModel() {
|
||||
|
||||
private val internalState = mutableStateOf(IdealTransferDetailsState(isMonthly = isMonthly))
|
||||
var state: State<IdealTransferDetailsState> = internalState
|
||||
private val internalState = MutableStateFlow(IdealTransferDetailsState())
|
||||
var state: StateFlow<IdealTransferDetailsState> = internalState
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val inAppPayment = withContext(Dispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
}
|
||||
|
||||
internalState.update {
|
||||
it.copy(inAppPayment = inAppPayment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onNameChanged(name: String) {
|
||||
internalState.value = internalState.value.copy(
|
||||
name = name
|
||||
)
|
||||
internalState.update {
|
||||
it.copy(name = name)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEmailChanged(email: String) {
|
||||
internalState.value = internalState.value.copy(
|
||||
email = email
|
||||
)
|
||||
internalState.update {
|
||||
it.copy(email = email)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFocusChanged(field: Field, isFocused: Boolean) {
|
||||
when (field) {
|
||||
Field.NAME -> {
|
||||
if (isFocused && internalState.value.nameFocusState == IdealTransferDetailsState.FocusState.NOT_FOCUSED) {
|
||||
internalState.value = internalState.value.copy(nameFocusState = IdealTransferDetailsState.FocusState.FOCUSED)
|
||||
} else if (!isFocused && internalState.value.nameFocusState == IdealTransferDetailsState.FocusState.FOCUSED) {
|
||||
internalState.value = internalState.value.copy(nameFocusState = IdealTransferDetailsState.FocusState.LOST_FOCUS)
|
||||
internalState.update { state ->
|
||||
when (field) {
|
||||
Field.NAME -> {
|
||||
if (isFocused && state.nameFocusState == IdealTransferDetailsState.FocusState.NOT_FOCUSED) {
|
||||
state.copy(nameFocusState = IdealTransferDetailsState.FocusState.FOCUSED)
|
||||
} else if (!isFocused && state.nameFocusState == IdealTransferDetailsState.FocusState.FOCUSED) {
|
||||
state.copy(nameFocusState = IdealTransferDetailsState.FocusState.LOST_FOCUS)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Field.EMAIL -> {
|
||||
if (isFocused && internalState.value.emailFocusState == IdealTransferDetailsState.FocusState.NOT_FOCUSED) {
|
||||
internalState.value = internalState.value.copy(emailFocusState = IdealTransferDetailsState.FocusState.FOCUSED)
|
||||
} else if (!isFocused && internalState.value.emailFocusState == IdealTransferDetailsState.FocusState.FOCUSED) {
|
||||
internalState.value = internalState.value.copy(emailFocusState = IdealTransferDetailsState.FocusState.LOST_FOCUS)
|
||||
Field.EMAIL -> {
|
||||
if (isFocused && state.emailFocusState == IdealTransferDetailsState.FocusState.NOT_FOCUSED) {
|
||||
state.copy(emailFocusState = IdealTransferDetailsState.FocusState.FOCUSED)
|
||||
} else if (!isFocused && state.emailFocusState == IdealTransferDetailsState.FocusState.FOCUSED) {
|
||||
state.copy(emailFocusState = IdealTransferDetailsState.FocusState.LOST_FOCUS)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -56,7 +57,6 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Dividers
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorAnimator
|
||||
@@ -72,7 +72,7 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
|
||||
private val args: BankTransferMandateFragmentArgs by navArgs()
|
||||
private val viewModel: BankTransferMandateViewModel by viewModel {
|
||||
BankTransferMandateViewModel(PaymentSourceType.Stripe.SEPADebit)
|
||||
BankTransferMandateViewModel(args.inAppPaymentId)
|
||||
}
|
||||
|
||||
private lateinit var statusBarColorAnimator: StatusBarColorAnimator
|
||||
@@ -112,14 +112,16 @@ class BankTransferMandateFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
if (args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.SEPA_DEBIT) {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.inAppPayment)
|
||||
)
|
||||
} else {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToIdealTransferDetailsFragment(args.inAppPayment)
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
if (viewModel.getPaymentMethodType() == InAppPaymentData.PaymentMethodType.SEPA_DEBIT) {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToBankTransferDetailsFragment(args.inAppPaymentId)
|
||||
)
|
||||
} else {
|
||||
findNavController().safeNavigate(
|
||||
BankTransferMandateFragmentDirections.actionBankTransferMandateFragmentToIdealTransferDetailsFragment(args.inAppPaymentId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import java.util.Locale
|
||||
|
||||
class BankTransferMandateRepository {
|
||||
object BankTransferMandateRepository {
|
||||
|
||||
fun getMandate(paymentSourceType: PaymentSourceType.Stripe): Single<String> {
|
||||
return Single
|
||||
|
||||
@@ -12,13 +12,25 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
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.transfer.details.BankTransferDetailsViewModel
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
|
||||
class BankTransferMandateViewModel(
|
||||
paymentSourceType: PaymentSourceType,
|
||||
repository: BankTransferMandateRepository = BankTransferMandateRepository()
|
||||
private val inAppPaymentId: InAppPaymentTable.InAppPaymentId
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BankTransferDetailsViewModel::class)
|
||||
}
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val internalMandate = mutableStateOf("")
|
||||
private val internalFailedToLoadMandate = mutableStateOf(false)
|
||||
@@ -27,14 +39,28 @@ class BankTransferMandateViewModel(
|
||||
val failedToLoadMandate: State<Boolean> = internalFailedToLoadMandate
|
||||
|
||||
init {
|
||||
disposables += repository.getMandate(paymentSourceType as PaymentSourceType.Stripe)
|
||||
val inAppPayment = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId)
|
||||
|
||||
disposables += inAppPayment
|
||||
.flatMap {
|
||||
BankTransferMandateRepository.getMandate(it.data.paymentMethodType.toPaymentSourceType() as PaymentSourceType.Stripe)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(
|
||||
onSuccess = { internalMandate.value = it },
|
||||
onError = { internalFailedToLoadMandate.value = true }
|
||||
onError = {
|
||||
Log.w(TAG, "Failed to load mandate.", it)
|
||||
internalFailedToLoadMandate.value = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getPaymentMethodType(): InAppPaymentData.PaymentMethodType {
|
||||
return withContext(SignalDispatchers.IO) {
|
||||
SignalDatabase.inAppPayments.getById(inAppPaymentId)!!.data.paymentMethodType
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
@@ -23,12 +23,9 @@
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="in_app_payment_type"
|
||||
app:argType="org.signal.donations.InAppPaymentType" />
|
||||
|
||||
<argument
|
||||
android:name="is_long_running"
|
||||
@@ -46,8 +43,8 @@
|
||||
tools:layout="@layout/credit_card_fragment">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
|
||||
<action
|
||||
@@ -75,7 +72,7 @@
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
android:name="waiting_for_auth_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
app:nullable="false" />
|
||||
</dialog>
|
||||
@@ -98,12 +95,10 @@
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="true" />
|
||||
<argument
|
||||
android:name="in_app_payment_type"
|
||||
app:argType="org.signal.donations.InAppPaymentType" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_paypalPaymentInProgressFragment_to_paypalConfirmationFragment"
|
||||
app:destination="@id/paypalConfirmationFragment" />
|
||||
@@ -132,8 +127,8 @@
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
</dialog>
|
||||
|
||||
@@ -143,8 +138,8 @@
|
||||
android:label="bank_transfer_mandate_fragment">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
<action
|
||||
android:id="@+id/action_bankTransferMandateFragment_to_bankTransferDetailsFragment"
|
||||
@@ -163,8 +158,8 @@
|
||||
android:label="bank_transfer_details_fragment">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
<action
|
||||
android:id="@+id/action_bankTransferDetailsFragment_to_stripePaymentInProgressFragment"
|
||||
@@ -181,8 +176,8 @@
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
</dialog>
|
||||
|
||||
@@ -192,8 +187,8 @@
|
||||
android:label="ideal_transfer_details_fragment">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
<action
|
||||
android:id="@+id/action_bankTransferDetailsFragment_to_stripePaymentInProgressFragment"
|
||||
@@ -270,8 +265,8 @@
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet">
|
||||
|
||||
<argument
|
||||
android:name="in_app_payment"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPayment"
|
||||
android:name="in_app_payment_id"
|
||||
app:argType="org.thoughtcrime.securesms.database.InAppPaymentTable$InAppPaymentId"
|
||||
app:nullable="false" />
|
||||
|
||||
</dialog>
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsTestRule
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentPayPalOneTimeSetupJob
|
||||
@@ -63,9 +64,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -94,9 +98,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -120,9 +127,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -152,9 +162,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -175,9 +188,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -207,9 +223,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -236,9 +255,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -267,9 +289,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -298,9 +323,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -338,9 +366,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
@@ -373,9 +404,12 @@ class SharedInAppPaymentPipelineTest {
|
||||
Completable.complete()
|
||||
}
|
||||
|
||||
every { SignalDatabase.inAppPayments.getById(inAppPayment.id) } returns inAppPayment
|
||||
|
||||
val test = SharedInAppPaymentPipeline.awaitTransaction(
|
||||
inAppPayment,
|
||||
inAppPayment.id,
|
||||
paymentSource,
|
||||
requiredActionHandler,
|
||||
requiredActionHandler
|
||||
).test()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user