From 5bdc7c2740a4a6b88da222a36f780f732403ef0b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 27 Sep 2024 11:04:57 -0300 Subject: [PATCH] Implement new top-level backups settings. --- .../securesms/backup/v2/BackupRepository.kt | 6 + .../MessageBackupsFlowViewModel.kt | 1 + .../settings/app/AppSettingsFragment.kt | 11 + .../app/backups/BackupsSettingsFragment.kt | 384 ++++++++++++++++++ .../app/backups/BackupsSettingsState.kt | 46 +++ .../app/backups/BackupsSettingsViewModel.kt | 116 ++++++ .../remote}/RemoteBackupsSettingsFragment.kt | 274 +++++++------ .../remote}/RemoteBackupsSettingsState.kt | 5 +- .../remote}/RemoteBackupsSettingsViewModel.kt | 18 +- .../type/BackupsTypeSettingsFragment.kt | 7 +- .../backups/type/BackupsTypeSettingsState.kt | 2 +- .../type/BackupsTypeSettingsViewModel.kt | 2 +- .../RemoteBackupsPaymentHistoryFragment.kt | 337 --------------- .../RemoteBackupsPaymentHistoryRepository.kt | 16 - .../RemoteBackupsPaymentHistoryState.kt | 17 - .../RemoteBackupsPaymentHistoryViewModel.kt | 40 -- .../RecurringInAppPaymentRepository.kt | 33 +- .../securesms/fonts/SignalSymbols.kt | 18 + .../jobs/InAppPaymentRedemptionJob.kt | 1 + .../securesms/keyvalue/InAppPaymentValues.kt | 1 + .../securesms/keyvalue/UiHintValues.java | 11 +- .../main/res/drawable/symbol_backup_24.xml | 12 + app/src/main/res/navigation/app_settings.xml | 38 +- .../app_settings_with_change_number.xml | 46 ++- app/src/main/res/values/strings.xml | 46 ++- 25 files changed, 870 insertions(+), 618 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats/backups => backups/remote}/RemoteBackupsSettingsFragment.kt (76%) rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats/backups => backups/remote}/RemoteBackupsSettingsState.kt (83%) rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats/backups => backups/remote}/RemoteBackupsSettingsViewModel.kt (80%) rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats => }/backups/type/BackupsTypeSettingsFragment.kt (95%) rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats => }/backups/type/BackupsTypeSettingsState.kt (85%) rename app/src/main/java/org/thoughtcrime/securesms/components/settings/app/{chats => }/backups/type/BackupsTypeSettingsViewModel.kt (95%) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryRepository.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryViewModel.kt create mode 100644 app/src/main/res/drawable/symbol_backup_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index c8e5263678..a52a3aa410 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -105,6 +105,7 @@ object BackupRepository { 403 -> { Log.w(TAG, "Received status 403. The user is not in the media tier. Updating local state.", error.exception) SignalStore.backup.backupTier = MessageBackupTier.FREE + SignalStore.uiHints.markHasEverEnabledRemoteBackups() // TODO [backup] If the user thought they were in media tier but aren't, feels like we should have a special UX flow for this? } } @@ -846,6 +847,11 @@ object BackupRepository { Log.i(TAG, "Could not retrieve backup tier.", e) null } + + if (SignalStore.backup.backupTier != null) { + SignalStore.uiHints.markHasEverEnabledRemoteBackups() + } + return SignalStore.backup.backupTier } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 69bb79b72c..0a13d5ffee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -182,6 +182,7 @@ class MessageBackupsFlowViewModel : ViewModel() { MessageBackupTier.FREE -> { SignalStore.backup.areBackupsEnabled = true SignalStore.backup.backupTier = MessageBackupTier.FREE + SignalStore.uiHints.markHasEverEnabledRemoteBackups() state.copy(stage = MessageBackupsStage.COMPLETED) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index d230b2409f..560e291f76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -201,6 +201,17 @@ class AppSettingsFragment : DSLSettingsFragment( isEnabled = state.isRegisteredAndUpToDate() ) + if (RemoteConfig.messageBackups) { + clickPref( + title = DSLSettingsText.from(R.string.preferences_chats__backups), + // TODO [message-backups] -- icon + onClick = { + findNavController().safeNavigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment) + }, + isEnabled = state.isRegisteredAndUpToDate() + ) + } + clickPref( title = DSLSettingsText.from(R.string.preferences__data_and_storage), icon = DSLSettingsIcon.from(R.drawable.symbol_data_24), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt new file mode 100644 index 0000000000..135ec715d3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt @@ -0,0 +1,384 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.backups + +import android.os.Bundle +import android.view.View +import androidx.activity.result.ActivityResultLauncher +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import org.signal.core.ui.Buttons +import org.signal.core.ui.Dividers +import org.signal.core.ui.Previews +import org.signal.core.ui.Rows +import org.signal.core.ui.Scaffolds +import org.signal.core.ui.SignalPreview +import org.signal.core.ui.Texts +import org.signal.core.util.money.FiatMoney +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType +import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher +import org.thoughtcrime.securesms.compose.ComposeFragment +import org.thoughtcrime.securesms.payments.FiatMoneyUtil +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.navigation.safeNavigate +import java.math.BigDecimal +import java.util.Currency +import java.util.Locale +import kotlin.time.Duration.Companion.seconds + +/** + * Top-level backups settings screen. + */ +class BackupsSettingsFragment : ComposeFragment() { + + private lateinit var checkoutLauncher: ActivityResultLauncher + + private val viewModel: BackupsSettingsViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + checkoutLauncher = createBackupsCheckoutLauncher { + findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment) + } + } + + override fun onResume() { + super.onResume() + viewModel.refreshState() + } + + @Composable + override fun FragmentContent() { + val state by viewModel.stateFlow.collectAsState() + + BackupsSettingsContent( + backupsSettingsState = state, + onNavigationClick = { findNavController().popBackStack() }, + onBackupsRowClick = { + when (state.enabledState) { + is BackupsSettingsState.EnabledState.Active, BackupsSettingsState.EnabledState.Inactive -> { + findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_remoteBackupsSettingsFragment) + } + + BackupsSettingsState.EnabledState.Never -> { + checkoutLauncher.launch(Unit) + } + + else -> Unit + } + }, + onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) } + ) + } +} + +@Composable +private fun BackupsSettingsContent( + backupsSettingsState: BackupsSettingsState, + onNavigationClick: () -> Unit = {}, + onBackupsRowClick: () -> Unit = {}, + onOnDeviceBackupsRowClick: () -> Unit = {} +) { + Scaffolds.Settings( + title = stringResource(R.string.preferences_chats__backups), + navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24), + onNavigationClick = onNavigationClick + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues) + ) { + item { + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__back_up_your_message_history), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.core_ui__gutter), vertical = 16.dp) + ) + } + + item { + when (backupsSettingsState.enabledState) { + BackupsSettingsState.EnabledState.Loading -> { + LoadingBackupsRow() + } + + BackupsSettingsState.EnabledState.Inactive -> { + InactiveBackupsRow( + onBackupsRowClick = onBackupsRowClick + ) + } + + is BackupsSettingsState.EnabledState.Active -> { + ActiveBackupsRow( + enabledState = backupsSettingsState.enabledState, + onBackupsRowClick = onBackupsRowClick + ) + } + + BackupsSettingsState.EnabledState.Never -> { + NeverEnabledBackupsRow( + onBackupsRowClick = onBackupsRowClick + ) + } + + BackupsSettingsState.EnabledState.Failed -> { + Text(text = "TODO") + } + } + } + + item { + Dividers.Default() + } + + item { + Texts.SectionHeader( + text = stringResource(R.string.RemoteBackupsSettingsFragment__other_ways_to_backup) + ) + + Rows.TextRow( + text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups), + label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to), + onClick = onOnDeviceBackupsRowClick + ) + } + } + } +} + +@Composable +private fun NeverEnabledBackupsRow( + onBackupsRowClick: () -> Unit = {} +) { + Rows.TextRow( + modifier = Modifier.height(IntrinsicSize.Min), + icon = { + Box( + modifier = Modifier + .fillMaxHeight() + .padding(top = 12.dp) + ) { + Icon( + painter = painterResource(R.drawable.symbol_backup_24), + contentDescription = null + ) + } + }, + text = { + Column { + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups) + ) + + Text( + text = stringResource(R.string.BackupsSettingsFragment_automatic_backups_with_signals), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + + Buttons.MediumTonal( + onClick = onBackupsRowClick, + modifier = Modifier.padding(top = 12.dp) + ) { + Text( + text = stringResource(R.string.BackupsSettingsFragment_set_up) + ) + } + } + } + ) +} + +@Composable +private fun InactiveBackupsRow( + onBackupsRowClick: () -> Unit = {} +) { + Rows.TextRow( + text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups), + label = stringResource(R.string.preferences_off), + icon = painterResource(R.drawable.symbol_backup_24), + onClick = onBackupsRowClick + ) +} + +@Composable +private fun ActiveBackupsRow( + enabledState: BackupsSettingsState.EnabledState.Active, + 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 { + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups) + ) + + when (enabledState.type) { + is MessageBackupsType.Paid -> { + Text( + text = stringResource( + R.string.BackupsSettingsFragment_s_month_renews_s, + FiatMoneyUtil.format(LocalContext.current.resources, enabledState.type.pricePerMonth), + DateUtils.formatDateWithYear(Locale.getDefault(), enabledState.expiresAt.inWholeMilliseconds) + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + } + + is MessageBackupsType.Free -> { + Text( + text = stringResource( + R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + } + } + + Text( + text = stringResource( + R.string.BackupsSettingsFragment_last_backup_s, + DateUtils.getDatelessRelativeTimeSpanFormattedDate( + LocalContext.current, + Locale.getDefault(), + enabledState.lastBackupAt.inWholeMilliseconds + ).value + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + } + }, + onClick = onBackupsRowClick + ) +} + +@Composable +private fun LoadingBackupsRow() { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(horizontal = dimensionResource(R.dimen.core_ui__gutter)) + ) { + CircularProgressIndicator() + } +} + +@SignalPreview +@Composable +private fun BackupsSettingsContentPreview() { + Previews.Preview { + BackupsSettingsContent( + backupsSettingsState = BackupsSettingsState( + enabledState = BackupsSettingsState.EnabledState.Active( + type = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")), + storageAllowanceBytes = 1_000_000 + ), + expiresAt = 0.seconds, + lastBackupAt = 0.seconds + ) + ) + ) + } +} + +@SignalPreview +@Composable +private fun InactiveBackupsRowPreview() { + Previews.Preview { + InactiveBackupsRow() + } +} + +@SignalPreview +@Composable +private fun ActivePaidBackupsRowPreview() { + Previews.Preview { + ActiveBackupsRow( + enabledState = BackupsSettingsState.EnabledState.Active( + type = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(4), Currency.getInstance("CAD")), + storageAllowanceBytes = 1_000_000 + ), + expiresAt = 0.seconds, + lastBackupAt = 0.seconds + ) + ) + } +} + +@SignalPreview +@Composable +private fun ActiveFreeBackupsRowPreview() { + Previews.Preview { + ActiveBackupsRow( + enabledState = BackupsSettingsState.EnabledState.Active( + type = MessageBackupsType.Free( + mediaRetentionDays = 30 + ), + expiresAt = 0.seconds, + lastBackupAt = 0.seconds + ) + ) + } +} + +@SignalPreview +@Composable +private fun LoadingBackupsRowPreview() { + Previews.Preview { + LoadingBackupsRow() + } +} + +@SignalPreview +@Composable +private fun NeverEnabledBackupsRowPreview() { + Previews.Preview { + NeverEnabledBackupsRow() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt new file mode 100644 index 0000000000..5834dfcb2f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.backups + +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType +import kotlin.time.Duration + +/** + * Screen state for top-level backups settings screen. + */ +data class BackupsSettingsState( + val enabledState: EnabledState = EnabledState.Loading +) { + /** + * Describes the 'enabled' state of backups. + */ + sealed interface EnabledState { + /** + * Loading data for this row + */ + data object Loading : 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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt new file mode 100644 index 0000000000..b78a533522 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.backups + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow +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.RecurringInAppPaymentRepository +import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.InternetConnectionObserver +import java.util.Currency +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class BackupsSettingsViewModel : ViewModel() { + + companion object { + private val TAG = Log.tag(BackupsSettingsViewModel::class) + } + + private val internalStateFlow = MutableStateFlow(BackupsSettingsState()) + + val stateFlow: StateFlow = internalStateFlow + + init { + viewModelScope.launch(Dispatchers.Default) { + InternetConnectionObserver.observe().asFlow() + .distinctUntilChanged() + .filter { it } + .drop(1) + .collect { + refreshState() + } + } + } + + fun refreshState() { + Log.d(TAG, "Refreshing state.") + loadEnabledState() + } + + private fun loadEnabledState() { + viewModelScope.launch(Dispatchers.IO) { + val enabledState = when (SignalStore.backup.backupTier) { + MessageBackupTier.FREE -> getEnabledStateForFreeTier() + MessageBackupTier.PAID -> getEnabledStateForPaidTier() + null -> getEnabledStateForNoTier() + } + + internalStateFlow.update { it.copy(enabledState = enabledState) } + } + } + + private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState { + return try { + BackupsSettingsState.EnabledState.Active( + expiresAt = 0.seconds, + lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds, + type = BackupRepository.getBackupsType(MessageBackupTier.FREE)!! + ) + } catch (e: Exception) { + Log.w(TAG, "Failed to build enabled state.", e) + BackupsSettingsState.EnabledState.Failed + } + } + + private suspend fun getEnabledStateForPaidTier(): BackupsSettingsState.EnabledState { + return try { + val backupType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as MessageBackupsType.Paid + val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrThrow() + 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 = backupType.storageAllowanceBytes + ) + ) + } else { + BackupsSettingsState.EnabledState.Inactive + } + } catch (e: Exception) { + Log.w(TAG, "Failed to build enabled state.", e) + BackupsSettingsState.EnabledState.Failed + } + } + + private fun getEnabledStateForNoTier(): BackupsSettingsState.EnabledState { + return if (SignalStore.uiHints.hasEverEnabledRemoteBackups) { + BackupsSettingsState.EnabledState.Never + } else { + BackupsSettingsState.EnabledState.Inactive + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt similarity index 76% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index e4f52f0a85..efeea40c24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups +package org.thoughtcrime.securesms.components.settings.app.backups.remote import android.os.Bundle import android.view.View @@ -18,9 +18,11 @@ 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.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -44,6 +46,7 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.fragment.app.setFragmentResultListener @@ -58,16 +61,18 @@ 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.theme.SignalTheme import org.signal.core.util.money.FiatMoney import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.ArchiveUploadProgress import org.thoughtcrime.securesms.backup.v2.BackupFrequency +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType -import org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment +import org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher import org.thoughtcrime.securesms.compose.ComposeFragment -import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord -import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.fonts.SignalSymbols +import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.util.DateUtils @@ -75,7 +80,10 @@ import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.viewModel import java.math.BigDecimal +import java.util.Currency import java.util.Locale +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds /** * Remote backups settings fragment. @@ -105,7 +113,8 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { requestedSnackbar = state.snackbar, contentCallbacks = callbacks, backupProgress = backupProgress, - backupSize = state.backupSize + backupSize = state.backupSize, + renewalTime = state.renewalTime ) } @@ -115,18 +124,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { findNavController().popBackStack() } - override fun onEnableBackupsClick() { - checkoutLauncher.launch(Unit) + override fun onBackupTypeActionClick(tier: MessageBackupTier) { + // TODO [message-backups] } override fun onBackUpUsingCellularClick(canUseCellular: Boolean) { viewModel.setCanBackUpUsingCellular(canUseCellular) } - override fun onViewPaymentHistory() { - findNavController().safeNavigate(R.id.action_remoteBackupsSettingsFragment_to_remoteBackupsPaymentHistoryFragment) - } - override fun onBackupNowClick() { viewModel.onBackupNowClick() } @@ -191,10 +196,9 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { */ private interface ContentCallbacks { fun onNavigationClick() = Unit - fun onEnableBackupsClick() = Unit fun onBackupsTypeClick() = Unit + fun onBackupTypeActionClick(tier: MessageBackupTier) = Unit fun onBackUpUsingCellularClick(canUseCellular: Boolean) = Unit - fun onViewPaymentHistory() = Unit fun onBackupNowClick() = Unit fun onTurnOffAndDeleteBackupsClick() = Unit fun onChangeBackupFrequencyClick() = Unit @@ -207,6 +211,7 @@ private interface ContentCallbacks { @Composable private fun RemoteBackupsSettingsContent( messageBackupsType: MessageBackupsType?, + renewalTime: Duration, lastBackupTimestamp: Long, canBackUpUsingCellular: Boolean, backupsFrequency: BackupFrequency, @@ -232,99 +237,88 @@ private fun RemoteBackupsSettingsContent( modifier = Modifier .padding(it) ) { - item { - BackupTypeRow( - messageBackupsType = messageBackupsType, - onEnableBackupsClick = contentCallbacks::onEnableBackupsClick, - onChangeBackupsTypeClick = contentCallbacks::onBackupsTypeClick - ) + if (messageBackupsType != null) { + item { + BackupTypeRow( + messageBackupsType = messageBackupsType, + renewalTime = renewalTime, + onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick + ) + } } - if (messageBackupsType == null) { + item { + Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details)) + } + + if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) { item { - Rows.TextRow( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__payment_history), - onClick = contentCallbacks::onViewPaymentHistory + LastBackupRow( + lastBackupTimestamp = lastBackupTimestamp, + onBackupNowClick = contentCallbacks::onBackupNowClick ) } } else { item { - Dividers.Default() + InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt()) } + } - 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 + 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 ) } - } else { - item { - InProgressBackupRow(progress = backupProgress.completedAttachments.toInt(), totalProgress = backupProgress.totalAttachments.toInt()) - } - } + }) + } - item { - Rows.TextRow(text = { + item { + Rows.TextRow( + text = { Column { Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_size), + text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ) Text( - text = Util.getPrettyFileSize(backupSize), + text = getTextForFrequency(backupsFrequency = backupsFrequency), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } - }) - } + }, + onClick = contentCallbacks::onChangeBackupFrequencyClick + ) + } - 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.ToggleRow( - checked = canBackUpUsingCellular, - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__back_up_using_cellular), - onCheckChanged = contentCallbacks::onBackUpUsingCellularClick - ) - } + item { + Dividers.Default() + } - item { - Dividers.Default() - } - - item { - Rows.TextRow( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup), - foregroundTint = MaterialTheme.colorScheme.error, - onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick - ) - } + item { + Rows.TextRow( + text = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backup), + foregroundTint = MaterialTheme.colorScheme.error, + onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick + ) } } } @@ -383,63 +377,71 @@ private fun RemoteBackupsSettingsContent( @Composable private fun BackupTypeRow( - messageBackupsType: MessageBackupsType?, - onEnableBackupsClick: () -> Unit, - onChangeBackupsTypeClick: () -> Unit + messageBackupsType: MessageBackupsType, + renewalTime: Duration, + onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {} ) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick) - .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) - .padding(top = 16.dp, bottom = 14.dp) + .padding(horizontal = 16.dp, vertical = 12.dp) + .background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp)) + .padding(24.dp) ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_type), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - - if (messageBackupsType == null) { - Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backups_disabled), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else if (messageBackupsType is MessageBackupsType.Paid) { - val localResources = LocalContext.current.resources - val formattedCurrency = remember(messageBackupsType.pricePerMonth) { - FiatMoneyUtil.format(localResources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) + Row { + Column { + val title = when (messageBackupsType) { + is MessageBackupsType.Paid -> stringResource(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media) + is MessageBackupsType.Free -> pluralStringResource(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays) } Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media), formattedCurrency) + text = buildAnnotatedString { + SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK) + append(" ") + append(title) + }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium ) - } else { - val retentionDays = (messageBackupsType as MessageBackupsType.Free).mediaRetentionDays - val localResources = LocalContext.current.resources - val formattedCurrency = remember { - val currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP) - FiatMoneyUtil.format(localResources, FiatMoney(BigDecimal.ZERO, currency), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) + + 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 = stringResource( - id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, - pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, retentionDays, retentionDays), - formattedCurrency - ) + text = cost, + modifier = Modifier.padding(top = 12.dp) ) + + if (messageBackupsType is MessageBackupsType.Paid) { + if (renewalTime > 0.seconds) { + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__renews_s, DateUtils.formatDateWithYear(Locale.getDefault(), renewalTime.inWholeMilliseconds)) + ) + } + } } + + // Icon } - if (messageBackupsType == null) { - Buttons.Small(onClick = onEnableBackupsClick) { - Text(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__enable_backups)) - } + val buttonText = when (messageBackupsType) { + is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__manage_or_cancel) + is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__upgrade) + } + + Buttons.LargeTonal( + onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }, + colors = ButtonDefaults.filledTonalButtonColors().copy( + containerColor = SignalTheme.colors.colorTransparent5 + ), + modifier = Modifier.padding(top = 12.dp) + ) { + Text( + text = buttonText + ) } } } @@ -668,7 +670,7 @@ private fun getTextForFrequency(backupsFrequency: BackupFrequency): String { private fun RemoteBackupsSettingsContentPreview() { Previews.Preview { RemoteBackupsSettingsContent( - messageBackupsType = null, + messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30), lastBackupTimestamp = -1, canBackUpUsingCellular = false, backupsFrequency = BackupFrequency.MANUAL, @@ -676,6 +678,7 @@ private fun RemoteBackupsSettingsContentPreview() { requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE, contentCallbacks = object : ContentCallbacks {}, backupProgress = null, + renewalTime = 1727193018.seconds, backupSize = 2300000 ) } @@ -685,13 +688,22 @@ private fun RemoteBackupsSettingsContentPreview() { @Composable private fun BackupTypeRowPreview() { Previews.Preview { - BackupTypeRow( - messageBackupsType = MessageBackupsType.Free( - mediaRetentionDays = 30 - ), - onChangeBackupsTypeClick = {}, - onEnableBackupsClick = {} - ) + Column { + BackupTypeRow( + messageBackupsType = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), + storageAllowanceBytes = 100_000_000 + ), + renewalTime = 1727193018.seconds + ) + + BackupTypeRow( + messageBackupsType = MessageBackupsType.Free( + mediaRetentionDays = 30 + ), + renewalTime = 0.seconds + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt similarity index 83% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 1212b12057..b70706b578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups +package org.thoughtcrime.securesms.components.settings.app.backups.remote import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds data class RemoteBackupsSettingsState( val messageBackupsType: MessageBackupsType? = null, @@ -14,6 +16,7 @@ data class RemoteBackupsSettingsState( val backupSize: Long = 0, val backupsFrequency: BackupFrequency = BackupFrequency.DAILY, val lastBackupTimestamp: Long = 0, + val renewalTime: Duration = 0.seconds, val dialog: Dialog = Dialog.NONE, val snackbar: Snackbar = Snackbar.NONE ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt similarity index 80% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index f4cf7fb644..3bf3fbd45a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups +package org.thoughtcrime.securesms.components.settings.app.backups.remote import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -17,11 +17,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.BackupRepository +import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository +import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.BackupMessagesJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.service.MessageBackupListener import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds /** * ViewModel for state management of RemoteBackupsSettingsFragment @@ -40,6 +43,19 @@ class RemoteBackupsSettingsViewModel : ViewModel() { init { refresh() + + viewModelScope.launch { + val activeSubscription = withContext(Dispatchers.IO) { + RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) + } + + if (activeSubscription.isSuccess) { + val subscription = activeSubscription.getOrThrow().activeSubscription + if (subscription.isActive && subscription != null) { + _state.update { it.copy(renewalTime = subscription.endOfCurrentPeriod.seconds) } + } + } + } } fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt index e6f5b3404f..1e1772333f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsFragment.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups.type +package org.thoughtcrime.securesms.components.settings.app.backups.type import android.os.Bundle import android.view.View @@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.viewModel import java.math.BigDecimal import java.util.Locale @@ -86,10 +85,6 @@ class BackupsTypeSettingsFragment : ComposeFragment() { findNavController().popBackStack() } - override fun onPaymentHistoryClick() { - findNavController().safeNavigate(R.id.action_backupsTypeSettingsFragment_to_remoteBackupsPaymentHistoryFragment) - } - override fun onChangeOrCancelSubscriptionClick() { checkoutLauncher.launch(Unit) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt similarity index 85% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt index 3c77c1e305..52dde1e5e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsState.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups.type +package org.thoughtcrime.securesms.components.settings.app.backups.type import androidx.compose.runtime.Stable import org.signal.donations.PaymentSourceType diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt index 02c477d36c..54c16164e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/type/BackupsTypeSettingsViewModel.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -package org.thoughtcrime.securesms.components.settings.app.chats.backups.type +package org.thoughtcrime.securesms.components.settings.app.backups.type import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryFragment.kt deleted file mode 100644 index 83b16f6944..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryFragment.kt +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.chats.backups.history - -import android.content.Intent -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.fragment.app.viewModels -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.fragment.findNavController -import androidx.navigation.navArgument -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toPersistentList -import org.signal.core.ui.Buttons -import org.signal.core.ui.Dialogs -import org.signal.core.ui.Dividers -import org.signal.core.ui.Previews -import org.signal.core.ui.Rows -import org.signal.core.ui.Scaffolds -import org.signal.core.ui.SignalPreview -import org.signal.core.ui.Texts -import org.signal.core.util.money.FiatMoney -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.app.subscription.receipts.ReceiptImageRenderer -import org.thoughtcrime.securesms.compose.ComposeFragment -import org.thoughtcrime.securesms.compose.Nav -import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord -import org.thoughtcrime.securesms.payments.FiatMoneyUtil -import org.thoughtcrime.securesms.util.DateUtils -import java.math.BigDecimal -import java.util.Calendar -import java.util.Currency -import java.util.Locale - -/** - * Displays a list or detail view of in-app-payment receipts related to - * backups. - */ -class RemoteBackupsPaymentHistoryFragment : ComposeFragment() { - - private val viewModel: RemoteBackupsPaymentHistoryViewModel by viewModels() - - @Composable - override fun FragmentContent() { - val state by viewModel.state.collectAsState() - val navController = rememberNavController() - - LaunchedEffect(Unit) { - navController.setOnBackPressedDispatcher(requireActivity().onBackPressedDispatcher) - navController.enableOnBackPressed(true) - } - - val onNavigationClick = remember { - { - if (!navController.popBackStack()) { - findNavController().popBackStack() - } - } - } - - Nav.Host(navController = navController, startDestination = "list") { - composable("list") { - PaymentHistoryContent( - state = state, - onNavigationClick = onNavigationClick, - onRecordClick = { navController.navigate("detail/${it.id}") } - ) - } - - composable("detail/{recordId}", listOf(navArgument("recordId") { type = NavType.LongType })) { backStackEntry -> - val recordId = backStackEntry.arguments?.getLong("recordId")!! - val record = state.records[recordId]!! - - PaymentHistoryDetails( - record = record, - onNavigationClick = onNavigationClick, - onShareClick = this@RemoteBackupsPaymentHistoryFragment::onShareClick - ) - - if (state.displayProgressDialog) { - Dialogs.IndeterminateProgressDialog() - } - } - } - } - - private fun onShareClick(record: InAppPaymentReceiptRecord) { - viewModel.onStartRenderingBitmap() - ReceiptImageRenderer.renderPng( - requireContext(), - viewLifecycleOwner, - record, - getString(R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup), - object : ReceiptImageRenderer.Callback { - override fun onBitmapRendered() { - viewModel.onEndRenderingBitmap() - } - - override fun onStartActivity(intent: Intent) { - startActivity(intent) - } - } - ) - } -} - -@Composable -private fun PaymentHistoryContent( - state: RemoteBackupsPaymentHistoryState, - onNavigationClick: () -> Unit, - onRecordClick: (InAppPaymentReceiptRecord) -> Unit -) { - Scaffolds.Settings( - title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_history), - navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24), - onNavigationClick = onNavigationClick - ) { - val itemList = remember(state.records) { state.records.values.toPersistentList() } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(it) - ) { - itemsIndexed( - items = itemList, - key = { _, item -> item.id } - ) { idx, item -> - val previous = itemList.getOrNull(idx - 1) - val previousYear = rememberYear(timestamp = previous?.timestamp ?: 0) - val ourYear = rememberYear(timestamp = item.timestamp) - - if (previousYear != ourYear) { - Texts.SectionHeader(text = "$ourYear") - } - - PaymentHistoryRow(item, onRecordClick) - } - } - } -} - -@Composable -private fun rememberYear(timestamp: Long): Int { - if (timestamp == 0L) { - return -1 - } - - val calendar = remember { - Calendar.getInstance() - } - - return remember(timestamp) { - calendar.timeInMillis = timestamp - calendar.get(Calendar.YEAR) - } -} - -@Composable -private fun PaymentHistoryRow( - record: InAppPaymentReceiptRecord, - onRecordClick: (InAppPaymentReceiptRecord) -> Unit -) { - val date = remember(record.timestamp) { - DateUtils.formatDateWithYear(Locale.getDefault(), record.timestamp) - } - - val onClick = remember(record) { - { onRecordClick(record) } - } - - Rows.TextRow(text = { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = date, - style = MaterialTheme.typography.bodyLarge - ) - - Text( - text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - val resources = LocalContext.current.resources - val fiat = remember(record.amount) { - FiatMoneyUtil.format(resources, record.amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) - } - - Text(text = fiat) - }, onClick = onClick) -} - -@Composable -private fun PaymentHistoryDetails( - record: InAppPaymentReceiptRecord, - onNavigationClick: () -> Unit, - onShareClick: (InAppPaymentReceiptRecord) -> Unit -) { - Scaffolds.Settings( - title = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__payment_details), - onNavigationClick = onNavigationClick, - navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24) - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(it) - ) { - val resources = LocalContext.current.resources - val formattedAmount = remember(record.amount) { - FiatMoneyUtil.format(resources, record.amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()) - } - - Image( - painter = painterResource(id = R.drawable.ic_signal_logo_type), - contentDescription = null, - modifier = Modifier - .align(alignment = Alignment.CenterHorizontally) - .padding(top = 24.dp, bottom = 16.dp) - ) - - Text( - text = formattedAmount, - style = MaterialTheme.typography.displayMedium, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - - Dividers.Default() - - Rows.TextRow( - text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__backup_type), - label = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__text_and_all_media_backup) - ) - - val formattedDate = remember(record.timestamp) { - DateUtils.formatDateWithYear(Locale.getDefault(), record.timestamp) - } - - Rows.TextRow( - text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__date_paid), - label = formattedDate - ) - - Spacer(modifier = Modifier.weight(1f)) - - Buttons.LargePrimary( - onClick = { onShareClick(record) }, - modifier = Modifier - .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) - .padding(bottom = 24.dp) - .fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.RemoteBackupsPaymentHistoryFragment__share)) - } - } - } -} - -@SignalPreview -@Composable -private fun PaymentHistoryContentPreview() { - Previews.Preview { - PaymentHistoryContent( - state = RemoteBackupsPaymentHistoryState( - records = persistentMapOf( - 1L to testRecord() - ) - ), - onNavigationClick = {}, - onRecordClick = {} - ) - } -} - -@SignalPreview -@Composable -private fun PaymentHistoryRowPreview() { - Previews.Preview { - PaymentHistoryRow( - record = testRecord(), - onRecordClick = {} - ) - } -} - -@SignalPreview -@Composable -private fun PaymentDetailsContentPreview() { - Previews.Preview { - PaymentHistoryDetails( - record = testRecord(), - onNavigationClick = {}, - onShareClick = {} - ) - } -} - -private fun testRecord(): InAppPaymentReceiptRecord { - return InAppPaymentReceiptRecord( - id = 1, - amount = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")), - timestamp = 1718739691000, - type = InAppPaymentReceiptRecord.Type.RECURRING_BACKUP, - subscriptionLevel = 201 - ) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryRepository.kt deleted file mode 100644 index 89f1655142..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.chats.backups.history - -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord - -object RemoteBackupsPaymentHistoryRepository { - - fun getReceipts(): List { - return SignalDatabase.donationReceipts.getReceipts(InAppPaymentReceiptRecord.Type.RECURRING_BACKUP) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryState.kt deleted file mode 100644 index 3189026113..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryState.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.chats.backups.history - -import androidx.compose.runtime.Stable -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.persistentMapOf -import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord - -@Stable -data class RemoteBackupsPaymentHistoryState( - val records: PersistentMap = persistentMapOf(), - val displayProgressDialog: Boolean = false -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryViewModel.kt deleted file mode 100644 index 9399dfbfe7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/history/RemoteBackupsPaymentHistoryViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.app.chats.backups.history - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.collections.immutable.toPersistentMap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class RemoteBackupsPaymentHistoryViewModel : ViewModel() { - - private val internalStateFlow = MutableStateFlow(RemoteBackupsPaymentHistoryState()) - val state: StateFlow = internalStateFlow - - init { - viewModelScope.launch { - val receipts = withContext(Dispatchers.IO) { - RemoteBackupsPaymentHistoryRepository.getReceipts() - } - - internalStateFlow.update { state -> state.copy(records = receipts.associateBy { it.id }.toPersistentMap()) } - } - } - - fun onStartRenderingBitmap() { - internalStateFlow.update { it.copy(displayProgressDialog = true) } - } - - fun onEndRenderingBitmap() { - internalStateFlow.update { it.copy(displayProgressDialog = false) } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt index c562015691..da0cf658b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription import androidx.annotation.CheckResult +import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers @@ -46,18 +47,26 @@ object RecurringInAppPaymentRepository { private val donationsService = AppDependencies.donationsService fun getActiveSubscription(type: InAppPaymentSubscriberRecord.Type): Single { - val localSubscription = InAppPaymentsRepository.getSubscriber(type) - return if (localSubscription != null) { - Single.fromCallable { donationsService.getSubscription(localSubscription.subscriberId) } - .subscribeOn(Schedulers.io()) - .flatMap(ServiceResponse::flattenResult) - .doOnSuccess { activeSubscription -> - if (activeSubscription.isActive && activeSubscription.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) { - InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds) - } - } - } else { - Single.just(ActiveSubscription.EMPTY) + return Single.fromCallable { + getActiveSubscriptionSync(type).getOrThrow() + }.subscribeOn(Schedulers.io()) + } + + @WorkerThread + fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): Result { + val response = InAppPaymentsRepository.getSubscriber(type)?.let { + donationsService.getSubscription(it.subscriberId) + } ?: return Result.success(ActiveSubscription.EMPTY) + + return try { + val result = response.resultOrThrow + if (result.isActive && result.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) { + InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds) + } + + Result.success(result) + } catch (e: Exception) { + Result.failure(e) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt b/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt index 1a684905f8..fd2246bd5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt @@ -10,6 +10,12 @@ import android.graphics.Typeface import android.text.SpannableStringBuilder import android.text.TextPaint import android.text.style.MetricAffectingSpan +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.withStyle /** * Helper object for working with the SignalSymbols font @@ -17,6 +23,7 @@ import android.text.style.MetricAffectingSpan object SignalSymbols { enum class Glyph(val unicode: Char) { + CHECKMARK('\u2713'), CHEVRON_RIGHT('\uE025'), PERSON_CIRCLE('\uE05E') } @@ -43,6 +50,17 @@ object SignalSymbols { return text } + @Composable + fun AnnotatedString.Builder.SignalSymbol(weight: Weight, glyph: Glyph) { + withStyle( + SpanStyle( + fontFamily = FontFamily(getTypeface(LocalContext.current, weight)) + ) + ) { + append(glyph.unicode.toString()) + } + } + private fun getTypeface(context: Context, weight: Weight): Typeface { return when (weight) { Weight.BOLD -> getBoldWeightedFont(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt index fcc0929384..be9aae4020 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRedemptionJob.kt @@ -258,6 +258,7 @@ class InAppPaymentRedemptionJob private constructor( Log.i(TAG, "Enabling backups and setting backup tier to PAID", true) SignalStore.backup.areBackupsEnabled = true SignalStore.backup.backupTier = MessageBackupTier.PAID + SignalStore.uiHints.markHasEverEnabledRemoteBackups() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt index e2419695ad..65ca82d2fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InAppPaymentValues.kt @@ -516,6 +516,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor SignalStore.backup.areBackupsEnabled = true SignalStore.backup.backupTier = MessageBackupTier.PAID + SignalStore.uiHints.markHasEverEnabledRemoteBackups() } val subscriber = InAppPaymentsRepository.requireSubscriber(subscriberType) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java index 81944216bc..4ddc5a3db7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHintValues.java @@ -27,6 +27,7 @@ public class UiHintValues extends SignalStoreValues { private static final String DISMISSED_CONTACTS_PERMISSION_BANNER = "uihints.dismissed_contacts_permission_banner"; private static final String HAS_SEEN_DELETE_SYNC_EDUCATION_SHEET = "uihints.has_seen_delete_sync_education_sheet"; private static final String LAST_SUPPORT_VERSION_SEEN = "uihints.last_support_version_seen"; + private static final String HAS_EVER_ENABLED_REMOTE_BACKUPS = "uihints.has_ever_enabled_remote_backups"; UiHintValues(@NonNull KeyValueStore store) { super(store); @@ -39,7 +40,7 @@ public class UiHintValues extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_COMPLETED_USERNAME_ONBOARDING, HAS_SEEN_TEXT_FORMATTING_ALERT); + return Arrays.asList(NEVER_DISPLAY_PULL_TO_FILTER_TIP, HAS_COMPLETED_USERNAME_ONBOARDING, HAS_SEEN_TEXT_FORMATTING_ALERT, HAS_EVER_ENABLED_REMOTE_BACKUPS); } public void markHasSeenGroupSettingsMenuToast() { @@ -200,4 +201,12 @@ public class UiHintValues extends SignalStoreValues { public void setLastSupportVersionSeen(int version) { putInteger(LAST_SUPPORT_VERSION_SEEN, version); } + + public void markHasEverEnabledRemoteBackups() { + putBoolean(HAS_EVER_ENABLED_REMOTE_BACKUPS, true); + } + + public boolean getHasEverEnabledRemoteBackups() { + return getBoolean(HAS_EVER_ENABLED_REMOTE_BACKUPS, false); + } } diff --git a/app/src/main/res/drawable/symbol_backup_24.xml b/app/src/main/res/drawable/symbol_backup_24.xml new file mode 100644 index 0000000000..2bf6c8968f --- /dev/null +++ b/app/src/main/res/drawable/symbol_backup_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/navigation/app_settings.xml b/app/src/main/res/navigation/app_settings.xml index b8d3c3f853..ae627bd003 100644 --- a/app/src/main/res/navigation/app_settings.xml +++ b/app/src/main/res/navigation/app_settings.xml @@ -29,6 +29,13 @@ android:defaultValue="true" app:argType="boolean" /> + + android:id="@+id/backupsSettingsFragment" + android:name="org.thoughtcrime.securesms.components.settings.app.backups.BackupsSettingsFragment"> + + + + + android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment"> - - - diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index 2fcb080693..a1b616ebfb 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -10,6 +10,13 @@ android:name="org.thoughtcrime.securesms.components.settings.app.AppSettingsFragment" android:label="app_settings_fragment" tools:layout="@layout/dsl_settings_fragment"> + + android:id="@+id/backupsSettingsFragment" + android:name="org.thoughtcrime.securesms.components.settings.app.backups.BackupsSettingsFragment"> + + + + + + - - + android:name="org.thoughtcrime.securesms.components.settings.app.backups.type.BackupsTypeSettingsFragment"> - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3087e4e1a8..a6cfa8627c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7336,19 +7336,15 @@ OK - - - Payment history - - Text and all media backup - - Payment details - - Backup type - - Date paid - - Share + + + %1$s/month, renews %2$s + + Last backup %1$s + + Automatic backups with Signal\'s secure end-to-end encrypted storage service. + + Set up @@ -7376,13 +7372,27 @@ Backup will be created overnight. - Backup type + Backup plan Backups disabled - - %1$s ยท %2$s/month - - Enable backups + + %1$s/month + + Your backup plan is free + + Renews %1$s + + Back up your message history so you never lose data when you get a new phone or reinstall Signal. + + Other ways to backup + + On-device backups + + Save your backups to a folder on this device + + Manage or cancel + + Upgrade %1$d/%2$d