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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
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.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -59,6 +61,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import org.signal.core.ui.R as CoreUiR
|
import org.signal.core.ui.R as CoreUiR
|
||||||
@@ -91,16 +94,16 @@ class BackupsSettingsFragment : ComposeFragment() {
|
|||||||
backupsSettingsState = state,
|
backupsSettingsState = state,
|
||||||
onNavigationClick = { requireActivity().onNavigateUp() },
|
onNavigationClick = { requireActivity().onNavigateUp() },
|
||||||
onBackupsRowClick = {
|
onBackupsRowClick = {
|
||||||
when (state.enabledState) {
|
when (state.backupState) {
|
||||||
is BackupsSettingsState.EnabledState.Active, BackupsSettingsState.EnabledState.Inactive -> {
|
BackupState.Loading, BackupState.Error, BackupState.NotAvailable -> Unit
|
||||||
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
BackupsSettingsState.EnabledState.Never -> {
|
BackupState.None -> {
|
||||||
checkoutLauncher.launch(null)
|
checkoutLauncher.launch(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> {
|
||||||
|
findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) },
|
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) },
|
||||||
@@ -152,14 +155,14 @@ private fun BackupsSettingsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
when (backupsSettingsState.enabledState) {
|
when (backupsSettingsState.backupState) {
|
||||||
BackupsSettingsState.EnabledState.Loading -> {
|
BackupState.Loading -> {
|
||||||
LoadingBackupsRow()
|
LoadingBackupsRow()
|
||||||
|
|
||||||
OtherWaysToBackUpHeading()
|
OtherWaysToBackUpHeading()
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupsSettingsState.EnabledState.Inactive -> {
|
is BackupState.Inactive -> {
|
||||||
InactiveBackupsRow(
|
InactiveBackupsRow(
|
||||||
onBackupsRowClick = onBackupsRowClick
|
onBackupsRowClick = onBackupsRowClick
|
||||||
)
|
)
|
||||||
@@ -167,16 +170,17 @@ private fun BackupsSettingsContent(
|
|||||||
OtherWaysToBackUpHeading()
|
OtherWaysToBackUpHeading()
|
||||||
}
|
}
|
||||||
|
|
||||||
is BackupsSettingsState.EnabledState.Active -> {
|
is BackupState.ActiveFree, is BackupState.ActivePaid -> {
|
||||||
ActiveBackupsRow(
|
ActiveBackupsRow(
|
||||||
enabledState = backupsSettingsState.enabledState,
|
backupState = backupsSettingsState.backupState,
|
||||||
onBackupsRowClick = onBackupsRowClick
|
onBackupsRowClick = onBackupsRowClick,
|
||||||
|
lastBackupAt = backupsSettingsState.lastBackupAt
|
||||||
)
|
)
|
||||||
|
|
||||||
OtherWaysToBackUpHeading()
|
OtherWaysToBackUpHeading()
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupsSettingsState.EnabledState.Never -> {
|
BackupState.None -> {
|
||||||
NeverEnabledBackupsRow(
|
NeverEnabledBackupsRow(
|
||||||
onBackupsRowClick = onBackupsRowClick
|
onBackupsRowClick = onBackupsRowClick
|
||||||
)
|
)
|
||||||
@@ -184,12 +188,46 @@ private fun BackupsSettingsContent(
|
|||||||
OtherWaysToBackUpHeading()
|
OtherWaysToBackUpHeading()
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupsSettingsState.EnabledState.Failed -> {
|
BackupState.Error -> {
|
||||||
WaitingForNetworkRow()
|
WaitingForNetworkRow()
|
||||||
OtherWaysToBackUpHeading()
|
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
|
@Composable
|
||||||
private fun ActiveBackupsRow(
|
private fun ActiveBackupsRow(
|
||||||
enabledState: BackupsSettingsState.EnabledState.Active,
|
backupState: BackupState.WithTypeAndRenewalTime,
|
||||||
|
lastBackupAt: Duration,
|
||||||
onBackupsRowClick: () -> Unit = {}
|
onBackupsRowClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Rows.TextRow(
|
Rows.TextRow(
|
||||||
@@ -316,13 +420,13 @@ private fun ActiveBackupsRow(
|
|||||||
Column {
|
Column {
|
||||||
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
|
||||||
|
|
||||||
when (enabledState.type) {
|
when (val type = backupState.messageBackupsType) {
|
||||||
is MessageBackupsType.Paid -> {
|
is MessageBackupsType.Paid -> {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
R.string.BackupsSettingsFragment_s_month_renews_s,
|
R.string.BackupsSettingsFragment_s_month_renews_s,
|
||||||
FiatMoneyUtil.format(LocalContext.current.resources, enabledState.type.pricePerMonth),
|
FiatMoneyUtil.format(LocalContext.current.resources, type.pricePerMonth),
|
||||||
DateUtils.formatDateWithYear(Locale.getDefault(), enabledState.expiresAt.inWholeMilliseconds)
|
DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
@@ -346,7 +450,7 @@ private fun ActiveBackupsRow(
|
|||||||
DateUtils.getDatelessRelativeTimeSpanFormattedDate(
|
DateUtils.getDatelessRelativeTimeSpanFormattedDate(
|
||||||
LocalContext.current,
|
LocalContext.current,
|
||||||
Locale.getDefault(),
|
Locale.getDefault(),
|
||||||
enabledState.lastBackupAt.inWholeMilliseconds
|
lastBackupAt.inWholeMilliseconds
|
||||||
).value
|
).value
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -404,14 +508,14 @@ private fun BackupsSettingsContentPreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupsSettingsContent(
|
BackupsSettingsContent(
|
||||||
backupsSettingsState = BackupsSettingsState(
|
backupsSettingsState = BackupsSettingsState(
|
||||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
backupState = BackupState.ActivePaid(
|
||||||
type = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 1_000_000,
|
storageAllowanceBytes = 1_000_000,
|
||||||
mediaTtl = 30.days
|
mediaTtl = 30.days
|
||||||
),
|
),
|
||||||
expiresAt = 0.seconds,
|
renewalTime = 0.seconds,
|
||||||
lastBackupAt = 0.seconds
|
price = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -424,7 +528,7 @@ private fun BackupsSettingsContentNotAvailablePreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupsSettingsContent(
|
BackupsSettingsContent(
|
||||||
backupsSettingsState = BackupsSettingsState(
|
backupsSettingsState = BackupsSettingsState(
|
||||||
enabledState = BackupsSettingsState.EnabledState.NotAvailable
|
backupState = BackupState.NotAvailable
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -436,7 +540,7 @@ private fun BackupsSettingsContentBackupTierInternalOverridePreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupsSettingsContent(
|
BackupsSettingsContent(
|
||||||
backupsSettingsState = BackupsSettingsState(
|
backupsSettingsState = BackupsSettingsState(
|
||||||
enabledState = BackupsSettingsState.EnabledState.Never,
|
backupState = BackupState.None,
|
||||||
showBackupTierInternalOverride = true,
|
showBackupTierInternalOverride = true,
|
||||||
backupTierInternalOverride = null
|
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
|
@SignalPreview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActivePaidBackupsRowPreview() {
|
private fun ActivePaidBackupsRowPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
ActiveBackupsRow(
|
ActiveBackupsRow(
|
||||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
backupState = BackupState.ActivePaid(
|
||||||
type = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 1_000_000,
|
storageAllowanceBytes = 1_000_000,
|
||||||
mediaTtl = 30.days
|
mediaTtl = 30.days
|
||||||
),
|
),
|
||||||
expiresAt = 0.seconds,
|
renewalTime = 0.seconds,
|
||||||
|
price = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD"))
|
||||||
|
),
|
||||||
lastBackupAt = 0.seconds
|
lastBackupAt = 0.seconds
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,14 +604,14 @@ private fun ActivePaidBackupsRowPreview() {
|
|||||||
private fun ActiveFreeBackupsRowPreview() {
|
private fun ActiveFreeBackupsRowPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
ActiveBackupsRow(
|
ActiveBackupsRow(
|
||||||
enabledState = BackupsSettingsState.EnabledState.Active(
|
backupState = BackupState.ActiveFree(
|
||||||
type = MessageBackupsType.Free(
|
messageBackupsType = MessageBackupsType.Free(
|
||||||
mediaRetentionDays = 30
|
mediaRetentionDays = 30
|
||||||
),
|
),
|
||||||
expiresAt = 0.seconds,
|
renewalTime = 0.seconds
|
||||||
|
),
|
||||||
lastBackupAt = 0.seconds
|
lastBackupAt = 0.seconds
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,49 +6,16 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
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
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen state for top-level backups settings screen.
|
* Screen state for top-level backups settings screen.
|
||||||
*/
|
*/
|
||||||
data class BackupsSettingsState(
|
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 showBackupTierInternalOverride: Boolean = false,
|
||||||
val backupTierInternalOverride: MessageBackupTier? = null
|
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 kotlinx.coroutines.rx3.asFlow
|
||||||
import org.signal.core.util.concurrent.SignalDispatchers
|
import org.signal.core.util.concurrent.SignalDispatchers
|
||||||
import org.signal.core.util.logging.Log
|
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.DeletionState
|
||||||
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.backup.v2.ui.subscription.MessageBackupsType
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.util.Environment
|
import org.thoughtcrime.securesms.util.Environment
|
||||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
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.milliseconds
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
class BackupsSettingsViewModel : ViewModel() {
|
class BackupsSettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
@@ -56,7 +51,7 @@ class BackupsSettingsViewModel : ViewModel() {
|
|||||||
.drop(1)
|
.drop(1)
|
||||||
.collect {
|
.collect {
|
||||||
Log.d(TAG, "Triggering refresh from internet reconnect.")
|
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.")
|
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() {
|
override fun onCleared() {
|
||||||
@@ -75,7 +77,9 @@ class BackupsSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun refreshState() {
|
fun refreshState() {
|
||||||
Log.d(TAG, "Refreshing state from manual call.")
|
Log.d(TAG, "Refreshing state from manual call.")
|
||||||
loadRequests.tryEmit(Unit)
|
viewModelScope.launch(SignalDispatchers.Default) {
|
||||||
|
loadRequests.emit(Unit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@@ -83,18 +87,16 @@ class BackupsSettingsViewModel : ViewModel() {
|
|||||||
return viewModelScope.launch(SignalDispatchers.IO) {
|
return viewModelScope.launch(SignalDispatchers.IO) {
|
||||||
if (!RemoteConfig.messageBackups) {
|
if (!RemoteConfig.messageBackups) {
|
||||||
Log.w(TAG, "Remote backups are not available on this device.")
|
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 {
|
} else {
|
||||||
val enabledState = when (SignalStore.backup.backupTier) {
|
val latestPurchase = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
|
||||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
val enabledState = BackupStateRepository.resolveBackupState(latestPurchase)
|
||||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
|
||||||
null -> getEnabledStateForNoTier()
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Found enabled state $enabledState. Updating UI state.")
|
Log.d(TAG, "Found enabled state $enabledState. Updating UI state.")
|
||||||
internalStateFlow.update {
|
internalStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
enabledState = enabledState,
|
backupState = enabledState,
|
||||||
|
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
|
||||||
showBackupTierInternalOverride = RemoteConfig.internalUser || Environment.IS_STAGING,
|
showBackupTierInternalOverride = RemoteConfig.internalUser || Environment.IS_STAGING,
|
||||||
backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride
|
backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride
|
||||||
)
|
)
|
||||||
@@ -108,62 +110,4 @@ class BackupsSettingsViewModel : ViewModel() {
|
|||||||
SignalStore.backup.deletionState = DeletionState.NONE
|
SignalStore.backup.deletionState = DeletionState.NONE
|
||||||
refreshState()
|
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.BetaHeader
|
||||||
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
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.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
|
||||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||||
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
|
||||||
@@ -448,19 +449,19 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
|
|
||||||
item {
|
item {
|
||||||
when (state.backupState) {
|
when (state.backupState) {
|
||||||
is RemoteBackupsSettingsState.BackupState.Loading -> {
|
is BackupState.Loading -> {
|
||||||
LoadingCard()
|
LoadingCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.Error -> {
|
is BackupState.Error -> {
|
||||||
ErrorCard()
|
ErrorCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.Pending -> {
|
is BackupState.Pending -> {
|
||||||
PendingCard(state.backupState.price)
|
PendingCard(state.backupState.price)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
is BackupState.SubscriptionMismatchMissingGooglePlay -> {
|
||||||
SubscriptionMismatchMissingGooglePlayCard(
|
SubscriptionMismatchMissingGooglePlayCard(
|
||||||
state = state.backupState,
|
state = state.backupState,
|
||||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
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(
|
BackupCard(
|
||||||
backupState = state.backupState,
|
backupState = state.backupState,
|
||||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
|
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
|
||||||
@@ -479,7 +480,7 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteBackupsSettingsState.BackupState.NotFound -> {
|
BackupState.NotFound -> {
|
||||||
SubscriptionNotFoundCard(
|
SubscriptionNotFoundCard(
|
||||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
|
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
|
||||||
onRenewClick = contentCallbacks::onRenewLostSubscription,
|
onRenewClick = contentCallbacks::onRenewLostSubscription,
|
||||||
@@ -487,6 +488,8 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BackupState.NotAvailable -> error("This shouldn't happen on this screen.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +585,7 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
SkipDownloadDuringDeleteDialog()
|
SkipDownloadDuringDeleteDialog()
|
||||||
} else {
|
} else {
|
||||||
SkipDownloadDialog(
|
SkipDownloadDialog(
|
||||||
renewalTime = if (state.backupState is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime) {
|
renewalTime = if (state.backupState is BackupState.WithTypeAndRenewalTime) {
|
||||||
state.backupState.renewalTime
|
state.backupState.renewalTime
|
||||||
} else {
|
} else {
|
||||||
error("Unexpected dialog display without renewal time.")
|
error("Unexpected dialog display without renewal time.")
|
||||||
@@ -800,7 +803,7 @@ private fun DescriptionText(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListScope.appendBackupDetailsItems(
|
private fun LazyListScope.appendBackupDetailsItems(
|
||||||
backupState: RemoteBackupsSettingsState.BackupState,
|
backupState: BackupState,
|
||||||
canViewBackupKey: Boolean,
|
canViewBackupKey: Boolean,
|
||||||
backupRestoreState: BackupRestoreState,
|
backupRestoreState: BackupRestoreState,
|
||||||
backupProgress: ArchiveUploadProgressState?,
|
backupProgress: ArchiveUploadProgressState?,
|
||||||
@@ -853,7 +856,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backupState !is RemoteBackupsSettingsState.BackupState.ActiveFree) {
|
if (backupState !is BackupState.ActiveFree) {
|
||||||
item {
|
item {
|
||||||
Rows.TextRow(text = {
|
Rows.TextRow(text = {
|
||||||
Column {
|
Column {
|
||||||
@@ -923,7 +926,7 @@ private fun LazyListScope.appendBackupDetailsItems(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BackupCard(
|
private fun BackupCard(
|
||||||
backupState: RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime,
|
backupState: BackupState.WithTypeAndRenewalTime,
|
||||||
buttonsEnabled: Boolean,
|
buttonsEnabled: Boolean,
|
||||||
onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {}
|
onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
@@ -950,21 +953,21 @@ private fun BackupCard(
|
|||||||
)
|
)
|
||||||
|
|
||||||
when (backupState) {
|
when (backupState) {
|
||||||
is RemoteBackupsSettingsState.BackupState.ActivePaid -> {
|
is BackupState.ActivePaid -> {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, backupState.price)),
|
text = stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, backupState.price)),
|
||||||
modifier = Modifier.padding(top = 12.dp)
|
modifier = Modifier.padding(top = 12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.ActiveFree -> {
|
is BackupState.ActiveFree -> {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free),
|
text = stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free),
|
||||||
modifier = Modifier.padding(top = 12.dp)
|
modifier = Modifier.padding(top = 12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RemoteBackupsSettingsState.BackupState.Inactive -> {
|
is BackupState.Inactive -> {
|
||||||
val text = when (messageBackupsType) {
|
val text = when (messageBackupsType) {
|
||||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive)
|
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive)
|
||||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__you_turned_off_backups)
|
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(
|
||||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_cancelled),
|
text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_cancelled),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
@@ -992,9 +995,9 @@ private fun BackupCard(
|
|||||||
|
|
||||||
if (messageBackupsType is MessageBackupsType.Paid) {
|
if (messageBackupsType is MessageBackupsType.Paid) {
|
||||||
val resource = when (backupState) {
|
val resource = when (backupState) {
|
||||||
is RemoteBackupsSettingsState.BackupState.ActivePaid -> R.string.RemoteBackupsSettingsFragment__renews_s
|
is BackupState.ActivePaid -> R.string.RemoteBackupsSettingsFragment__renews_s
|
||||||
is RemoteBackupsSettingsState.BackupState.Inactive -> R.string.RemoteBackupsSettingsFragment__expired_on_s
|
is BackupState.Inactive -> R.string.RemoteBackupsSettingsFragment__expired_on_s
|
||||||
is RemoteBackupsSettingsState.BackupState.Canceled -> R.string.RemoteBackupsSettingsFragment__expires_on_s
|
is BackupState.Canceled -> R.string.RemoteBackupsSettingsFragment__expires_on_s
|
||||||
else -> error("Not supported here.")
|
else -> error("Not supported here.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,7 +1029,7 @@ private fun BackupCard(
|
|||||||
enabled = buttonsEnabled,
|
enabled = buttonsEnabled,
|
||||||
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }
|
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }
|
||||||
)
|
)
|
||||||
} else if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
|
} else if (backupState is BackupState.Canceled) {
|
||||||
CallToActionButton(
|
CallToActionButton(
|
||||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resubscribe),
|
text = stringResource(R.string.RemoteBackupsSettingsFragment__resubscribe),
|
||||||
enabled = buttonsEnabled,
|
enabled = buttonsEnabled,
|
||||||
@@ -1288,7 +1291,7 @@ private fun SubscriptionNotFoundCard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SubscriptionMismatchMissingGooglePlayCard(
|
private fun SubscriptionMismatchMissingGooglePlayCard(
|
||||||
state: RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay,
|
state: BackupState.SubscriptionMismatchMissingGooglePlay,
|
||||||
isRenewEnabled: Boolean,
|
isRenewEnabled: Boolean,
|
||||||
onRenewClick: () -> Unit = {},
|
onRenewClick: () -> Unit = {},
|
||||||
onLearnMoreClick: () -> Unit = {}
|
onLearnMoreClick: () -> Unit = {}
|
||||||
@@ -1662,10 +1665,10 @@ private fun BackupFrequencyDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun BackupReadyToDownloadRow(
|
private fun BackupReadyToDownloadRow(
|
||||||
ready: BackupRestoreState.Ready,
|
ready: BackupRestoreState.Ready,
|
||||||
backupState: RemoteBackupsSettingsState.BackupState,
|
backupState: BackupState,
|
||||||
onDownloadClick: () -> Unit = {}
|
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)
|
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, ready.bytes)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
|
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,
|
dialog = RemoteBackupsSettingsState.Dialog.NONE,
|
||||||
snackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
snackbar = RemoteBackupsSettingsState.Snackbar.NONE,
|
||||||
backupMediaSize = 2300000,
|
backupMediaSize = 2300000,
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
|
backupState = BackupState.ActiveFree(
|
||||||
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30)
|
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30)
|
||||||
),
|
),
|
||||||
hasRedemptionError = true,
|
hasRedemptionError = true,
|
||||||
@@ -1784,7 +1787,7 @@ private fun SubscriptionNotFoundCardPreview() {
|
|||||||
private fun SubscriptionMismatchMissingGooglePlayCardPreview() {
|
private fun SubscriptionMismatchMissingGooglePlayCardPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
SubscriptionMismatchMissingGooglePlayCard(
|
SubscriptionMismatchMissingGooglePlayCard(
|
||||||
state = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
|
state = BackupState.SubscriptionMismatchMissingGooglePlay(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 100_000_000,
|
storageAllowanceBytes = 100_000_000,
|
||||||
@@ -1803,7 +1806,7 @@ private fun BackupCardPreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
Column {
|
Column {
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.ActivePaid(
|
backupState = BackupState.ActivePaid(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 100_000_000,
|
storageAllowanceBytes = 100_000_000,
|
||||||
@@ -1816,7 +1819,7 @@ private fun BackupCardPreview() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
backupState = BackupState.Canceled(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 100_000_000,
|
storageAllowanceBytes = 100_000_000,
|
||||||
@@ -1828,7 +1831,7 @@ private fun BackupCardPreview() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.Inactive(
|
backupState = BackupState.Inactive(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 100_000_000,
|
storageAllowanceBytes = 100_000_000,
|
||||||
@@ -1840,7 +1843,7 @@ private fun BackupCardPreview() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.ActivePaid(
|
backupState = BackupState.ActivePaid(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||||
storageAllowanceBytes = 100_000_000,
|
storageAllowanceBytes = 100_000_000,
|
||||||
@@ -1853,7 +1856,7 @@ private fun BackupCardPreview() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
BackupCard(
|
BackupCard(
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
|
backupState = BackupState.ActiveFree(
|
||||||
messageBackupsType = MessageBackupsType.Free(
|
messageBackupsType = MessageBackupsType.Free(
|
||||||
mediaRetentionDays = 30
|
mediaRetentionDays = 30
|
||||||
)
|
)
|
||||||
@@ -1870,7 +1873,7 @@ private fun BackupReadyToDownloadPreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupReadyToDownloadRow(
|
BackupReadyToDownloadRow(
|
||||||
ready = BackupRestoreState.Ready("12GB"),
|
ready = BackupRestoreState.Ready("12GB"),
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.None
|
backupState = BackupState.None
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1881,7 +1884,7 @@ private fun BackupReadyToDownloadAfterCancelPreview() {
|
|||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BackupReadyToDownloadRow(
|
BackupReadyToDownloadRow(
|
||||||
ready = BackupRestoreState.Ready("12GB"),
|
ready = BackupRestoreState.Ready("12GB"),
|
||||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
backupState = BackupState.Canceled(
|
||||||
messageBackupsType = MessageBackupsType.Paid(
|
messageBackupsType = MessageBackupsType.Paid(
|
||||||
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
|
||||||
storageAllowanceBytes = 10.gibiBytes.bytes,
|
storageAllowanceBytes = 10.gibiBytes.bytes,
|
||||||
|
|||||||
@@ -5,11 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
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.BackupFrequency
|
||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
data class RemoteBackupsSettingsState(
|
data class RemoteBackupsSettingsState(
|
||||||
val backupsEnabled: Boolean,
|
val backupsEnabled: Boolean,
|
||||||
@@ -27,94 +24,6 @@ data class RemoteBackupsSettingsState(
|
|||||||
val snackbar: Snackbar = Snackbar.NONE
|
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 {
|
enum class Dialog {
|
||||||
NONE,
|
NONE,
|
||||||
TURN_OFF_AND_DELETE_BACKUPS,
|
TURN_OFF_AND_DELETE_BACKUPS,
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.reactive.asFlow
|
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.bytes
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.money.FiatMoney
|
|
||||||
import org.signal.core.util.throttleLatest
|
import org.signal.core.util.throttleLatest
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
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.status.BackupStatusData
|
||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
|
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.InAppPaymentsRepository
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
|
||||||
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.attachmentUpdates
|
import org.thoughtcrime.securesms.database.attachmentUpdates
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
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
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,8 +222,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||||
val tier = SignalStore.backup.latestBackupTier
|
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
backupsEnabled = SignalStore.backup.areBackupsEnabled,
|
backupsEnabled = SignalStore.backup.areBackupsEnabled,
|
||||||
@@ -243,7 +230,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
canBackUpUsingCellular = SignalStore.backup.backupWithCellular,
|
||||||
canRestoreUsingCellular = SignalStore.backup.restoreWithCellular,
|
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) {
|
val state = BackupStateRepository.resolveBackupState(lastPurchase)
|
||||||
Log.d(TAG, "We have a pending subscription.")
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(backupState = state)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
<string name="BackupsSettingsFragment_last_backup_s">Last backup %1$s</string>
|
||||||
<!-- Subtitle for row for no backup ever created -->
|
<!-- 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>
|
<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 -->
|
<!-- Action button label to set up backups -->
|
||||||
<string name="BackupsSettingsFragment_set_up">Set up</string>
|
<string name="BackupsSettingsFragment_set_up">Set up</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user