From 81d99c9d30baeaad77a9af6191f046cb64253086 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 25 Sep 2024 16:14:41 -0300 Subject: [PATCH] Implement happy path for backups subscriptions. --- .../ConfirmBackupCancellationDialog.kt | 104 ------------------ .../MessageBackupsFlowFragment.kt | 26 ++++- .../MessageBackupsFlowViewModel.kt | 103 ++++++++++------- .../v2/ui/subscription/MessageBackupsStage.kt | 2 +- .../MessageBackupsTypeSelectionScreen.kt | 11 ++ .../subscription/InAppPaymentsRepository.kt | 3 +- .../errors/DonationErrorSource.kt | 5 + .../jobs/InAppPaymentPurchaseTokenJob.kt | 5 + .../jobs/InAppPaymentRecurringContextJob.kt | 17 ++- .../jobs/InAppPaymentRedemptionJob.kt | 7 ++ .../java/org/signal/billing/BillingApiImpl.kt | 2 + .../internal/push/PushServiceSocket.java | 2 +- 12 files changed, 132 insertions(+), 155 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/ConfirmBackupCancellationDialog.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/ConfirmBackupCancellationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/ConfirmBackupCancellationDialog.kt deleted file mode 100644 index 3363b35a09..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/ConfirmBackupCancellationDialog.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.backup.v2.ui.subscription - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import org.signal.core.ui.Previews -import org.signal.core.ui.SignalPreview -import org.thoughtcrime.securesms.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ConfirmBackupCancellationDialog( - onConfirmAndDownloadNow: () -> Unit, - onConfirmAndDownloadLater: () -> Unit, - onKeepSubscriptionClick: () -> Unit -) { - BasicAlertDialog(onDismissRequest = onKeepSubscriptionClick) { - Surface( - shape = AlertDialogDefaults.shape, - color = AlertDialogDefaults.containerColor - ) { - Column { - Text( - text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_cancellation), - color = AlertDialogDefaults.titleContentColor, - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier - .padding(top = 24.dp) - .padding(horizontal = 24.dp) - ) - - Text( - text = stringResource(id = R.string.ConfirmBackupCancellationDialog__you_wont_be_charged_again), - color = AlertDialogDefaults.textContentColor, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier - .padding(top = 16.dp) - .padding(horizontal = 24.dp) - ) - - TextButton( - onClick = onConfirmAndDownloadNow, - modifier = Modifier - .align(Alignment.End) - .padding(end = 12.dp) - ) { - Text( - text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_and_download_now) - ) - } - - TextButton( - onClick = onConfirmAndDownloadLater, - modifier = Modifier - .align(Alignment.End) - .padding(end = 12.dp) - ) { - Text( - text = stringResource(id = R.string.ConfirmBackupCancellationDialog__confirm_and_download_later) - ) - } - - TextButton( - onClick = onKeepSubscriptionClick, - modifier = Modifier - .align(Alignment.End) - .padding(end = 12.dp, bottom = 12.dp) - ) { - Text( - text = stringResource(id = R.string.ConfirmBackupCancellationDialog__keep_subscription) - ) - } - } - } - } -} - -@SignalPreview -@Composable -private fun ConfirmCancellationDialogPreview() { - Previews.Preview { - ConfirmBackupCancellationDialog( - onKeepSubscriptionClick = {}, - onConfirmAndDownloadNow = {}, - onConfirmAndDownloadLater = {} - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index 9d68e3de52..ddabf9e879 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -6,6 +6,8 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription import android.app.Activity +import android.os.Bundle +import android.view.View import androidx.activity.OnBackPressedCallback import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -15,10 +17,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.rx3.asFlowable import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.Nav +import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.viewModel @@ -26,9 +31,20 @@ import org.thoughtcrime.securesms.util.viewModel /** * Handles the selection, payment, and changing of a user's backup tier. */ -class MessageBackupsFlowFragment : ComposeFragment() { +class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelegate.ErrorHandlerCallback { private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() } + private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + errorHandler.attach( + fragment = this, + errorHandlerCallback = this, + inAppPaymentIdSource = viewModel.stateFlow.asFlowable() + .filter { it.inAppPayment != null } + .map { it.inAppPayment!!.id } + ) + } @Composable override fun FragmentContent() { @@ -83,6 +99,7 @@ class MessageBackupsFlowFragment : ComposeFragment() { composable(route = MessageBackupsStage.Route.TYPE_SELECTION.name) { MessageBackupsTypeSelectionScreen( + stage = state.stage, currentBackupTier = state.currentMessageBackupTier, selectedBackupTier = state.selectedMessageBackupTier, availableBackupTypes = state.availableBackupTypes.filter { it.tier == MessageBackupTier.FREE || state.hasBackupSubscriberAvailable }, @@ -123,4 +140,11 @@ class MessageBackupsFlowFragment : ComposeFragment() { } } } + + override fun onUserLaunchedAnExternalApplication() = error("Not supported by this fragment.") + + override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Not supported by this fragment.") + override fun exitCheckoutFlow() { + requireActivity().finishAfterTransition() + } } 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 53e6da0a50..69bb79b72c 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 @@ -9,8 +9,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.take @@ -22,12 +24,15 @@ import kotlinx.coroutines.withContext import org.signal.core.util.billing.BillingPurchaseResult 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 import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentError +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError +import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord @@ -84,13 +89,12 @@ class MessageBackupsFlowViewModel : ViewModel() { AppDependencies.billingApi.getBillingPurchaseResults().collect { result -> when (result) { is BillingPurchaseResult.Success -> { - internalStateFlow.update { it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT) } + Log.d(TAG, "Got successful purchase result for purchase at ${result.purchaseTime}") + val id = internalStateFlow.value.inAppPayment!!.id try { - handleSuccess( - result, - internalStateFlow.value.inAppPayment!!.id - ) + Log.d(TAG, "Attempting to handle successful purchase.") + handleSuccess(result, id) internalStateFlow.update { it.copy( @@ -98,6 +102,14 @@ class MessageBackupsFlowViewModel : ViewModel() { ) } } catch (e: Exception) { + Log.d(TAG, "Failed to handle purchase.", e) + InAppPaymentsRepository.handlePipelineError( + inAppPaymentId = id, + donationErrorSource = DonationErrorSource.BACKUPS, + paymentSourceType = PaymentSourceType.GooglePlayBilling, + error = e + ) + internalStateFlow.update { it.copy( stage = MessageBackupsStage.FAILURE, @@ -123,7 +135,7 @@ class MessageBackupsFlowViewModel : ViewModel() { MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD) MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION) MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it) - MessageBackupsStage.CHECKOUT_SHEET -> validateGatewayAndUpdateState(it) + MessageBackupsStage.CHECKOUT_SHEET -> it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT) MessageBackupsStage.CREATING_IN_APP_PAYMENT -> error("This is driven by an async coroutine.") MessageBackupsStage.PROCESS_PAYMENT -> error("This is driven by an async coroutine.") MessageBackupsStage.PROCESS_FREE -> error("This is driven by an async coroutine.") @@ -174,49 +186,44 @@ class MessageBackupsFlowViewModel : ViewModel() { state.copy(stage = MessageBackupsStage.COMPLETED) } - MessageBackupTier.PAID -> state.copy(stage = MessageBackupsStage.CHECKOUT_SHEET) - } - } + MessageBackupTier.PAID -> { + check(state.selectedMessageBackupTier == MessageBackupTier.PAID) + check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier }) + check(state.hasBackupSubscriberAvailable) - private fun validateGatewayAndUpdateState(state: MessageBackupsFlowState): MessageBackupsFlowState { - check(state.selectedMessageBackupTier == MessageBackupTier.PAID) - check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier }) - check(state.hasBackupSubscriberAvailable) + viewModelScope.launch(Dispatchers.IO) { + internalStateFlow.update { it.copy(inAppPayment = null) } - viewModelScope.launch(Dispatchers.IO) { - withContext(Dispatchers.Main) { - internalStateFlow.update { it.copy(inAppPayment = null) } - } + val paidFiat = AppDependencies.billingApi.queryProduct()!!.price - val paidFiat = AppDependencies.billingApi.queryProduct()!!.price - - SignalDatabase.inAppPayments.clearCreated() - val id = SignalDatabase.inAppPayments.insert( - type = InAppPaymentType.RECURRING_BACKUP, - state = InAppPaymentTable.State.CREATED, - subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId, - endOfPeriod = null, - inAppPaymentData = InAppPaymentData( - badge = null, - label = state.selectedMessageBackupTierLabel!!, - amount = paidFiat.toFiatValue(), - level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(), - recipientId = Recipient.self().id.serialize(), - paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING, - redemption = InAppPaymentData.RedemptionState( - stage = InAppPaymentData.RedemptionState.Stage.INIT + SignalDatabase.inAppPayments.clearCreated() + val id = SignalDatabase.inAppPayments.insert( + type = InAppPaymentType.RECURRING_BACKUP, + state = InAppPaymentTable.State.CREATED, + subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId, + endOfPeriod = null, + inAppPaymentData = InAppPaymentData( + badge = null, + label = state.selectedMessageBackupTierLabel!!, + amount = paidFiat.toFiatValue(), + level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(), + recipientId = Recipient.self().id.serialize(), + paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING, + redemption = InAppPaymentData.RedemptionState( + stage = InAppPaymentData.RedemptionState.Stage.INIT + ) + ) ) - ) - ) - val inAppPayment = SignalDatabase.inAppPayments.getById(id)!! + val inAppPayment = SignalDatabase.inAppPayments.getById(id)!! + internalStateFlow.update { + it.copy(inAppPayment = inAppPayment, stage = MessageBackupsStage.CHECKOUT_SHEET) + } + } - withContext(Dispatchers.Main) { - internalStateFlow.update { it.copy(inAppPayment = inAppPayment, stage = MessageBackupsStage.PROCESS_PAYMENT) } + state.copy(stage = MessageBackupsStage.CREATING_IN_APP_PAYMENT) } } - - return state.copy(stage = MessageBackupsStage.CREATING_IN_APP_PAYMENT) } /** @@ -237,6 +244,8 @@ class MessageBackupsFlowViewModel : ViewModel() { @OptIn(FlowPreview::class) private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) { withContext(Dispatchers.IO) { + Log.d(TAG, "Setting purchase token data on InAppPayment.") + val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!! SignalDatabase.inAppPayments.update( inAppPayment.copy( @@ -248,20 +257,30 @@ class MessageBackupsFlowViewModel : ViewModel() { ) ) + Log.d(TAG, "Enqueueing InAppPaymentPurchaseTokenJob chain.") InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue() } val terminalInAppPayment = withContext(Dispatchers.IO) { + Log.d(TAG, "Awaiting completion of job chain for up to 10 seconds.") InAppPaymentsRepository.observeUpdates(inAppPaymentId).asFlow() .filter { it.state == InAppPaymentTable.State.END } .take(1) .timeout(10.seconds) + .catch { exception -> + if (exception is TimeoutCancellationException) { + throw DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError(DonationErrorSource.BACKUPS) + } + } .first() } if (terminalInAppPayment.data.error != null) { - throw InAppPaymentError(terminalInAppPayment.data.error) + val err = InAppPaymentError(terminalInAppPayment.data.error) + Log.d(TAG, "An error occurred during the job chain!", err) + throw err } else { + Log.d(TAG, "Job chain completed successfully.") return } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt index ec9d00b809..817ed42505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt @@ -15,8 +15,8 @@ enum class MessageBackupsStage( BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION), BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD), TYPE_SELECTION(route = Route.TYPE_SELECTION), - CHECKOUT_SHEET(route = Route.TYPE_SELECTION), CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION), + CHECKOUT_SHEET(route = Route.TYPE_SELECTION), PROCESS_PAYMENT(route = Route.TYPE_SELECTION), PROCESS_FREE(route = Route.TYPE_SELECTION), COMPLETED(route = Route.TYPE_SELECTION), diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt index da73ced2d1..890ede5e27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.persistentListOf import org.signal.core.ui.Buttons +import org.signal.core.ui.Dialogs import org.signal.core.ui.Previews import org.signal.core.ui.Scaffolds import org.signal.core.ui.SignalPreview @@ -64,6 +65,7 @@ import java.util.Currency @OptIn(ExperimentalTextApi::class) @Composable fun MessageBackupsTypeSelectionScreen( + stage: MessageBackupsStage, currentBackupTier: MessageBackupTier?, selectedBackupTier: MessageBackupTier?, availableBackupTypes: List, @@ -168,6 +170,13 @@ fun MessageBackupsTypeSelectionScreen( ) ) } + + when (stage) { + MessageBackupsStage.CREATING_IN_APP_PAYMENT -> Dialogs.IndeterminateProgressDialog() + MessageBackupsStage.PROCESS_PAYMENT -> Dialogs.IndeterminateProgressDialog() + MessageBackupsStage.PROCESS_FREE -> Dialogs.IndeterminateProgressDialog() + else -> Unit + } } } } @@ -179,6 +188,7 @@ private fun MessageBackupsTypeSelectionScreenPreview() { Previews.Preview { MessageBackupsTypeSelectionScreen( + stage = MessageBackupsStage.TYPE_SELECTION, selectedBackupTier = MessageBackupTier.FREE, availableBackupTypes = testBackupTypes(), onMessageBackupsTierSelected = { selectedBackupsType = it }, @@ -197,6 +207,7 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() { Previews.Preview { MessageBackupsTypeSelectionScreen( + stage = MessageBackupsStage.TYPE_SELECTION, selectedBackupTier = MessageBackupTier.FREE, availableBackupTypes = testBackupTypes(), onMessageBackupsTierSelected = { selectedBackupsType = it }, 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 67c23c08bc..1b68eef28b 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 @@ -224,6 +224,7 @@ object InAppPaymentsRepository { DonationErrorSource.ONE_TIME -> InAppPaymentType.ONE_TIME_DONATION DonationErrorSource.MONTHLY -> InAppPaymentType.RECURRING_DONATION DonationErrorSource.GIFT -> InAppPaymentType.ONE_TIME_GIFT + DonationErrorSource.BACKUPS -> InAppPaymentType.RECURRING_BACKUP DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentType.UNKNOWN DonationErrorSource.KEEP_ALIVE -> InAppPaymentType.UNKNOWN DonationErrorSource.UNKNOWN -> InAppPaymentType.UNKNOWN @@ -266,7 +267,7 @@ object InAppPaymentsRepository { InAppPaymentType.ONE_TIME_GIFT -> DonationErrorSource.GIFT InAppPaymentType.ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME InAppPaymentType.RECURRING_DONATION -> DonationErrorSource.MONTHLY - InAppPaymentType.RECURRING_BACKUP -> DonationErrorSource.UNKNOWN // TODO [message-backups] error handling + InAppPaymentType.RECURRING_BACKUP -> DonationErrorSource.BACKUPS } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorSource.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorSource.kt index e45ade8806..55eb153500 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/errors/DonationErrorSource.kt @@ -31,6 +31,11 @@ enum class DonationErrorSource(private val code: String) { */ KEEP_ALIVE("keep-alive"), + /** + * Refers to backup payments. + */ + BACKUPS("backups"), + UNKNOWN("unknown"); fun serialize(): String = code diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentPurchaseTokenJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentPurchaseTokenJob.kt index d83a873462..c6f3ad12a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentPurchaseTokenJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentPurchaseTokenJob.kt @@ -177,6 +177,11 @@ class InAppPaymentPurchaseTokenJob private constructor( info("Scheduling retry.") throw InAppPaymentRetryException() } + + else -> { + warning("An unknown error occurred.", applicationError) + throw IOException(applicationError) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt index e8e0a3bf32..1cbb4dfdcc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.jobs import okio.ByteString.Companion.toByteString import org.signal.core.util.logging.Log +import org.signal.donations.InAppPaymentType import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.receipts.ReceiptCredential import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation @@ -65,11 +66,17 @@ class InAppPaymentRecurringContextJob private constructor( * meaning the job will always load the freshest data it can about the payment. */ fun createJobChain(inAppPayment: InAppPaymentTable.InAppPayment, makePrimary: Boolean = false): Chain { - return AppDependencies.jobManager - .startChain(create(inAppPayment)) - .then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary)) - .then(RefreshOwnProfileJob()) - .then(MultiDeviceProfileContentUpdateJob()) + return if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) { + AppDependencies.jobManager + .startChain(create(inAppPayment)) + .then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary)) + } else { + AppDependencies.jobManager + .startChain(create(inAppPayment)) + .then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary)) + .then(RefreshOwnProfileJob()) + .then(MultiDeviceProfileContentUpdateJob()) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt index 7cc8b218f4..fcc0929384 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log import org.signal.donations.InAppPaymentType import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType import org.thoughtcrime.securesms.database.InAppPaymentTable @@ -252,6 +253,12 @@ class InAppPaymentRedemptionJob private constructor( ) ) ) + + if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) { + Log.i(TAG, "Enabling backups and setting backup tier to PAID", true) + SignalStore.backup.areBackupsEnabled = true + SignalStore.backup.backupTier = MessageBackupTier.PAID + } } private fun verifyServiceResponse(serviceResponse: ServiceResponse, onFatalError: (Int) -> Unit = {}) { diff --git a/billing/src/main/java/org/signal/billing/BillingApiImpl.kt b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt index c57994d8ba..4e67fb2212 100644 --- a/billing/src/main/java/org/signal/billing/BillingApiImpl.kt +++ b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt @@ -74,8 +74,10 @@ internal class BillingApiImpl( Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.") val newestPurchase = purchases.maxByOrNull { it.purchaseTime } if (newestPurchase == null) { + Log.d(TAG, "purchasesUpdatedListener: no purchase.") BillingPurchaseResult.None } else { + Log.d(TAG, "purchasesUpdatedListener: successful purchase at ${newestPurchase.purchaseTime}") BillingPurchaseResult.Success( purchaseToken = newestPurchase.purchaseToken, isAcknowledged = newestPurchase.isAcknowledged, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 2bdf5b1b3f..251cd150e4 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -1432,7 +1432,7 @@ public class PushServiceSocket { } public void linkPlayBillingPurchaseToken(String subscriberId, String purchaseToken) throws IOException { - makeServiceRequestWithoutAuthentication(String.format(LINK_PLAY_BILLING_PURCHASE_TOKEN, subscriberId, purchaseToken), "PUT", "", NO_HEADERS, new LinkGooglePlayBillingPurchaseTokenResponseCodeHandler()); + makeServiceRequestWithoutAuthentication(String.format(LINK_PLAY_BILLING_PURCHASE_TOKEN, subscriberId, purchaseToken), "POST", "", NO_HEADERS, new LinkGooglePlayBillingPurchaseTokenResponseCodeHandler()); } public void updateSubscriptionLevel(String subscriberId, String level, String currencyCode, String idempotencyKey) throws IOException {