mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Add additional backup screen states.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Describes the state of the user's selected backup tier.
|
||||
*/
|
||||
sealed interface BackupState {
|
||||
/**
|
||||
* Backups are not available on this device
|
||||
*/
|
||||
data object NotAvailable : BackupState
|
||||
|
||||
/**
|
||||
* User has no active backup tier, no tier history
|
||||
*/
|
||||
data object None : BackupState
|
||||
|
||||
/**
|
||||
* The exact backup state is being loaded from the network.
|
||||
*/
|
||||
data object Loading : BackupState
|
||||
|
||||
/**
|
||||
* User has a paid backup subscription pending redemption
|
||||
*/
|
||||
data class Pending(
|
||||
val price: FiatMoney
|
||||
) : BackupState
|
||||
|
||||
/**
|
||||
* A backup state with a type and renewal time
|
||||
*/
|
||||
sealed interface WithTypeAndRenewalTime : BackupState {
|
||||
val messageBackupsType: MessageBackupsType
|
||||
val renewalTime: Duration
|
||||
|
||||
fun isActive(): Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an active paid backup. Pricing comes from the subscription object.
|
||||
*/
|
||||
data class ActivePaid(
|
||||
override val messageBackupsType: MessageBackupsType.Paid,
|
||||
val price: FiatMoney,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime {
|
||||
override fun isActive(): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an active free backup.
|
||||
*/
|
||||
data class ActiveFree(
|
||||
override val messageBackupsType: MessageBackupsType.Free,
|
||||
override val renewalTime: Duration = 0.seconds
|
||||
) : WithTypeAndRenewalTime {
|
||||
override fun isActive(): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an inactive backup
|
||||
*/
|
||||
data class Inactive(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration = 0.seconds
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* User has an active backup but no active subscription
|
||||
*/
|
||||
data object NotFound : BackupState
|
||||
|
||||
/**
|
||||
* User has a canceled paid tier backup
|
||||
*/
|
||||
data class Canceled(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* Subscription mismatch detected.
|
||||
*/
|
||||
data class SubscriptionMismatchMissingGooglePlay(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* An error occurred retrieving the network state
|
||||
*/
|
||||
data object Error : BackupState
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Manages BackupState information gathering for the UI.
|
||||
*/
|
||||
object BackupStateRepository {
|
||||
|
||||
private val TAG = Log.tag(BackupStateRepository::class)
|
||||
|
||||
suspend fun resolveBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState {
|
||||
if (lastPurchase?.state == InAppPaymentTable.State.PENDING) {
|
||||
Log.d(TAG, "We have a pending subscription.")
|
||||
return BackupState.Pending(
|
||||
price = lastPurchase.data.amount!!.toFiatMoney()
|
||||
)
|
||||
}
|
||||
|
||||
if (SignalStore.backup.subscriptionStateMismatchDetected) {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.")
|
||||
|
||||
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
|
||||
is BillingPurchaseResult.Success -> {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] Found a purchase: $purchaseResult")
|
||||
purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] No purchase found in Google Play Billing: $purchaseResult")
|
||||
false
|
||||
}
|
||||
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
|
||||
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
|
||||
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
|
||||
}
|
||||
|
||||
val hasActiveSignalSubscription = activeSubscription?.isActive == true
|
||||
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription")
|
||||
|
||||
when {
|
||||
hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
|
||||
val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription)
|
||||
|
||||
if (type == null) {
|
||||
Log.d(TAG, "[subscriptionMismatchDetected] failed to load backup configuration. Likely a network error.")
|
||||
return BackupState.Error
|
||||
}
|
||||
|
||||
return BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||
messageBackupsType = type,
|
||||
renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
}
|
||||
|
||||
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true")
|
||||
return BackupState.NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when (SignalStore.backup.latestBackupTier) {
|
||||
MessageBackupTier.PAID -> {
|
||||
getPaidBackupState(lastPurchase)
|
||||
}
|
||||
|
||||
MessageBackupTier.FREE -> {
|
||||
getFreeBackupState()
|
||||
}
|
||||
|
||||
null -> {
|
||||
Log.d(TAG, "Updating UI state with NONE null tier.")
|
||||
return BackupState.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPaidBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState {
|
||||
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.")
|
||||
|
||||
val type = withContext(Dispatchers.IO) {
|
||||
BackupRepository.getBackupsType(MessageBackupTier.PAID) as? MessageBackupsType.Paid
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempting to retrieve current subscription...")
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
return if (activeSubscription.isSuccess) {
|
||||
Log.d(TAG, "Retrieved subscription details.")
|
||||
|
||||
val subscription = activeSubscription.getOrThrow().activeSubscription
|
||||
if (subscription != null) {
|
||||
Log.d(TAG, "Subscription found. Updating UI state with subscription details. Status: ${subscription.status}")
|
||||
|
||||
val subscriberType = type ?: buildPaidTypeFromSubscription(subscription)
|
||||
if (subscriberType == null) {
|
||||
Log.d(TAG, "Failed to create backup type. Possible network error.")
|
||||
|
||||
BackupState.Error
|
||||
} else {
|
||||
when {
|
||||
subscription.isCanceled && subscription.isActive -> BackupState.Canceled(
|
||||
messageBackupsType = subscriberType,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
subscription.isActive -> BackupState.ActivePaid(
|
||||
messageBackupsType = subscriberType,
|
||||
price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)),
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
else -> BackupState.Inactive(
|
||||
messageBackupsType = subscriberType,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ActiveSubscription had null subscription object.")
|
||||
if (SignalStore.backup.areBackupsEnabled) {
|
||||
BackupState.NotFound
|
||||
} else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) {
|
||||
val canceledType = type ?: buildPaidTypeFromInAppPayment(lastPurchase)
|
||||
if (canceledType == null) {
|
||||
Log.w(TAG, "Failed to load canceled type information. Possible network error.")
|
||||
BackupState.Error
|
||||
} else {
|
||||
BackupState.Canceled(
|
||||
messageBackupsType = canceledType,
|
||||
renewalTime = lastPurchase.endOfPeriod
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val inactiveType = type ?: buildPaidTypeWithoutPricing()
|
||||
if (inactiveType == null) {
|
||||
Log.w(TAG, "Failed to load inactive type information. Possible network error.")
|
||||
BackupState.Error
|
||||
} else {
|
||||
BackupState.Inactive(
|
||||
messageBackupsType = inactiveType,
|
||||
renewalTime = lastPurchase?.endOfPeriod ?: 0.seconds
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.")
|
||||
BackupState.Error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFreeBackupState(): BackupState {
|
||||
val type = withContext(Dispatchers.IO) {
|
||||
BackupRepository.getBackupsType(MessageBackupTier.FREE) as MessageBackupsType.Free
|
||||
}
|
||||
|
||||
val backupState = if (SignalStore.backup.areBackupsEnabled) {
|
||||
BackupState.ActiveFree(type)
|
||||
} else {
|
||||
BackupState.Inactive(type)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating UI state with $backupState FREE tier.")
|
||||
return backupState
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out a Paid type utilizing pricing information stored in the user's active subscription object.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeFromSubscription(subscription: ActiveSubscription.Subscription): MessageBackupsType.Paid? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
val price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = price,
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out a Paid type utilizing pricing information stored in the given in-app payment.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeFromInAppPayment(inAppPayment: InAppPaymentTable.InAppPayment): MessageBackupsType.Paid? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
val price = inAppPayment.data.amount!!.toFiatMoney()
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = price,
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* In the case of an Inactive subscription, we only care about the storage allowance and TTL, both of which we can
|
||||
* grab from the backup level configuration.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeWithoutPricing(): MessageBackupsType? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault())),
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -59,6 +61,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
@@ -91,16 +94,16 @@ class BackupsSettingsFragment : ComposeFragment() {
|
||||
backupsSettingsState = state,
|
||||
onNavigationClick = { requireActivity().onNavigateUp() },
|
||||
onBackupsRowClick = {
|
||||
when (state.enabledState) {
|
||||
is BackupsSettingsState.EnabledState.Active, BackupsSettingsState.EnabledState.Inactive -> {
|
||||
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
||||
}
|
||||
when (state.backupState) {
|
||||
BackupState.Loading, BackupState.Error, BackupState.NotAvailable -> Unit
|
||||
|
||||
BackupsSettingsState.EnabledState.Never -> {
|
||||
BackupState.None -> {
|
||||
checkoutLauncher.launch(null)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
else -> {
|
||||
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
||||
}
|
||||
}
|
||||
},
|
||||
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) },
|
||||
@@ -152,14 +155,14 @@ private fun BackupsSettingsContent(
|
||||
}
|
||||
|
||||
item {
|
||||
when (backupsSettingsState.enabledState) {
|
||||
BackupsSettingsState.EnabledState.Loading -> {
|
||||
when (backupsSettingsState.backupState) {
|
||||
BackupState.Loading -> {
|
||||
LoadingBackupsRow()
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Inactive -> {
|
||||
is BackupState.Inactive -> {
|
||||
InactiveBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
@@ -167,16 +170,17 @@ private fun BackupsSettingsContent(
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
is BackupsSettingsState.EnabledState.Active -> {
|
||||
is BackupState.ActiveFree, is BackupState.ActivePaid -> {
|
||||
ActiveBackupsRow(
|
||||
enabledState = backupsSettingsState.enabledState,
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
backupState = backupsSettingsState.backupState,
|
||||
onBackupsRowClick = onBackupsRowClick,
|
||||
lastBackupAt = backupsSettingsState.lastBackupAt
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Never -> {
|
||||
BackupState.None -> {
|
||||
NeverEnabledBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
@@ -184,12 +188,46 @@ private fun BackupsSettingsContent(
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Failed -> {
|
||||
BackupState.Error -> {
|
||||
WaitingForNetworkRow()
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.NotAvailable -> Unit
|
||||
BackupState.NotAvailable -> Unit
|
||||
|
||||
BackupState.NotFound -> {
|
||||
NotFoundBackupRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
is BackupState.Pending -> {
|
||||
PendingBackupRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
is BackupState.Canceled -> {
|
||||
ActiveBackupsRow(
|
||||
backupState = backupsSettingsState.backupState,
|
||||
lastBackupAt = backupsSettingsState.lastBackupAt
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
is BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
||||
ActiveBackupsRow(
|
||||
backupState = backupsSettingsState.backupState,
|
||||
lastBackupAt = backupsSettingsState.lastBackupAt
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,9 +330,75 @@ private fun InactiveBackupsRow(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotFoundBackupRow(
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
icon = {
|
||||
Box(
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_backup_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
Text(
|
||||
text = stringResource(R.string.BackupsSettingsFragment_subscription_not_found_on_this_device),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingBackupRow(
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
icon = {
|
||||
Box(
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__payment_pending),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = onBackupsRowClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveBackupsRow(
|
||||
enabledState: BackupsSettingsState.EnabledState.Active,
|
||||
backupState: BackupState.WithTypeAndRenewalTime,
|
||||
lastBackupAt: Duration,
|
||||
onBackupsRowClick: () -> Unit = {}
|
||||
) {
|
||||
Rows.TextRow(
|
||||
@@ -316,13 +420,13 @@ private fun ActiveBackupsRow(
|
||||
Column {
|
||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||
|
||||
when (enabledState.type) {
|
||||
when (val type = backupState.messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> {
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.BackupsSettingsFragment_s_month_renews_s,
|
||||
FiatMoneyUtil.format(LocalContext.current.resources, enabledState.type.pricePerMonth),
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), enabledState.expiresAt.inWholeMilliseconds)
|
||||
FiatMoneyUtil.format(LocalContext.current.resources, type.pricePerMonth),
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
@@ -346,7 +450,7 @@ private fun ActiveBackupsRow(
|
||||
DateUtils.getDatelessRelativeTimeSpanFormattedDate(
|
||||
LocalContext.current,
|
||||
Locale.getDefault(),
|
||||
enabledState.lastBackupAt.inWholeMilliseconds
|
||||
lastBackupAt.inWholeMilliseconds
|
||||
).value
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -404,14 +508,14 @@ private fun BackupsSettingsContentPreview() {
|
||||
Previews.Preview {
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = BackupsSettingsState(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Paid(
|
||||
backupState = BackupState.ActivePaid(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 1_000_000,
|
||||
mediaTtl = 30.days
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = 0.seconds
|
||||
renewalTime = 0.seconds,
|
||||
price = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD"))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -424,7 +528,7 @@ private fun BackupsSettingsContentNotAvailablePreview() {
|
||||
Previews.Preview {
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = BackupsSettingsState(
|
||||
enabledState = BackupsSettingsState.EnabledState.NotAvailable
|
||||
backupState = BackupState.NotAvailable
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -436,7 +540,7 @@ private fun BackupsSettingsContentBackupTierInternalOverridePreview() {
|
||||
Previews.Preview {
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = BackupsSettingsState(
|
||||
enabledState = BackupsSettingsState.EnabledState.Never,
|
||||
backupState = BackupState.None,
|
||||
showBackupTierInternalOverride = true,
|
||||
backupTierInternalOverride = null
|
||||
)
|
||||
@@ -460,21 +564,38 @@ private fun InactiveBackupsRowPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun NotFoundBackupRowPreview() {
|
||||
Previews.Preview {
|
||||
NotFoundBackupRow()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun PendingBackupRowPreview() {
|
||||
Previews.Preview {
|
||||
PendingBackupRow()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ActivePaidBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
ActiveBackupsRow(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Paid(
|
||||
backupState = BackupState.ActivePaid(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 1_000_000,
|
||||
mediaTtl = 30.days
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
renewalTime = 0.seconds,
|
||||
price = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD"))
|
||||
),
|
||||
lastBackupAt = 0.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,14 +604,14 @@ private fun ActivePaidBackupsRowPreview() {
|
||||
private fun ActiveFreeBackupsRowPreview() {
|
||||
Previews.Preview {
|
||||
ActiveBackupsRow(
|
||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
||||
type = MessageBackupsType.Free(
|
||||
backupState = BackupState.ActiveFree(
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
expiresAt = 0.seconds,
|
||||
renewalTime = 0.seconds
|
||||
),
|
||||
lastBackupAt = 0.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,49 +6,16 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Screen state for top-level backups settings screen.
|
||||
*/
|
||||
data class BackupsSettingsState(
|
||||
val enabledState: EnabledState = EnabledState.Loading,
|
||||
val backupState: BackupState = BackupState.Loading,
|
||||
val lastBackupAt: Duration = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
val showBackupTierInternalOverride: Boolean = false,
|
||||
val backupTierInternalOverride: MessageBackupTier? = null
|
||||
) {
|
||||
/**
|
||||
* Describes the 'enabled' state of backups.
|
||||
*/
|
||||
sealed interface EnabledState {
|
||||
/**
|
||||
* Loading data for this row
|
||||
*/
|
||||
data object Loading : EnabledState
|
||||
|
||||
/**
|
||||
* Google Play Billing is not available on this device
|
||||
*/
|
||||
data object NotAvailable : EnabledState
|
||||
|
||||
/**
|
||||
* Backups have never been enabled.
|
||||
*/
|
||||
data object Never : EnabledState
|
||||
|
||||
/**
|
||||
* Backups were active at one point, but have been turned off.
|
||||
*/
|
||||
data object Inactive : EnabledState
|
||||
|
||||
/**
|
||||
* Backup state couldn't be retrieved from the server for some reason
|
||||
*/
|
||||
data object Failed : EnabledState
|
||||
|
||||
/**
|
||||
* Backups are currently active.
|
||||
*/
|
||||
data class Active(val type: MessageBackupsType, val expiresAt: Duration, val lastBackupAt: Duration) : EnabledState
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -20,21 +20,16 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
@@ -56,7 +51,7 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
.drop(1)
|
||||
.collect {
|
||||
Log.d(TAG, "Triggering refresh from internet reconnect.")
|
||||
loadRequests.tryEmit(Unit)
|
||||
loadRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +62,13 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
Log.d(TAG, "-- Completed state load.")
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(SignalDispatchers.Default) {
|
||||
InAppPaymentsRepository.observeLatestBackupPayment().collect {
|
||||
Log.d(TAG, "Triggering refresh from payment state change.")
|
||||
loadRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -75,7 +77,9 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
fun refreshState() {
|
||||
Log.d(TAG, "Refreshing state from manual call.")
|
||||
loadRequests.tryEmit(Unit)
|
||||
viewModelScope.launch(SignalDispatchers.Default) {
|
||||
loadRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -83,18 +87,16 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
return viewModelScope.launch(SignalDispatchers.IO) {
|
||||
if (!RemoteConfig.messageBackups) {
|
||||
Log.w(TAG, "Remote backups are not available on this device.")
|
||||
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable, showBackupTierInternalOverride = false) }
|
||||
internalStateFlow.update { it.copy(backupState = BackupState.NotAvailable, showBackupTierInternalOverride = false) }
|
||||
} else {
|
||||
val enabledState = when (SignalStore.backup.backupTier) {
|
||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
||||
null -> getEnabledStateForNoTier()
|
||||
}
|
||||
val latestPurchase = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
|
||||
val enabledState = BackupStateRepository.resolveBackupState(latestPurchase)
|
||||
|
||||
Log.d(TAG, "Found enabled state $enabledState. Updating UI state.")
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
enabledState = enabledState,
|
||||
backupState = enabledState,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
showBackupTierInternalOverride = RemoteConfig.internalUser || Environment.IS_STAGING,
|
||||
backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride
|
||||
)
|
||||
@@ -108,62 +110,4 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
SignalStore.backup.deletionState = DeletionState.NONE
|
||||
refreshState()
|
||||
}
|
||||
|
||||
private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to grab enabled state for free tier.")
|
||||
val backupType = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
|
||||
|
||||
Log.d(TAG, "Retrieved backup type. Returning active state...")
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = 0.seconds,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
type = backupType
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to build enabled state.", e)
|
||||
BackupsSettingsState.EnabledState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getEnabledStateForPaidTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to grab enabled state for paid tier.")
|
||||
val backupConfiguration = BackupRepository.getBackupLevelConfiguration() ?: return BackupsSettingsState.EnabledState.Failed
|
||||
|
||||
Log.d(TAG, "Retrieved backup type. Grabbing active subscription...")
|
||||
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrThrow()
|
||||
|
||||
Log.d(TAG, "Retrieved subscription. Active? ${activeSubscription.isActive}")
|
||||
if (activeSubscription.isActive) {
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
expiresAt = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds,
|
||||
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||
type = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney.fromSignalNetworkAmount(
|
||||
activeSubscription.activeSubscription.amount,
|
||||
Currency.getInstance(activeSubscription.activeSubscription.currency)
|
||||
),
|
||||
storageAllowanceBytes = backupConfiguration.storageAllowanceBytes,
|
||||
mediaTtl = backupConfiguration.mediaTtlDays.days
|
||||
)
|
||||
)
|
||||
} else {
|
||||
BackupsSettingsState.EnabledState.Inactive
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to build enabled state.", e)
|
||||
BackupsSettingsState.EnabledState.Failed
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnabledStateForNoTier(): BackupsSettingsState.EnabledState {
|
||||
Log.d(TAG, "Grabbing enabled state for no tier.")
|
||||
return if (SignalStore.uiHints.hasEverEnabledRemoteBackups) {
|
||||
BackupsSettingsState.EnabledState.Inactive
|
||||
} else {
|
||||
BackupsSettingsState.EnabledState.Never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
|
||||
import org.thoughtcrime.securesms.components.compose.BetaHeader
|
||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||
@@ -448,19 +449,19 @@ private fun RemoteBackupsSettingsContent(
|
||||
|
||||
item {
|
||||
when (state.backupState) {
|
||||
is RemoteBackupsSettingsState.BackupState.Loading -> {
|
||||
is BackupState.Loading -> {
|
||||
LoadingCard()
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.Error -> {
|
||||
is BackupState.Error -> {
|
||||
ErrorCard()
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.Pending -> {
|
||||
is BackupState.Pending -> {
|
||||
PendingCard(state.backupState.price)
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
||||
is BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
||||
SubscriptionMismatchMissingGooglePlayCard(
|
||||
state = state.backupState,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
||||
@@ -469,9 +470,9 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.BackupState.None -> Unit
|
||||
BackupState.None -> Unit
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
|
||||
is BackupState.WithTypeAndRenewalTime -> {
|
||||
BackupCard(
|
||||
backupState = state.backupState,
|
||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
|
||||
@@ -479,7 +480,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.BackupState.NotFound -> {
|
||||
BackupState.NotFound -> {
|
||||
SubscriptionNotFoundCard(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
|
||||
onRenewClick = contentCallbacks::onRenewLostSubscription,
|
||||
@@ -487,6 +488,8 @@ private fun RemoteBackupsSettingsContent(
|
||||
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
||||
)
|
||||
}
|
||||
|
||||
BackupState.NotAvailable -> error("This shouldn't happen on this screen.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,7 +585,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
SkipDownloadDuringDeleteDialog()
|
||||
} else {
|
||||
SkipDownloadDialog(
|
||||
renewalTime = if (state.backupState is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime) {
|
||||
renewalTime = if (state.backupState is BackupState.WithTypeAndRenewalTime) {
|
||||
state.backupState.renewalTime
|
||||
} else {
|
||||
error("Unexpected dialog display without renewal time.")
|
||||
@@ -800,7 +803,7 @@ private fun DescriptionText(
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDetailsItems(
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
backupState: BackupState,
|
||||
canViewBackupKey: Boolean,
|
||||
backupRestoreState: BackupRestoreState,
|
||||
backupProgress: ArchiveUploadProgressState?,
|
||||
@@ -853,7 +856,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
}
|
||||
}
|
||||
|
||||
if (backupState !is RemoteBackupsSettingsState.BackupState.ActiveFree) {
|
||||
if (backupState !is BackupState.ActiveFree) {
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
Column {
|
||||
@@ -923,7 +926,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
|
||||
@Composable
|
||||
private fun BackupCard(
|
||||
backupState: RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime,
|
||||
backupState: BackupState.WithTypeAndRenewalTime,
|
||||
buttonsEnabled: Boolean,
|
||||
onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {}
|
||||
) {
|
||||
@@ -950,21 +953,21 @@ private fun BackupCard(
|
||||
)
|
||||
|
||||
when (backupState) {
|
||||
is RemoteBackupsSettingsState.BackupState.ActivePaid -> {
|
||||
is BackupState.ActivePaid -> {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, backupState.price)),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.ActiveFree -> {
|
||||
is BackupState.ActiveFree -> {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.Inactive -> {
|
||||
is BackupState.Inactive -> {
|
||||
val text = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive)
|
||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__you_turned_off_backups)
|
||||
@@ -978,7 +981,7 @@ private fun BackupCard(
|
||||
)
|
||||
}
|
||||
|
||||
is RemoteBackupsSettingsState.BackupState.Canceled -> {
|
||||
is BackupState.Canceled -> {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_cancelled),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
@@ -992,9 +995,9 @@ private fun BackupCard(
|
||||
|
||||
if (messageBackupsType is MessageBackupsType.Paid) {
|
||||
val resource = when (backupState) {
|
||||
is RemoteBackupsSettingsState.BackupState.ActivePaid -> R.string.RemoteBackupsSettingsFragment__renews_s
|
||||
is RemoteBackupsSettingsState.BackupState.Inactive -> R.string.RemoteBackupsSettingsFragment__expired_on_s
|
||||
is RemoteBackupsSettingsState.BackupState.Canceled -> R.string.RemoteBackupsSettingsFragment__expires_on_s
|
||||
is BackupState.ActivePaid -> R.string.RemoteBackupsSettingsFragment__renews_s
|
||||
is BackupState.Inactive -> R.string.RemoteBackupsSettingsFragment__expired_on_s
|
||||
is BackupState.Canceled -> R.string.RemoteBackupsSettingsFragment__expires_on_s
|
||||
else -> error("Not supported here.")
|
||||
}
|
||||
|
||||
@@ -1026,7 +1029,7 @@ private fun BackupCard(
|
||||
enabled = buttonsEnabled,
|
||||
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }
|
||||
)
|
||||
} else if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
} else if (backupState is BackupState.Canceled) {
|
||||
CallToActionButton(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resubscribe),
|
||||
enabled = buttonsEnabled,
|
||||
@@ -1288,7 +1291,7 @@ private fun SubscriptionNotFoundCard(
|
||||
|
||||
@Composable
|
||||
private fun SubscriptionMismatchMissingGooglePlayCard(
|
||||
state: RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay,
|
||||
state: BackupState.SubscriptionMismatchMissingGooglePlay,
|
||||
isRenewEnabled: Boolean,
|
||||
onRenewClick: () -> Unit = {},
|
||||
onLearnMoreClick: () -> Unit = {}
|
||||
@@ -1662,10 +1665,10 @@ private fun BackupFrequencyDialog(
|
||||
@Composable
|
||||
private fun BackupReadyToDownloadRow(
|
||||
ready: BackupRestoreState.Ready,
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
backupState: BackupState,
|
||||
onDownloadClick: () -> Unit = {}
|
||||
) {
|
||||
val string = if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
||||
val string = if (backupState is BackupState.Canceled) {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, ready.bytes)
|
||||
} else {
|
||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
|
||||
@@ -1719,7 +1722,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
||||
dialog = RemoteBackupsSettingsState.Dialog.NONE,
|
||||
snackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||
backupMediaSize = 2300000,
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
|
||||
backupState = BackupState.ActiveFree(
|
||||
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30)
|
||||
),
|
||||
hasRedemptionError = true,
|
||||
@@ -1784,7 +1787,7 @@ private fun SubscriptionNotFoundCardPreview() {
|
||||
private fun SubscriptionMismatchMissingGooglePlayCardPreview() {
|
||||
Previews.Preview {
|
||||
SubscriptionMismatchMissingGooglePlayCard(
|
||||
state = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||
state = BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000,
|
||||
@@ -1803,7 +1806,7 @@ private fun BackupCardPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupCard(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ActivePaid(
|
||||
backupState = BackupState.ActivePaid(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000,
|
||||
@@ -1816,7 +1819,7 @@ private fun BackupCardPreview() {
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
backupState = BackupState.Canceled(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000,
|
||||
@@ -1828,7 +1831,7 @@ private fun BackupCardPreview() {
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Inactive(
|
||||
backupState = BackupState.Inactive(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000,
|
||||
@@ -1840,7 +1843,7 @@ private fun BackupCardPreview() {
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ActivePaid(
|
||||
backupState = BackupState.ActivePaid(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000,
|
||||
@@ -1853,7 +1856,7 @@ private fun BackupCardPreview() {
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
|
||||
backupState = BackupState.ActiveFree(
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
)
|
||||
@@ -1870,7 +1873,7 @@ private fun BackupReadyToDownloadPreview() {
|
||||
Previews.Preview {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = BackupRestoreState.Ready("12GB"),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.None
|
||||
backupState = BackupState.None
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1881,7 +1884,7 @@ private fun BackupReadyToDownloadAfterCancelPreview() {
|
||||
Previews.Preview {
|
||||
BackupReadyToDownloadRow(
|
||||
ready = BackupRestoreState.Ready("12GB"),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
backupState = BackupState.Canceled(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||
storageAllowanceBytes = 10.gibiBytes.bytes,
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||
|
||||
data class RemoteBackupsSettingsState(
|
||||
val backupsEnabled: Boolean,
|
||||
@@ -27,94 +24,6 @@ data class RemoteBackupsSettingsState(
|
||||
val snackbar: Snackbar = Snackbar.NONE
|
||||
) {
|
||||
|
||||
/**
|
||||
* Describes the state of the user's selected backup tier.
|
||||
*/
|
||||
sealed interface BackupState {
|
||||
|
||||
/**
|
||||
* User has no active backup tier, no tier history
|
||||
*/
|
||||
data object None : BackupState
|
||||
|
||||
/**
|
||||
* The exact backup state is being loaded from the network.
|
||||
*/
|
||||
data object Loading : BackupState
|
||||
|
||||
/**
|
||||
* User has a paid backup subscription pending redemption
|
||||
*/
|
||||
data class Pending(
|
||||
val price: FiatMoney
|
||||
) : BackupState
|
||||
|
||||
/**
|
||||
* A backup state with a type and renewal time
|
||||
*/
|
||||
sealed interface WithTypeAndRenewalTime : BackupState {
|
||||
val messageBackupsType: MessageBackupsType
|
||||
val renewalTime: Duration
|
||||
|
||||
fun isActive(): Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an active paid backup. Pricing comes from the subscription object.
|
||||
*/
|
||||
data class ActivePaid(
|
||||
override val messageBackupsType: MessageBackupsType.Paid,
|
||||
val price: FiatMoney,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime {
|
||||
override fun isActive(): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an active free backup.
|
||||
*/
|
||||
data class ActiveFree(
|
||||
override val messageBackupsType: MessageBackupsType.Free,
|
||||
override val renewalTime: Duration = 0.seconds
|
||||
) : WithTypeAndRenewalTime {
|
||||
override fun isActive(): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* User has an inactive backup
|
||||
*/
|
||||
data class Inactive(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration = 0.seconds
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* User has an active backup but no active subscription
|
||||
*/
|
||||
data object NotFound : BackupState
|
||||
|
||||
/**
|
||||
* User has a canceled paid tier backup
|
||||
*/
|
||||
data class Canceled(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* Subscription mismatch detected.
|
||||
*/
|
||||
data class SubscriptionMismatchMissingGooglePlay(
|
||||
override val messageBackupsType: MessageBackupsType,
|
||||
override val renewalTime: Duration
|
||||
) : WithTypeAndRenewalTime
|
||||
|
||||
/**
|
||||
* An error occurred retrieving the network state
|
||||
*/
|
||||
data object Error : BackupState
|
||||
}
|
||||
|
||||
enum class Dialog {
|
||||
NONE,
|
||||
TURN_OFF_AND_DELETE_BACKUPS,
|
||||
|
||||
@@ -20,11 +20,8 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.throttleLatest
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||
@@ -35,25 +32,17 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.attachmentUpdates
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -233,8 +222,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||
val tier = SignalStore.backup.latestBackupTier
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupsEnabled = SignalStore.backup.areBackupsEnabled,
|
||||
@@ -243,7 +230,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
||||
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfStorageSpaceUx()
|
||||
isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfStorageSpaceUx(),
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,264 +246,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPurchase?.state == InAppPaymentTable.State.PENDING) {
|
||||
Log.d(TAG, "We have a pending subscription.")
|
||||
val state = BackupStateRepository.resolveBackupState(lastPurchase)
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Pending(
|
||||
price = lastPurchase.data.amount!!.toFiatMoney()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.backup.subscriptionStateMismatchDetected) {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.")
|
||||
|
||||
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
|
||||
is BillingPurchaseResult.Success -> {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] Found a purchase: $purchaseResult")
|
||||
purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] No purchase found in Google Play Billing: $purchaseResult")
|
||||
false
|
||||
}
|
||||
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
|
||||
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
|
||||
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
|
||||
}
|
||||
|
||||
val hasActiveSignalSubscription = activeSubscription?.isActive == true
|
||||
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription")
|
||||
|
||||
when {
|
||||
hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
|
||||
val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription)
|
||||
|
||||
if (type == null) {
|
||||
Log.d(TAG, "[subscriptionMismatchDetected] failed to load backup configuration. Likely a network error.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Error
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||
messageBackupsType = type,
|
||||
renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
|
||||
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true")
|
||||
return
|
||||
it.copy(backupState = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (tier) {
|
||||
MessageBackupTier.PAID -> {
|
||||
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.")
|
||||
|
||||
val type = withContext(Dispatchers.IO) {
|
||||
BackupRepository.getBackupsType(tier) as? MessageBackupsType.Paid
|
||||
}
|
||||
|
||||
Log.d(TAG, "Attempting to retrieve current subscription...")
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
if (activeSubscription.isSuccess) {
|
||||
Log.d(TAG, "Retrieved subscription details.")
|
||||
|
||||
val subscription = activeSubscription.getOrThrow().activeSubscription
|
||||
if (subscription != null) {
|
||||
Log.d(TAG, "Subscription found. Updating UI state with subscription details. Status: ${subscription.status}")
|
||||
|
||||
val subscriberType = type ?: buildPaidTypeFromSubscription(subscription)
|
||||
if (subscriberType == null) {
|
||||
Log.d(TAG, "Failed to create backup type. Possible network error.")
|
||||
_state.update {
|
||||
it.copy(backupState = RemoteBackupsSettingsState.BackupState.Error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409",
|
||||
backupState = when {
|
||||
subscription.isCanceled && subscription.isActive -> RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
messageBackupsType = subscriberType,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
subscription.isActive -> RemoteBackupsSettingsState.BackupState.ActivePaid(
|
||||
messageBackupsType = subscriberType,
|
||||
price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)),
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
else -> RemoteBackupsSettingsState.BackupState.Inactive(
|
||||
messageBackupsType = subscriberType,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ActiveSubscription had null subscription object.")
|
||||
if (SignalStore.backup.areBackupsEnabled) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.NotFound
|
||||
)
|
||||
}
|
||||
} else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) {
|
||||
val canceledType = type ?: buildPaidTypeFromInAppPayment(lastPurchase)
|
||||
if (canceledType == null) {
|
||||
Log.w(TAG, "Failed to load canceled type information. Possible network error.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Error
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
messageBackupsType = canceledType,
|
||||
renewalTime = lastPurchase.endOfPeriod
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val inactiveType = type ?: buildPaidTypeWithoutPricing()
|
||||
if (inactiveType == null) {
|
||||
Log.w(TAG, "Failed to load inactive type information. Possible network error.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Error
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Inactive(
|
||||
messageBackupsType = inactiveType,
|
||||
renewalTime = lastPurchase?.endOfPeriod ?: 0.seconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageBackupTier.FREE -> {
|
||||
val type = withContext(Dispatchers.IO) {
|
||||
BackupRepository.getBackupsType(tier) as MessageBackupsType.Free
|
||||
}
|
||||
|
||||
val backupState = if (SignalStore.backup.areBackupsEnabled) {
|
||||
RemoteBackupsSettingsState.BackupState.ActiveFree(type)
|
||||
} else {
|
||||
RemoteBackupsSettingsState.BackupState.Inactive(type)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating UI state with $backupState FREE tier.")
|
||||
_state.update { it.copy(backupState = backupState) }
|
||||
}
|
||||
|
||||
null -> {
|
||||
Log.d(TAG, "Updating UI state with NONE null tier.")
|
||||
_state.update { it.copy(backupState = RemoteBackupsSettingsState.BackupState.None) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out a Paid type utilizing pricing information stored in the user's active subscription object.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeFromSubscription(subscription: ActiveSubscription.Subscription): MessageBackupsType.Paid? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
val price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = price,
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds out a Paid type utilizing pricing information stored in the given in-app payment.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeFromInAppPayment(inAppPayment: InAppPaymentTable.InAppPayment): MessageBackupsType.Paid? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
val price = inAppPayment.data.amount!!.toFiatMoney()
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = price,
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* In the case of an Inactive subscription, we only care about the storage allowance and TTL, both of which we can
|
||||
* grab from the backup level configuration.
|
||||
*
|
||||
* @return A paid type, or null if we were unable to get the backup level configuration.
|
||||
*/
|
||||
private fun buildPaidTypeWithoutPricing(): MessageBackupsType? {
|
||||
val config = BackupRepository.getBackupLevelConfiguration() ?: return null
|
||||
|
||||
return MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault())),
|
||||
storageAllowanceBytes = config.storageAllowanceBytes,
|
||||
mediaTtl = config.mediaTtlDays.days
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8067,6 +8067,8 @@
|
||||
<string name="BackupsSettingsFragment_last_backup_s">Last backup %1$s</string>
|
||||
<!-- Subtitle for row for no backup ever created -->
|
||||
<string name="BackupsSettingsFragment_automatic_backups_with_signals">Automatic backups with Signal\'s secure end-to-end encrypted storage service.</string>
|
||||
<!-- Subtitle for row for backups that are active but subscription not found -->
|
||||
<string name="BackupsSettingsFragment_subscription_not_found_on_this_device">"Subscription not found on this device."</string>
|
||||
<!-- Action button label to set up backups -->
|
||||
<string name="BackupsSettingsFragment_set_up">Set up</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user