Implement happy path for backups subscriptions.
This commit is contained in:
parent
c80ebd5658
commit
81d99c9d30
12 changed files with 132 additions and 155 deletions
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<MessageBackupsType>,
|
||||
|
@ -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 },
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -177,6 +177,11 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
|||
info("Scheduling retry.")
|
||||
throw InAppPaymentRetryException()
|
||||
}
|
||||
|
||||
else -> {
|
||||
warning("An unknown error occurred.", applicationError)
|
||||
throw IOException(applicationError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <T> verifyServiceResponse(serviceResponse: ServiceResponse<T>, onFatalError: (Int) -> Unit = {}) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue