diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 0a3b52a5a1..f3a92d4754 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt index ec4292d250..4def578a30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/gifts/flow/GiftFlowConfirmationFragment.kt @@ -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) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheet.kt index 6a99b20eee..89765df5b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheet.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheetViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheetViewModel.kt new file mode 100644 index 0000000000..aa85c8154c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPendingBottomSheetViewModel.kt @@ -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(null) + val inAppPayment: StateFlow = internalInAppPayment + + init { + viewModelScope.launch { + val inAppPayment = withContext(SignalDispatchers.IO) { + SignalDatabase.inAppPayments.getById(inAppPaymentId)!! + } + + internalInAppPayment.update { inAppPayment } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt index 84a9632ef9..dab6d69a4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt index 863cc0aa91..28e0edfcc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/completed/InAppPaymentsBottomSheetDelegate.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt index f61187b67f..bb82708d8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/DonateToSignalFragment.kt @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentCheckoutDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentCheckoutDelegate.kt index e2940d79a1..558e7f0fca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentCheckoutDelegate.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentCheckoutDelegate.kt @@ -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) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentProcessorActionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentProcessorActionResult.kt index 1033041fcd..31249fc2cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentProcessorActionResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/InAppPaymentProcessorActionResult.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipeline.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipeline.kt index 53843414f5..90f170e94a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipeline.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipeline.kt @@ -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 { + 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 { 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)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt index ba2409de48..dfa413133c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardFragment.kt @@ -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 ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardViewModel.kt index 16db84b6b9..5a275dcbcb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/card/CreditCardViewModel.kt @@ -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 = BehaviorProcessor.create() private val currentYear: Int private val currentMonth: Int + private val internalInAppPayment = MutableStateFlow(null) + val inAppPayment: Flow = 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt index 870cb8290d..68d946a399 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorBottomSheet.kt @@ -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().googlePayRepository) - }) + private val viewModel: GatewaySelectorViewModel by viewModel { + GatewaySelectorViewModel(args, requireListener().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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorRepository.kt index ba2022cbd3..24363a033b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorRepository.kt @@ -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 { return Single.fromCallable { - donationsService.getDonationsConfiguration(Locale.getDefault()) + AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault()) }.flatMap { it.flattenResult() } .map { configuration -> val available = configuration.getAvailablePaymentMethods(currencyCode).map { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorState.kt index c46b983b1f..c341c34686 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorState.kt @@ -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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt index d0815514f4..ea43e7d5f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/gateway/GatewaySelectorViewModel.kt @@ -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.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 { - 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 create(modelClass: Class): T { - return modelClass.cast(GatewaySelectorViewModel(args, repository, gatewaySelectorRepository)) as T - } + return GatewaySelectorRepository.setInAppPaymentMethodType(state.inAppPayment, inAppPaymentMethodType).observeOn(AndroidSchedulers.mainThread()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalCompleteOrderBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalCompleteOrderBottomSheet.kt index 26c6bc6f9a..ce6a4e3445 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalCompleteOrderBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalCompleteOrderBottomSheet.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt index d300cab5ef..b92314976e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressFragment.kt @@ -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())) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt index 94359f2119..e8c98a17b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/paypal/PayPalPaymentInProgressViewModel.kt @@ -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 { + 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 { - 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 { + 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 create(modelClass: Class): T { - return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository)) as T - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt index ac936ff398..89a3a194d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt index 21fa7a328d..52af2c7556 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressFragment.kt @@ -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())) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt index 04d7f52515..0590013ae7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/StripePaymentInProgressViewModel.kt @@ -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 { + 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) } } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt index 14e21920b2..6869698f3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsFragment.kt @@ -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 ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsViewModel.kt index 604b80699d..51d5173978 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/details/BankTransferDetailsViewModel.kt @@ -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 = internalState + val inAppPayment: Flow = InAppPaymentsRepository.requireInAppPayment(inAppPaymentId).toFlowable().asFlow() + fun setDisplayFindAccountInfoSheet(displayFindAccountInfoSheet: Boolean) { internalState.value = internalState.value.copy( displayFindAccountInfoSheet = displayFindAccountInfoSheet diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt index 947ebc5590..86b01938e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsFragment.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsState.kt index 627b6bfc30..4df72aca1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsState.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsViewModel.kt index 18a0592855..c13be6354c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/ideal/IdealTransferDetailsViewModel.kt @@ -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 = internalState + private val internalState = MutableStateFlow(IdealTransferDetailsState()) + var state: StateFlow = 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 + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt index 954377e63c..3125b24a70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateFragment.kt @@ -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) + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateRepository.kt index e3100be824..c3b2d2cf32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateRepository.kt @@ -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 { return Single diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateViewModel.kt index 2d9e5efe57..8c575637c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/transfer/mandate/BankTransferMandateViewModel.kt @@ -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 = 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() } diff --git a/app/src/main/res/navigation/checkout.xml b/app/src/main/res/navigation/checkout.xml index 2735b72b4c..581e403bfc 100644 --- a/app/src/main/res/navigation/checkout.xml +++ b/app/src/main/res/navigation/checkout.xml @@ -23,12 +23,9 @@ app:nullable="false" /> - @@ -98,12 +95,10 @@ app:nullable="false" /> - + @@ -132,8 +127,8 @@ tools:layout="@layout/dsl_settings_bottom_sheet"> @@ -143,8 +138,8 @@ android:label="bank_transfer_mandate_fragment"> @@ -192,8 +187,8 @@ android:label="ideal_transfer_details_fragment"> diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipelineTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipelineTest.kt index 2a09d31140..6104dda7b5 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipelineTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/SharedInAppPaymentPipelineTest.kt @@ -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()