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
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
@ -15,10 +17,13 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.rx3.asFlowable
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
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.ComposeFragment
|
||||||
import org.thoughtcrime.securesms.compose.Nav
|
import org.thoughtcrime.securesms.compose.Nav
|
||||||
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
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.
|
* 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 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
|
@Composable
|
||||||
override fun FragmentContent() {
|
override fun FragmentContent() {
|
||||||
|
@ -83,6 +99,7 @@ class MessageBackupsFlowFragment : ComposeFragment() {
|
||||||
|
|
||||||
composable(route = MessageBackupsStage.Route.TYPE_SELECTION.name) {
|
composable(route = MessageBackupsStage.Route.TYPE_SELECTION.name) {
|
||||||
MessageBackupsTypeSelectionScreen(
|
MessageBackupsTypeSelectionScreen(
|
||||||
|
stage = state.stage,
|
||||||
currentBackupTier = state.currentMessageBackupTier,
|
currentBackupTier = state.currentMessageBackupTier,
|
||||||
selectedBackupTier = state.selectedMessageBackupTier,
|
selectedBackupTier = state.selectedMessageBackupTier,
|
||||||
availableBackupTypes = state.availableBackupTypes.filter { it.tier == MessageBackupTier.FREE || state.hasBackupSubscriberAvailable },
|
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 androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.take
|
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.billing.BillingPurchaseResult
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
|
import org.signal.donations.PaymentSourceType
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
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.DonationSerializationHelper.toFiatValue
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
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.RecurringInAppPaymentRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentError
|
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.InAppPaymentTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
|
@ -84,13 +89,12 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
AppDependencies.billingApi.getBillingPurchaseResults().collect { result ->
|
AppDependencies.billingApi.getBillingPurchaseResults().collect { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is BillingPurchaseResult.Success -> {
|
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 {
|
try {
|
||||||
handleSuccess(
|
Log.d(TAG, "Attempting to handle successful purchase.")
|
||||||
result,
|
handleSuccess(result, id)
|
||||||
internalStateFlow.value.inAppPayment!!.id
|
|
||||||
)
|
|
||||||
|
|
||||||
internalStateFlow.update {
|
internalStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -98,6 +102,14 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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 {
|
internalStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
stage = MessageBackupsStage.FAILURE,
|
stage = MessageBackupsStage.FAILURE,
|
||||||
|
@ -123,7 +135,7 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD)
|
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD)
|
||||||
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
|
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
|
||||||
MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it)
|
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.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_PAYMENT -> error("This is driven by an async coroutine.")
|
||||||
MessageBackupsStage.PROCESS_FREE -> error("This is driven by an async coroutine.")
|
MessageBackupsStage.PROCESS_FREE -> error("This is driven by an async coroutine.")
|
||||||
|
@ -174,19 +186,13 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
state.copy(stage = MessageBackupsStage.COMPLETED)
|
state.copy(stage = MessageBackupsStage.COMPLETED)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBackupTier.PAID -> state.copy(stage = MessageBackupsStage.CHECKOUT_SHEET)
|
MessageBackupTier.PAID -> {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateGatewayAndUpdateState(state: MessageBackupsFlowState): MessageBackupsFlowState {
|
|
||||||
check(state.selectedMessageBackupTier == MessageBackupTier.PAID)
|
check(state.selectedMessageBackupTier == MessageBackupTier.PAID)
|
||||||
check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier })
|
check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier })
|
||||||
check(state.hasBackupSubscriberAvailable)
|
check(state.hasBackupSubscriberAvailable)
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
internalStateFlow.update { it.copy(inAppPayment = null) }
|
internalStateFlow.update { it.copy(inAppPayment = null) }
|
||||||
}
|
|
||||||
|
|
||||||
val paidFiat = AppDependencies.billingApi.queryProduct()!!.price
|
val paidFiat = AppDependencies.billingApi.queryProduct()!!.price
|
||||||
|
|
||||||
|
@ -210,13 +216,14 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
|
|
||||||
val inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
|
val inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
|
||||||
|
internalStateFlow.update {
|
||||||
withContext(Dispatchers.Main) {
|
it.copy(inAppPayment = inAppPayment, stage = MessageBackupsStage.CHECKOUT_SHEET)
|
||||||
internalStateFlow.update { it.copy(inAppPayment = inAppPayment, stage = MessageBackupsStage.PROCESS_PAYMENT) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.copy(stage = MessageBackupsStage.CREATING_IN_APP_PAYMENT)
|
state.copy(stage = MessageBackupsStage.CREATING_IN_APP_PAYMENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -237,6 +244,8 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Setting purchase token data on InAppPayment.")
|
||||||
|
|
||||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||||
SignalDatabase.inAppPayments.update(
|
SignalDatabase.inAppPayments.update(
|
||||||
inAppPayment.copy(
|
inAppPayment.copy(
|
||||||
|
@ -248,20 +257,30 @@ class MessageBackupsFlowViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Log.d(TAG, "Enqueueing InAppPaymentPurchaseTokenJob chain.")
|
||||||
InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue()
|
InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
val terminalInAppPayment = withContext(Dispatchers.IO) {
|
val terminalInAppPayment = withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Awaiting completion of job chain for up to 10 seconds.")
|
||||||
InAppPaymentsRepository.observeUpdates(inAppPaymentId).asFlow()
|
InAppPaymentsRepository.observeUpdates(inAppPaymentId).asFlow()
|
||||||
.filter { it.state == InAppPaymentTable.State.END }
|
.filter { it.state == InAppPaymentTable.State.END }
|
||||||
.take(1)
|
.take(1)
|
||||||
.timeout(10.seconds)
|
.timeout(10.seconds)
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is TimeoutCancellationException) {
|
||||||
|
throw DonationError.BadgeRedemptionError.TimeoutWaitingForTokenError(DonationErrorSource.BACKUPS)
|
||||||
|
}
|
||||||
|
}
|
||||||
.first()
|
.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (terminalInAppPayment.data.error != null) {
|
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 {
|
} else {
|
||||||
|
Log.d(TAG, "Job chain completed successfully.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ enum class MessageBackupsStage(
|
||||||
BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION),
|
BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION),
|
||||||
BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD),
|
BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD),
|
||||||
TYPE_SELECTION(route = Route.TYPE_SELECTION),
|
TYPE_SELECTION(route = Route.TYPE_SELECTION),
|
||||||
CHECKOUT_SHEET(route = Route.TYPE_SELECTION),
|
|
||||||
CREATING_IN_APP_PAYMENT(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_PAYMENT(route = Route.TYPE_SELECTION),
|
||||||
PROCESS_FREE(route = Route.TYPE_SELECTION),
|
PROCESS_FREE(route = Route.TYPE_SELECTION),
|
||||||
COMPLETED(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 androidx.compose.ui.unit.dp
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
|
import org.signal.core.ui.Dialogs
|
||||||
import org.signal.core.ui.Previews
|
import org.signal.core.ui.Previews
|
||||||
import org.signal.core.ui.Scaffolds
|
import org.signal.core.ui.Scaffolds
|
||||||
import org.signal.core.ui.SignalPreview
|
import org.signal.core.ui.SignalPreview
|
||||||
|
@ -64,6 +65,7 @@ import java.util.Currency
|
||||||
@OptIn(ExperimentalTextApi::class)
|
@OptIn(ExperimentalTextApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBackupsTypeSelectionScreen(
|
fun MessageBackupsTypeSelectionScreen(
|
||||||
|
stage: MessageBackupsStage,
|
||||||
currentBackupTier: MessageBackupTier?,
|
currentBackupTier: MessageBackupTier?,
|
||||||
selectedBackupTier: MessageBackupTier?,
|
selectedBackupTier: MessageBackupTier?,
|
||||||
availableBackupTypes: List<MessageBackupsType>,
|
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 {
|
Previews.Preview {
|
||||||
MessageBackupsTypeSelectionScreen(
|
MessageBackupsTypeSelectionScreen(
|
||||||
|
stage = MessageBackupsStage.TYPE_SELECTION,
|
||||||
selectedBackupTier = MessageBackupTier.FREE,
|
selectedBackupTier = MessageBackupTier.FREE,
|
||||||
availableBackupTypes = testBackupTypes(),
|
availableBackupTypes = testBackupTypes(),
|
||||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||||
|
@ -197,6 +207,7 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
||||||
|
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
MessageBackupsTypeSelectionScreen(
|
MessageBackupsTypeSelectionScreen(
|
||||||
|
stage = MessageBackupsStage.TYPE_SELECTION,
|
||||||
selectedBackupTier = MessageBackupTier.FREE,
|
selectedBackupTier = MessageBackupTier.FREE,
|
||||||
availableBackupTypes = testBackupTypes(),
|
availableBackupTypes = testBackupTypes(),
|
||||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||||
|
|
|
@ -224,6 +224,7 @@ object InAppPaymentsRepository {
|
||||||
DonationErrorSource.ONE_TIME -> InAppPaymentType.ONE_TIME_DONATION
|
DonationErrorSource.ONE_TIME -> InAppPaymentType.ONE_TIME_DONATION
|
||||||
DonationErrorSource.MONTHLY -> InAppPaymentType.RECURRING_DONATION
|
DonationErrorSource.MONTHLY -> InAppPaymentType.RECURRING_DONATION
|
||||||
DonationErrorSource.GIFT -> InAppPaymentType.ONE_TIME_GIFT
|
DonationErrorSource.GIFT -> InAppPaymentType.ONE_TIME_GIFT
|
||||||
|
DonationErrorSource.BACKUPS -> InAppPaymentType.RECURRING_BACKUP
|
||||||
DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentType.UNKNOWN
|
DonationErrorSource.GIFT_REDEMPTION -> InAppPaymentType.UNKNOWN
|
||||||
DonationErrorSource.KEEP_ALIVE -> InAppPaymentType.UNKNOWN
|
DonationErrorSource.KEEP_ALIVE -> InAppPaymentType.UNKNOWN
|
||||||
DonationErrorSource.UNKNOWN -> InAppPaymentType.UNKNOWN
|
DonationErrorSource.UNKNOWN -> InAppPaymentType.UNKNOWN
|
||||||
|
@ -266,7 +267,7 @@ object InAppPaymentsRepository {
|
||||||
InAppPaymentType.ONE_TIME_GIFT -> DonationErrorSource.GIFT
|
InAppPaymentType.ONE_TIME_GIFT -> DonationErrorSource.GIFT
|
||||||
InAppPaymentType.ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
|
InAppPaymentType.ONE_TIME_DONATION -> DonationErrorSource.ONE_TIME
|
||||||
InAppPaymentType.RECURRING_DONATION -> DonationErrorSource.MONTHLY
|
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"),
|
KEEP_ALIVE("keep-alive"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refers to backup payments.
|
||||||
|
*/
|
||||||
|
BACKUPS("backups"),
|
||||||
|
|
||||||
UNKNOWN("unknown");
|
UNKNOWN("unknown");
|
||||||
|
|
||||||
fun serialize(): String = code
|
fun serialize(): String = code
|
||||||
|
|
|
@ -177,6 +177,11 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||||
info("Scheduling retry.")
|
info("Scheduling retry.")
|
||||||
throw InAppPaymentRetryException()
|
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 okio.ByteString.Companion.toByteString
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.signal.libsignal.zkgroup.VerificationFailedException
|
import org.signal.libsignal.zkgroup.VerificationFailedException
|
||||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredential
|
import org.signal.libsignal.zkgroup.receipts.ReceiptCredential
|
||||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||||
|
@ -65,13 +66,19 @@ class InAppPaymentRecurringContextJob private constructor(
|
||||||
* meaning the job will always load the freshest data it can about the payment.
|
* meaning the job will always load the freshest data it can about the payment.
|
||||||
*/
|
*/
|
||||||
fun createJobChain(inAppPayment: InAppPaymentTable.InAppPayment, makePrimary: Boolean = false): Chain {
|
fun createJobChain(inAppPayment: InAppPaymentTable.InAppPayment, makePrimary: Boolean = false): Chain {
|
||||||
return AppDependencies.jobManager
|
return if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
|
||||||
|
AppDependencies.jobManager
|
||||||
|
.startChain(create(inAppPayment))
|
||||||
|
.then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary))
|
||||||
|
} else {
|
||||||
|
AppDependencies.jobManager
|
||||||
.startChain(create(inAppPayment))
|
.startChain(create(inAppPayment))
|
||||||
.then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary))
|
.then(InAppPaymentRedemptionJob.create(inAppPayment, makePrimary))
|
||||||
.then(RefreshOwnProfileJob())
|
.then(RefreshOwnProfileJob())
|
||||||
.then(MultiDeviceProfileContentUpdateJob())
|
.then(MultiDeviceProfileContentUpdateJob())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAdded() {
|
override fun onAdded() {
|
||||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.jobs
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
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
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
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 = {}) {
|
private fun <T> verifyServiceResponse(serviceResponse: ServiceResponse<T>, onFatalError: (Int) -> Unit = {}) {
|
||||||
|
|
|
@ -74,8 +74,10 @@ internal class BillingApiImpl(
|
||||||
Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.")
|
Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.")
|
||||||
val newestPurchase = purchases.maxByOrNull { it.purchaseTime }
|
val newestPurchase = purchases.maxByOrNull { it.purchaseTime }
|
||||||
if (newestPurchase == null) {
|
if (newestPurchase == null) {
|
||||||
|
Log.d(TAG, "purchasesUpdatedListener: no purchase.")
|
||||||
BillingPurchaseResult.None
|
BillingPurchaseResult.None
|
||||||
} else {
|
} else {
|
||||||
|
Log.d(TAG, "purchasesUpdatedListener: successful purchase at ${newestPurchase.purchaseTime}")
|
||||||
BillingPurchaseResult.Success(
|
BillingPurchaseResult.Success(
|
||||||
purchaseToken = newestPurchase.purchaseToken,
|
purchaseToken = newestPurchase.purchaseToken,
|
||||||
isAcknowledged = newestPurchase.isAcknowledged,
|
isAcknowledged = newestPurchase.isAcknowledged,
|
||||||
|
|
|
@ -1432,7 +1432,7 @@ public class PushServiceSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void linkPlayBillingPurchaseToken(String subscriberId, String purchaseToken) throws IOException {
|
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 {
|
public void updateSubscriptionLevel(String subscriberId, String level, String currencyCode, String idempotencyKey) throws IOException {
|
||||||
|
|
Loading…
Add table
Reference in a new issue