Update remote backups settings to align with design.
This commit is contained in:
parent
6a77631b09
commit
b073005ff9
7 changed files with 516 additions and 157 deletions
|
@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
|||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
|
@ -73,6 +74,7 @@ import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
|
@ -113,9 +115,24 @@ object BackupRepository {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
fun turnOffAndDeleteBackup() {
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
SignalStore.backup.disableBackups()
|
||||
fun turnOffAndDeleteBackup(): Boolean {
|
||||
return try {
|
||||
Log.d(TAG, "Attempting to disable backups.")
|
||||
getBackupTier().runIfSuccessful { tier ->
|
||||
if (tier == MessageBackupTier.PAID) {
|
||||
Log.d(TAG, "User is currently on a paid tier. Canceling.")
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
Log.d(TAG, "Successfully canceled paid tier.")
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Disabling backups.")
|
||||
SignalStore.backup.disableBackups()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to turn off backups.", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSignalDatabaseSnapshot(baseName: String): SignalDatabase {
|
||||
|
@ -515,6 +532,25 @@ object BackupRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If backups are initialized, this method will query the server for the current backup level.
|
||||
* If backups are not initialized, this method will return either the stored tier or a 404 result.
|
||||
*/
|
||||
fun getBackupTier(): NetworkResult<MessageBackupTier> {
|
||||
return if (SignalStore.backup.backupsInitialized) {
|
||||
getBackupTier(Recipient.self().requireAci())
|
||||
} else if (SignalStore.backup.backupTier != null) {
|
||||
NetworkResult.Success(SignalStore.backup.backupTier!!)
|
||||
} else {
|
||||
NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(404))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the backup tier for the given ACI. Note that this will set the user's backup
|
||||
* tier to FREE if they are not on PAID, so avoid this method if you don't intend that
|
||||
* to be the case.
|
||||
*/
|
||||
private fun getBackupTier(aci: ACI): NetworkResult<MessageBackupTier> {
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
|
@ -66,6 +67,7 @@ import org.signal.core.ui.Scaffolds
|
|||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.Snackbars
|
||||
import org.signal.core.ui.Texts
|
||||
import org.signal.core.ui.horizontalGutters
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
|
@ -119,6 +121,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
val callbacks = remember { Callbacks() }
|
||||
|
||||
RemoteBackupsSettingsContent(
|
||||
backupsInitialized = state.backupsInitialized,
|
||||
messageBackupsType = state.messageBackupsType,
|
||||
lastBackupTimestamp = state.lastBackupTimestamp,
|
||||
canBackUpUsingCellular = state.canBackUpUsingCellular,
|
||||
|
@ -128,7 +131,8 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
contentCallbacks = callbacks,
|
||||
backupProgress = backupProgress,
|
||||
backupSize = state.backupSize,
|
||||
renewalTime = state.renewalTime
|
||||
renewalTime = state.renewalTime,
|
||||
backupState = state.backupState
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -145,6 +149,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onLaunchBackupsCheckoutFlow() {
|
||||
checkoutLauncher.launch(null)
|
||||
}
|
||||
|
||||
override fun onBackUpUsingCellularClick(canUseCellular: Boolean) {
|
||||
viewModel.setCanBackUpUsingCellular(canUseCellular)
|
||||
}
|
||||
|
@ -245,6 +253,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
*/
|
||||
private interface ContentCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun onLaunchBackupsCheckoutFlow() = Unit
|
||||
fun onBackupTypeActionClick(tier: MessageBackupTier) = Unit
|
||||
fun onBackUpUsingCellularClick(canUseCellular: Boolean) = Unit
|
||||
fun onBackupNowClick() = Unit
|
||||
|
@ -259,7 +268,9 @@ private interface ContentCallbacks {
|
|||
|
||||
@Composable
|
||||
private fun RemoteBackupsSettingsContent(
|
||||
backupsInitialized: Boolean,
|
||||
messageBackupsType: MessageBackupsType?,
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
renewalTime: Duration,
|
||||
lastBackupTimestamp: Long,
|
||||
canBackUpUsingCellular: Boolean,
|
||||
|
@ -286,101 +297,65 @@ private fun RemoteBackupsSettingsContent(
|
|||
modifier = Modifier
|
||||
.padding(it)
|
||||
) {
|
||||
if (messageBackupsType != null) {
|
||||
if (backupState == RemoteBackupsSettingsState.BackupState.LOADING) {
|
||||
item {
|
||||
BackupTypeRow(
|
||||
LoadingCard()
|
||||
}
|
||||
} else if (backupState == RemoteBackupsSettingsState.BackupState.ERROR) {
|
||||
item {
|
||||
ErrorCard()
|
||||
}
|
||||
} else if (messageBackupsType != null) {
|
||||
item {
|
||||
BackupCard(
|
||||
messageBackupsType = messageBackupsType,
|
||||
renewalTime = renewalTime,
|
||||
backupState = backupState,
|
||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
if (backupsInitialized) {
|
||||
appendBackupDetailsItems(
|
||||
backupProgress = backupProgress,
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
backupSize = backupSize,
|
||||
backupsFrequency = backupsFrequency,
|
||||
canBackUpUsingCellular = canBackUpUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else {
|
||||
// TODO [backups] -- Download progress bar / state if required.
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
item {
|
||||
InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = Util.getPrettyFileSize(backupSize),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Buttons.LargePrimary(
|
||||
onClick = { contentCallbacks.onBackupTypeActionClick(MessageBackupTier.FREE) },
|
||||
modifier = Modifier.horizontalGutters()
|
||||
) {
|
||||
Text(text = stringResource(R.string.RemoteBackupsSettingsFragment__reenable_backups))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = getTextForFrequency(backupsFrequency = backupsFrequency),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = contentCallbacks::onChangeBackupFrequencyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__view_backup_key),
|
||||
onClick = contentCallbacks::onViewBackupKeyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (requestedDialog) {
|
||||
RemoteBackupsSettingsState.Dialog.NONE -> {}
|
||||
RemoteBackupsSettingsState.Dialog.TURN_OFF_FAILED -> {
|
||||
FailedToTurnOffBackupDialog(
|
||||
onDismiss = contentCallbacks::onDialogDismissed
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.Dialog.TURN_OFF_AND_DELETE_BACKUPS -> {
|
||||
TurnOffAndDeleteBackupsDialog(
|
||||
onConfirm = contentCallbacks::onTurnOffAndDeleteBackupsConfirm,
|
||||
|
@ -431,9 +406,104 @@ private fun RemoteBackupsSettingsContent(
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDetailsItems(
|
||||
backupProgress: ArchiveUploadProgressState?,
|
||||
lastBackupTimestamp: Long,
|
||||
backupSize: Long,
|
||||
backupsFrequency: BackupFrequency,
|
||||
canBackUpUsingCellular: Boolean,
|
||||
contentCallbacks: ContentCallbacks
|
||||
) {
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
|
||||
}
|
||||
|
||||
if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) {
|
||||
item {
|
||||
LastBackupRow(
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
onBackupNowClick = contentCallbacks::onBackupNowClick
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = Util.getPrettyFileSize(backupSize),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = getTextForFrequency(backupsFrequency = backupsFrequency),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = contentCallbacks::onChangeBackupFrequencyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = canBackUpUsingCellular,
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular),
|
||||
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__view_backup_key),
|
||||
onClick = contentCallbacks::onViewBackupKeyClick
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup),
|
||||
foregroundTint = MaterialTheme.colorScheme.error,
|
||||
onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackupTypeRow(
|
||||
private fun BackupCard(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
renewalTime: Duration,
|
||||
onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {}
|
||||
) {
|
||||
|
@ -453,28 +523,63 @@ private fun BackupTypeRow(
|
|||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK)
|
||||
append(" ")
|
||||
if (backupState == RemoteBackupsSettingsState.BackupState.ACTIVE) {
|
||||
SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK)
|
||||
append(" ")
|
||||
}
|
||||
|
||||
append(title)
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
val cost = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth))
|
||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free)
|
||||
when (backupState) {
|
||||
RemoteBackupsSettingsState.BackupState.ACTIVE -> {
|
||||
val cost = when (messageBackupsType) {
|
||||
is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth))
|
||||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = cost,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.BackupState.INACTIVE -> {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
RemoteBackupsSettingsState.BackupState.CANCELED -> {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_cancelled),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
else -> error("Not supported here.")
|
||||
}
|
||||
|
||||
Text(
|
||||
text = cost,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
|
||||
if (messageBackupsType is MessageBackupsType.Paid) {
|
||||
@Suppress("KotlinConstantConditions")
|
||||
val resource = when (backupState) {
|
||||
RemoteBackupsSettingsState.BackupState.ACTIVE -> R.string.RemoteBackupsSettingsFragment__renews_s
|
||||
RemoteBackupsSettingsState.BackupState.INACTIVE -> R.string.RemoteBackupsSettingsFragment__expired_on_s
|
||||
RemoteBackupsSettingsState.BackupState.CANCELED -> R.string.RemoteBackupsSettingsFragment__expires_on_s
|
||||
else -> error("Not supported here.")
|
||||
}
|
||||
|
||||
if (renewalTime > 0.seconds) {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__renews_s, DateUtils.formatDateWithYear(Locale.getDefault(), renewalTime.inWholeMilliseconds))
|
||||
text = stringResource(resource, DateUtils.formatDateWithYear(Locale.getDefault(), renewalTime.inWholeMilliseconds))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -494,21 +599,52 @@ private fun BackupTypeRow(
|
|||
is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__upgrade)
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) },
|
||||
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
||||
containerColor = SignalTheme.colors.colorTransparent5,
|
||||
contentColor = colorResource(R.color.signal_light_colorOnSurface)
|
||||
),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = buttonText
|
||||
)
|
||||
if (backupState == RemoteBackupsSettingsState.BackupState.ACTIVE) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) },
|
||||
colors = ButtonDefaults.filledTonalButtonColors().copy(
|
||||
containerColor = SignalTheme.colors.colorTransparent5,
|
||||
contentColor = colorResource(R.color.signal_light_colorOnSurface)
|
||||
),
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = buttonText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxCard(content: @Composable () -> Unit) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 150.dp)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp))
|
||||
.padding(24.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingCard() {
|
||||
BoxCard {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorCard() {
|
||||
BoxCard {
|
||||
Text(text = "Error") // TODO [alex] -- Finalized error card
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InProgressBackupRow(
|
||||
progress: Int?,
|
||||
|
@ -590,6 +726,19 @@ private fun LastBackupRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FailedToTurnOffBackupDialog(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = "TODO",
|
||||
body = "TODO",
|
||||
confirm = stringResource(id = android.R.string.ok),
|
||||
onConfirm = {},
|
||||
onDismiss = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnOffAndDeleteBackupsDialog(
|
||||
onConfirm: () -> Unit,
|
||||
|
@ -733,6 +882,7 @@ private fun getTextForFrequency(backupsFrequency: BackupFrequency): String {
|
|||
private fun RemoteBackupsSettingsContentPreview() {
|
||||
Previews.Preview {
|
||||
RemoteBackupsSettingsContent(
|
||||
backupsInitialized = true,
|
||||
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30),
|
||||
lastBackupTimestamp = -1,
|
||||
canBackUpUsingCellular = false,
|
||||
|
@ -742,28 +892,65 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||
contentCallbacks = object : ContentCallbacks {},
|
||||
backupProgress = null,
|
||||
renewalTime = 1727193018.seconds,
|
||||
backupSize = 2300000
|
||||
backupSize = 2300000,
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ACTIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupTypeRowPreview() {
|
||||
private fun LoadingCardPreview() {
|
||||
Previews.Preview {
|
||||
LoadingCard()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ErrorCardPreview() {
|
||||
Previews.Preview {
|
||||
ErrorCard()
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupCardPreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
BackupTypeRow(
|
||||
BackupCard(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000
|
||||
),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ACTIVE,
|
||||
renewalTime = 1727193018.seconds
|
||||
)
|
||||
|
||||
BackupTypeRow(
|
||||
BackupCard(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000
|
||||
),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.CANCELED,
|
||||
renewalTime = 1727193018.seconds
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
messageBackupsType = MessageBackupsType.Paid(
|
||||
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
|
||||
storageAllowanceBytes = 100_000_000
|
||||
),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.INACTIVE,
|
||||
renewalTime = 1727193018.seconds
|
||||
)
|
||||
|
||||
BackupCard(
|
||||
messageBackupsType = MessageBackupsType.Free(
|
||||
mediaRetentionDays = 30
|
||||
),
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ACTIVE,
|
||||
renewalTime = 0.seconds
|
||||
)
|
||||
}
|
||||
|
@ -789,6 +976,16 @@ private fun InProgressRowPreview() {
|
|||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun FailedToTurnOffBackupDialogPreview() {
|
||||
Previews.Preview {
|
||||
FailedToTurnOffBackupDialog(
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun TurnOffAndDeleteBackupsDialogPreview() {
|
||||
|
|
|
@ -11,8 +11,10 @@ import kotlin.time.Duration
|
|||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class RemoteBackupsSettingsState(
|
||||
val backupsInitialized: Boolean,
|
||||
val messageBackupsType: MessageBackupsType? = null,
|
||||
val canBackUpUsingCellular: Boolean = false,
|
||||
val backupState: BackupState = BackupState.LOADING,
|
||||
val backupSize: Long = 0,
|
||||
val backupsFrequency: BackupFrequency = BackupFrequency.DAILY,
|
||||
val lastBackupTimestamp: Long = 0,
|
||||
|
@ -20,12 +22,43 @@ data class RemoteBackupsSettingsState(
|
|||
val dialog: Dialog = Dialog.NONE,
|
||||
val snackbar: Snackbar = Snackbar.NONE
|
||||
) {
|
||||
/**
|
||||
* Describes the state of the user's selected backup tier.
|
||||
*/
|
||||
enum class BackupState {
|
||||
/**
|
||||
* The exact backup state is being loaded from the network.
|
||||
*/
|
||||
LOADING,
|
||||
|
||||
/**
|
||||
* User has an active backup
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* User has an inactive paid tier backup
|
||||
*/
|
||||
INACTIVE,
|
||||
|
||||
/**
|
||||
* User has a canceled paid tier backup
|
||||
*/
|
||||
CANCELED,
|
||||
|
||||
/**
|
||||
* An error occurred retrieving the network state
|
||||
*/
|
||||
ERROR
|
||||
}
|
||||
|
||||
enum class Dialog {
|
||||
NONE,
|
||||
TURN_OFF_AND_DELETE_BACKUPS,
|
||||
BACKUP_FREQUENCY,
|
||||
DELETING_BACKUP,
|
||||
BACKUP_DELETED
|
||||
BACKUP_DELETED,
|
||||
TURN_OFF_FAILED
|
||||
}
|
||||
|
||||
enum class Snackbar {
|
||||
|
|
|
@ -15,8 +15,10 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
|
@ -30,34 +32,24 @@ import kotlin.time.Duration.Companion.seconds
|
|||
* ViewModel for state management of RemoteBackupsSettingsFragment
|
||||
*/
|
||||
class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(RemoteBackupsSettingsFragment::class)
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
RemoteBackupsSettingsState(
|
||||
backupsInitialized = SignalStore.backup.backupsInitialized,
|
||||
messageBackupsType = null,
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
backupSize = SignalStore.backup.totalBackupSize,
|
||||
backupsFrequency = SignalStore.backup.backupFrequency
|
||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular
|
||||
)
|
||||
)
|
||||
|
||||
val state: StateFlow<RemoteBackupsSettingsState> = _state
|
||||
|
||||
init {
|
||||
refresh()
|
||||
|
||||
viewModelScope.launch {
|
||||
val activeSubscription = withContext(Dispatchers.IO) {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
}
|
||||
|
||||
if (activeSubscription.isSuccess) {
|
||||
val subscription = activeSubscription.getOrThrow().activeSubscription
|
||||
if (subscription != null) {
|
||||
_state.update { it.copy(renewalTime = subscription.endOfCurrentPeriod.seconds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
|
||||
SignalStore.backup.backupWithCellular = canBackUpUsingCellular
|
||||
_state.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) }
|
||||
|
@ -80,17 +72,85 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Attempting to synchronize backup tier from archive service.")
|
||||
|
||||
val backupTier = withContext(Dispatchers.IO) {
|
||||
BackupRepository.getBackupTier()
|
||||
}
|
||||
|
||||
backupTier.runIfSuccessful {
|
||||
Log.d(TAG, "Setting backup tier to $it")
|
||||
SignalStore.backup.backupTier = it
|
||||
}
|
||||
|
||||
val tier = SignalStore.backup.backupTier
|
||||
val backupType = if (tier != null) BackupRepository.getBackupsType(tier) else null
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupsInitialized = SignalStore.backup.backupsInitialized,
|
||||
messageBackupsType = backupType,
|
||||
backupState = RemoteBackupsSettingsState.BackupState.LOADING,
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
backupSize = SignalStore.backup.totalBackupSize,
|
||||
backupsFrequency = SignalStore.backup.backupFrequency
|
||||
backupsFrequency = SignalStore.backup.backupFrequency,
|
||||
canBackUpUsingCellular = SignalStore.backup.backupWithCellular
|
||||
)
|
||||
}
|
||||
|
||||
when (tier) {
|
||||
MessageBackupTier.PAID -> {
|
||||
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.")
|
||||
|
||||
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.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds,
|
||||
backupState = when {
|
||||
subscription.isActive -> RemoteBackupsSettingsState.BackupState.ACTIVE
|
||||
subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.CANCELED
|
||||
else -> RemoteBackupsSettingsState.BackupState.INACTIVE
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ActiveSubscription had null subscription object. Updating UI state with INACTIVE subscription.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
renewalTime = 0.seconds,
|
||||
backupState = RemoteBackupsSettingsState.BackupState.INACTIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.")
|
||||
_state.update {
|
||||
it.copy(
|
||||
renewalTime = 0.seconds,
|
||||
backupState = RemoteBackupsSettingsState.BackupState.ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageBackupTier.FREE -> {
|
||||
Log.d(TAG, "Updating UI state with ACTIVE FREE tier.")
|
||||
_state.update { it.copy(renewalTime = 0.seconds, backupState = RemoteBackupsSettingsState.BackupState.ACTIVE) }
|
||||
}
|
||||
null -> {
|
||||
Log.d(TAG, "Updating UI state with INACTIVE null tier.")
|
||||
_state.update { it.copy(renewalTime = 0.seconds, backupState = RemoteBackupsSettingsState.BackupState.INACTIVE) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,28 +158,23 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||
viewModelScope.launch {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.DELETING_BACKUP)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val succeeded = withContext(Dispatchers.IO) {
|
||||
BackupRepository.turnOffAndDeleteBackup()
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_DELETED)
|
||||
delay(2000.milliseconds)
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
|
||||
refresh()
|
||||
if (succeeded) {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_DELETED)
|
||||
delay(2000.milliseconds)
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
|
||||
refresh()
|
||||
} else {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_FAILED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshBackupState() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
lastBackupTimestamp = SignalStore.backup.lastBackupTime,
|
||||
backupSize = SignalStore.backup.totalBackupSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackupNowClick() {
|
||||
BackupMessagesJob.enqueue()
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting
|
|||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
|
@ -55,65 +56,70 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
|
|||
}
|
||||
|
||||
override suspend fun doRun(): Result {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
Log.i(TAG, "User is not registered. Exiting.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (!RemoteConfig.messageBackups) {
|
||||
Log.i(TAG, "Message backups are not enabled. Exiting.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (!SignalStore.backup.backupsInitialized) {
|
||||
Log.i(TAG, "Backups are not initialized on this device. Exiting.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (!AppDependencies.billingApi.isApiAvailable()) {
|
||||
Log.i(TAG, "Google Play Billing API is not available on this device. Exiting.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
BackupRepository.getBackupTier().runIfSuccessful {
|
||||
Log.i(TAG, "Successfully retrieved backup tier $it. Applying.")
|
||||
SignalStore.backup.backupTier = it
|
||||
}
|
||||
|
||||
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
|
||||
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
|
||||
|
||||
val subscriberId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
if (subscriberId == null && hasActivePurchase) {
|
||||
Log.w(TAG, "User has active Google Play Billing purchase but no subscriber id! User should cancel backup and resubscribe.")
|
||||
updateLocalState(null)
|
||||
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val tier = SignalStore.backup.backupTier
|
||||
if (subscriberId == null && tier == MessageBackupTier.PAID) {
|
||||
Log.w(TAG, "User has no subscriber id but PAID backup tier. Reverting to no backup tier and informing the user.")
|
||||
updateLocalState(null)
|
||||
Log.w(TAG, "User has no subscriber id but PAID backup tier. User will need to cancel and resubscribe.")
|
||||
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
|
||||
if (activeSubscription?.isActive == true && tier != MessageBackupTier.PAID) {
|
||||
Log.w(TAG, "User has an active subscription but no backup tier. Setting to PAID and enabling backups.")
|
||||
updateLocalState(MessageBackupTier.PAID)
|
||||
Log.w(TAG, "User has an active subscription but no backup tier.")
|
||||
// TODO [message-backups] Set UI flag hint here to launch error sheet?
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (activeSubscription?.isActive != true && tier == MessageBackupTier.PAID) {
|
||||
Log.w(TAG, "User subscription is inactive or does not exist. Clearing backup tier.")
|
||||
Log.w(TAG, "User subscription is inactive or does not exist. User will need to cancel and resubscribe.")
|
||||
// TODO [message-backups] Set UI hint?
|
||||
updateLocalState(null)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (activeSubscription?.isActive != true && hasActivePurchase) {
|
||||
Log.w(TAG, "User subscription is inactive but user has a recent purchase. Clearing backup tier.")
|
||||
Log.w(TAG, "User subscription is inactive but user has a recent purchase. User will need to cancel and resubscribe.")
|
||||
// TODO [message-backups] Set UI hint?
|
||||
updateLocalState(null)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun updateLocalState(backupTier: MessageBackupTier?) {
|
||||
synchronized(InAppPaymentSubscriberRecord.Type.BACKUP) {
|
||||
SignalStore.backup.backupTier = backupTier
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
|
|
@ -7550,11 +7550,13 @@
|
|||
<!-- Snackbar text displayed when backup type is downgraded -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_type_changed_and_subcription_deleted">Backup type changed and subscription cancelled</string>
|
||||
<!-- Snackbar text displayed when backup subscription is cancelled -->
|
||||
<string name="RemoteBackupsSettingsFragment__subscription_cancelled">Subscription cancelled</string>
|
||||
<string name="RemoteBackupsSettingsFragment__subscription_cancelled">Subscription canceled</string>
|
||||
<!-- Snackbar text displayed when backup is successfully downloaded -->
|
||||
<string name="RemoteBackupsSettingsFragment__download_complete">Download complete</string>
|
||||
<!-- Snackbar text displayed when backup will be created overnight -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_will_be_created_overnight">Backup will be created overnight.</string>
|
||||
<!-- Text displayed in card when subscription is inactive -->
|
||||
<string name="RemoteBackupsSettingsFragment__subscription_inactive">Subscription inactive</string>
|
||||
<!-- Title text in row detailing selected backup type -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_plan">Backup plan</string>
|
||||
<!-- Subtitle text in row detailing selected backup type displayed when backups are disabled -->
|
||||
|
@ -7563,8 +7565,12 @@
|
|||
<string name="RemoteBackupsSettingsFragment__s_per_month">%1$s/month</string>
|
||||
<!-- String for free backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__your_backup_plan_is_free">Your backup plan is free</string>
|
||||
<!-- Displays the date the subscription will renew -->
|
||||
<!-- Displays the date the subscription will renew. Placeholder is the date. -->
|
||||
<string name="RemoteBackupsSettingsFragment__renews_s">Renews %1$s</string>
|
||||
<!-- Displays the date the subscription will expire. Placeholder is the date. -->
|
||||
<string name="RemoteBackupsSettingsFragment__expires_on_s">Expires on %1$s</string>
|
||||
<!-- Displays the date the subscription expired. Placeholder is the date. -->
|
||||
<string name="RemoteBackupsSettingsFragment__expired_on_s">Expired on %1$s</string>
|
||||
<!-- Displayed at the top of the screen when no backup is currently enabled -->
|
||||
<string name="RemoteBackupsSettingsFragment__back_up_your_message_history">Back up your message history so you never lose data when you get a new phone or reinstall Signal.</string>
|
||||
<!-- Section header for other ways to back up -->
|
||||
|
@ -7609,6 +7615,10 @@
|
|||
<string name="RemoteBackupsSettingsFragment__manually_back_up">Manually back up</string>
|
||||
<!-- The body of an alert dialog shown when we detect the user may be confused by the lock screen -->
|
||||
<string name="PassphrasePromptActivity_help_prompt_body">Please enter your device pin, password or pattern.</string>
|
||||
<!-- Button label to re-enable backups (launches into checkout flow) -->
|
||||
<string name="RemoteBackupsSettingsFragment__reenable_backups">Re-enable backups</string>
|
||||
<!-- Notice displayed when backups have been disabled and deleted -->
|
||||
<string name="RemoteBackupsSettingsFragment__backups_have_been_turned_off">Backups have been turned off and your data has been deleted from Signal\'s secure storage service.</string>
|
||||
|
||||
<!-- MessageBackupsEducationScreen -->
|
||||
<!-- Screen subtitle underneath large headline title -->
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
||||
/**
|
||||
* Applies sensible horizontal padding to the given component.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.horizontalGutters(
|
||||
gutterSize: Dp = dimensionResource(R.dimen.core_ui__gutter)
|
||||
): Modifier {
|
||||
return padding(horizontal = gutterSize)
|
||||
}
|
Loading…
Add table
Reference in a new issue