Pass InAppPayments around by ID instead of passing the entire object.

This commit is contained in:
Alex Hart
2025-04-08 10:42:58 -03:00
committed by Michelle Tang
parent 1aed82d5b7
commit c9795141df
32 changed files with 594 additions and 417 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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