diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt
index 4c5c750372..690be544d5 100644
--- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt
+++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt
@@ -136,7 +136,7 @@ class MessageBackupsCheckoutActivityTest {
// Backup education screen
composeTestRule.onNodeWithText(context.getString(R.string.RemoteBackupsSettingsFragment__signal_backups)).assertIsDisplayed()
- composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsEducationScreen__enable_backups)).performClick()
+ composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsEducationScreen__continue)).performClick()
// Key education screen
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyEducationScreen__your_backup_key)).assertIsDisplayed()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreen.kt
new file mode 100644
index 0000000000..8bd2dce31e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreen.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2.ui.subscription
+
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.UiContext
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+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 kotlinx.coroutines.launch
+import org.signal.core.models.AccountEntropyPool
+import org.signal.core.models.ServiceId
+import org.signal.core.ui.compose.Buttons
+import org.signal.core.ui.compose.DayNightPreviews
+import org.signal.core.ui.compose.Dialogs
+import org.signal.core.ui.compose.Previews
+import org.signal.core.ui.compose.Scaffolds
+import org.signal.core.ui.compose.SignalIcons
+import org.signal.core.ui.compose.theme.SignalTheme
+import org.signal.passwordmanager.SignalCredentialManager
+import org.signal.signallogin.card.SignalLoginCard
+import org.thoughtcrime.securesms.R
+import java.util.UUID
+import org.signal.core.ui.R as CoreUiR
+import org.signal.signallogin.R as SignalLoginR
+
+object MessageBackupsConfirmRecoveryKeyTestTags {
+ const val CONFIRM_BUTTON = "confirm-recovery-key-screen-confirm-button"
+ const val ENTER_MANUALLY_BUTTON = "confirm-recovery-key-screen-enter-manually-button"
+}
+
+/**
+ * Asks a user who already has a Signal Login to confirm the recovery key that came with it, by pulling the credential
+ * back out of their password manager. Shown in place of the record-and-verify screens, since the key already exists and
+ * was already presented to them during registration.
+ */
+@Composable
+fun MessageBackupsConfirmRecoveryKeyScreen(
+ aci: ServiceId.ACI,
+ aep: AccountEntropyPool,
+ onNavigationClick: () -> Unit = {},
+ onViewDetailsClick: () -> Unit = {},
+ onConfirmed: () -> Unit = {},
+ onEnterManuallyClick: () -> Unit = {}
+) {
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val successMessage = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__recovery_key_confirmed)
+
+ var displayConfirmationFailure by remember { mutableStateOf(false) }
+ if (displayConfirmationFailure) {
+ ConfirmationFailureDialog(
+ onEnterManuallyClick = onEnterManuallyClick,
+ onDismiss = { displayConfirmationFailure = false }
+ )
+ }
+
+ Scaffolds.Settings(
+ title = "",
+ navigationIcon = SignalIcons.ArrowStart.imageVector,
+ onNavigationClick = onNavigationClick
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Image(
+ painter = painterResource(SignalLoginR.drawable.image_signal_login_keys),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 12.dp)
+ .size(96.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__confirm_your_recovery_key),
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(top = 24.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__your_recovery_key_is_a_64_character_code),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+
+ SignalLoginCard(
+ aci = aci,
+ aep = aep,
+ onViewDetailsClicked = onViewDetailsClick,
+ modifier = Modifier.padding(top = 32.dp)
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+
+ Text(
+ text = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__you_will_be_prompted_with_your_saved_password),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ )
+
+ Buttons.LargeTonal(
+ onClick = {
+ coroutineScope.launch {
+ if (getKeyFromCredentialManager(context, aci) == aep.displayValue) {
+ Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
+ onConfirmed()
+ } else {
+ displayConfirmationFailure = true
+ }
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON)
+ ) {
+ Text(text = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__confirm_recovery_key))
+ }
+
+ Buttons.LargeTonal(
+ onClick = onEnterManuallyClick,
+ colors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = SignalTheme.colors.colorSurface2,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, bottom = 16.dp)
+ .testTag(MessageBackupsConfirmRecoveryKeyTestTags.ENTER_MANUALLY_BUTTON)
+ ) {
+ Text(text = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__enter_manually))
+ }
+ }
+ }
+}
+
+@Composable
+private fun ConfirmationFailureDialog(
+ onEnterManuallyClick: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ Dialogs.SimpleAlertDialog(
+ title = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__error_confirming_recovery_key),
+ body = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__your_recovery_key_could_not_be_confirmed),
+ confirm = stringResource(R.string.MessageBackupsConfirmRecoveryKeyScreen__enter_manually),
+ dismiss = stringResource(android.R.string.cancel),
+ onConfirm = onEnterManuallyClick,
+ onDismiss = onDismiss
+ )
+}
+
+/**
+ * The password manager stores the Signal Login under the account key, so that is the credential id we ask for.
+ */
+private suspend fun getKeyFromCredentialManager(
+ @UiContext activityContext: Context,
+ aci: ServiceId.ACI
+): String? {
+ return SignalCredentialManager.getCredential(activityContext, aci.toString().uppercase())?.password
+}
+
+@DayNightPreviews
+@Composable
+private fun MessageBackupsConfirmRecoveryKeyScreenPreview() {
+ Previews.Preview {
+ MessageBackupsConfirmRecoveryKeyScreen(
+ aci = ServiceId.ACI.from(UUID.fromString("a6b28482-2e32-83d0-7f23-91360a4c2b91")),
+ aep = AccountEntropyPool("uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t")
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt
index 009e9db9a5..a417f9823a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsEducationScreen.kt
@@ -14,10 +14,10 @@ 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.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,18 +32,19 @@ import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
+import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.signal.core.ui.R as CoreUiR
/**
* Educational content which allows user to proceed to set up automatic backups
- * or navigate to a support page to learn more.
+ * or leave the flow without enabling them.
*/
@Composable
fun MessageBackupsEducationScreen(
onNavigationClick: () -> Unit,
onEnableBackups: () -> Unit,
- onLearnMore: () -> Unit
+ onNotNow: () -> Unit
) {
Scaffolds.Settings(
onNavigationClick = onNavigationClick,
@@ -65,7 +66,7 @@ fun MessageBackupsEducationScreen(
) {
item {
Image(
- painter = painterResource(id = R.drawable.image_signal_backups),
+ painter = painterResource(id = R.drawable.image_signal_backups_clock),
contentDescription = null,
modifier = Modifier
.padding(top = 24.dp)
@@ -119,18 +120,22 @@ fun MessageBackupsEducationScreen(
modifier = Modifier.fillMaxWidth()
) {
Text(
- text = stringResource(id = R.string.MessageBackupsEducationScreen__enable_backups)
+ text = stringResource(id = R.string.MessageBackupsEducationScreen__continue)
)
}
- TextButton(
- onClick = onLearnMore,
+ Buttons.LargeTonal(
+ onClick = onNotNow,
+ colors = ButtonDefaults.filledTonalButtonColors(
+ containerColor = SignalTheme.colors.colorSurface2,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ ),
modifier = Modifier
.fillMaxWidth()
- .padding(bottom = 16.dp)
+ .padding(top = 16.dp, bottom = 16.dp)
) {
Text(
- text = stringResource(id = R.string.MessageBackupsEducationScreen__learn_more)
+ text = stringResource(id = R.string.MessageBackupsEducationScreen__not_now)
)
}
}
@@ -144,7 +149,7 @@ private fun MessageBackupsEducationSheetPreview() {
MessageBackupsEducationScreen(
onNavigationClick = {},
onEnableBackups = {},
- onLearnMore = {}
+ onNotNow = {}
)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
index 021798dd7f..32bf54bb68 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
@@ -6,10 +6,12 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.app.Activity
+import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -18,6 +20,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
+import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -29,15 +32,21 @@ import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlowable
+import org.signal.core.ui.compose.CollectActions
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Dialogs
+import org.signal.core.util.Result
import org.signal.core.util.Util
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.getSerializableCompat
import org.signal.passwordmanager.SignalCredentialManager
+import org.signal.signallogin.pdf.SignalLoginPdfRenderer
+import org.signal.signallogin.viewdetails.SignalLoginViewDetailsScreen
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.components.settings.app.account.signallogin.SignalLoginViewDetailsAction
+import org.thoughtcrime.securesms.components.settings.app.account.signallogin.SignalLoginViewDetailsViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.compose.Nav
import org.thoughtcrime.securesms.database.InAppPaymentTable
@@ -58,6 +67,8 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
const val TIER = "tier"
const val CLIPBOARD_TIMEOUT_SECONDS = 60
+ private const val PDF_MIME_TYPE = "application/pdf"
+
fun create(messageBackupTier: MessageBackupTier?): MessageBackupsFlowFragment {
return MessageBackupsFlowFragment().apply {
arguments = bundleOf(TIER to messageBackupTier)
@@ -69,10 +80,25 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
MessageBackupsFlowViewModel(
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
- isCredentialManagerSupported = SignalCredentialManager.isSupported(requireContext())
+ isCredentialManagerSupported = SignalCredentialManager.isSupported(requireContext()),
+ isPhoneNumberless = SignalStore.account.isPhoneNumberless
)
}
+ private val signalLoginViewDetailsViewModel: SignalLoginViewDetailsViewModel by viewModels()
+
+ private val savePdfLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument(PDF_MIME_TYPE)) { uri: Uri? ->
+ if (uri != null) {
+ val context = requireContext().applicationContext
+ lifecycleScope.launch {
+ val result = SignalLoginPdfRenderer.renderTo(context, uri, signalLoginViewDetailsViewModel.state.value)
+ if (result is Result.Failure) {
+ Toast.makeText(context, result.failure.userMessageRes, Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ }
+
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -134,9 +160,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
MessageBackupsEducationScreen(
onNavigationClick = viewModel::goToPreviousStage,
onEnableBackups = viewModel::goToNextStage,
- onLearnMore = {
- CommunicationActions.openBrowserLink(requireContext(), getString(R.string.remote_backup_support_url))
- }
+ onNotNow = viewModel::goToPreviousStage
)
}
@@ -201,7 +225,34 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
MessageBackupsKeyVerifyScreen(
backupKey = state.accountEntropyPool.displayValue,
onNavigationClick = viewModel::goToPreviousStage,
- onNextClick = viewModel::goToNextStage
+ onNextClick = viewModel::goToNextStage,
+ mode = if (state.isPhoneNumberless) {
+ MessageBackupsKeyVerifyScreenMode.SIGNAL_LOGIN
+ } else {
+ MessageBackupsKeyVerifyScreenMode.DEFAULT
+ }
+ )
+ }
+
+ composable(route = MessageBackupsStage.Route.CONFIRM_RECOVERY_KEY.name) {
+ MessageBackupsConfirmRecoveryKeyScreen(
+ aci = state.aci,
+ aep = state.accountEntropyPool,
+ onNavigationClick = viewModel::goToPreviousStage,
+ onViewDetailsClick = viewModel::goToSignalLoginViewDetails,
+ onConfirmed = viewModel::onRecoveryKeyConfirmed,
+ onEnterManuallyClick = viewModel::goToEnterRecoveryKeyManually
+ )
+ }
+
+ composable(route = MessageBackupsStage.Route.SIGNAL_LOGIN_VIEW_DETAILS.name) {
+ val signalLoginState by signalLoginViewDetailsViewModel.state.collectAsStateWithLifecycle()
+
+ CollectActions(signalLoginViewDetailsViewModel.actions) { action -> handleSignalLoginViewDetailsAction(action) }
+
+ SignalLoginViewDetailsScreen(
+ state = signalLoginState,
+ onEvent = signalLoginViewDetailsViewModel::onEvent
)
}
@@ -273,6 +324,23 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
}
}
+ private fun handleSignalLoginViewDetailsAction(action: SignalLoginViewDetailsAction) {
+ when (action) {
+ SignalLoginViewDetailsAction.NavigateBack -> viewModel.goToPreviousStage()
+ SignalLoginViewDetailsAction.LaunchSaveToPasswordManager -> {
+ lifecycleScope.launch {
+ SignalCredentialManager.saveCredential(
+ activityContext = requireActivity(),
+ username = signalLoginViewDetailsViewModel.state.value.accountKey,
+ password = signalLoginViewDetailsViewModel.state.value.recoveryKey
+ )
+ }
+ }
+ SignalLoginViewDetailsAction.LaunchSaveAsPdf -> savePdfLauncher.launch(SignalLoginPdfRenderer.suggestedFileName(requireContext()))
+ is SignalLoginViewDetailsAction.CopyTextToClipboard -> Util.copyToClipboard(requireContext(), action.text, CLIPBOARD_TIMEOUT_SECONDS)
+ }
+ }
+
override fun onUserLaunchedAnExternalApplication() = error("Not supported by this fragment.")
override fun navigateToDonationPending(inAppPayment: InAppPaymentTable.InAppPayment) = error("Not supported by this fragment.")
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
index dcbabc7e38..db689d2a54 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.Immutable
import org.signal.core.models.AccountEntropyPool
+import org.signal.core.models.ServiceId
import org.signal.core.util.billing.BillingResponseCode
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
@@ -24,6 +25,8 @@ data class MessageBackupsFlowState(
val startScreen: MessageBackupsStage,
val stage: MessageBackupsStage = startScreen,
val accountEntropyPool: AccountEntropyPool = SignalStore.account.accountEntropyPool,
+ val aci: ServiceId.ACI = SignalStore.account.requireAci(),
+ val isPhoneNumberless: Boolean = false,
val failure: Throwable? = null,
val paymentReadyState: PaymentReadyState = PaymentReadyState.NOT_READY,
val backupKeySaveState: BackupKeySaveState? = null
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 18a6efbd8e..80c4b3a769 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
@@ -54,6 +54,7 @@ class MessageBackupsFlowViewModel(
private val initialTierSelection: MessageBackupTier?,
googlePlayApiAvailability: Int,
private val isCredentialManagerSupported: Boolean,
+ private val isPhoneNumberless: Boolean,
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
) : ViewModel(), BackupKeyCredentialManagerHandler {
@@ -68,7 +69,8 @@ class MessageBackupsFlowViewModel(
googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability),
currentMessageBackupTier = SignalStore.backup.backupTier,
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
- startScreen = startScreen
+ startScreen = startScreen,
+ isPhoneNumberless = isPhoneNumberless
)
)
@@ -237,11 +239,13 @@ class MessageBackupsFlowViewModel(
internalStateFlow.update {
when (it.stage) {
MessageBackupsStage.CANCEL -> error("Unsupported state transition from terminal state CANCEL")
- MessageBackupsStage.EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_EDUCATION)
+ MessageBackupsStage.EDUCATION -> it.copy(stage = if (isPhoneNumberless) MessageBackupsStage.CONFIRM_RECOVERY_KEY else MessageBackupsStage.BACKUP_KEY_EDUCATION)
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = if (isCredentialManagerSupported) MessageBackupsStage.BACKUP_KEY_RECORD else MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY)
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_VERIFY)
MessageBackupsStage.BACKUP_KEY_VERIFY -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
+ MessageBackupsStage.CONFIRM_RECOVERY_KEY -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
+ MessageBackupsStage.SIGNAL_LOGIN_VIEW_DETAILS -> error("Unsupported state transition from SIGNAL_LOGIN_VIEW_DETAILS")
MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it)
MessageBackupsStage.CHECKOUT_SHEET -> it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT)
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> error("This is driven by an async coroutine.")
@@ -264,8 +268,10 @@ class MessageBackupsFlowViewModel(
MessageBackupsStage.BACKUP_KEY_EDUCATION -> MessageBackupsStage.EDUCATION
MessageBackupsStage.BACKUP_KEY_RECORD -> MessageBackupsStage.BACKUP_KEY_EDUCATION
MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY -> if (isCredentialManagerSupported) MessageBackupsStage.BACKUP_KEY_RECORD else MessageBackupsStage.BACKUP_KEY_EDUCATION
- MessageBackupsStage.BACKUP_KEY_VERIFY -> MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY
- MessageBackupsStage.TYPE_SELECTION -> MessageBackupsStage.BACKUP_KEY_RECORD
+ MessageBackupsStage.BACKUP_KEY_VERIFY -> if (isPhoneNumberless) MessageBackupsStage.CONFIRM_RECOVERY_KEY else MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY
+ MessageBackupsStage.CONFIRM_RECOVERY_KEY -> MessageBackupsStage.EDUCATION
+ MessageBackupsStage.SIGNAL_LOGIN_VIEW_DETAILS -> MessageBackupsStage.CONFIRM_RECOVERY_KEY
+ MessageBackupsStage.TYPE_SELECTION -> if (isPhoneNumberless) MessageBackupsStage.CONFIRM_RECOVERY_KEY else MessageBackupsStage.BACKUP_KEY_RECORD
MessageBackupsStage.CHECKOUT_SHEET -> MessageBackupsStage.TYPE_SELECTION
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> MessageBackupsStage.CREATING_IN_APP_PAYMENT
MessageBackupsStage.PROCESS_PAYMENT -> MessageBackupsStage.PROCESS_PAYMENT
@@ -290,6 +296,26 @@ class MessageBackupsFlowViewModel(
}
}
+ /** The user asked to see the full keys that make up their Signal Login. */
+ fun goToSignalLoginViewDetails() {
+ internalStateFlow.update {
+ it.copy(stage = MessageBackupsStage.SIGNAL_LOGIN_VIEW_DETAILS)
+ }
+ }
+
+ /** The user chose to type their recovery key in by hand instead of pulling it from their password manager. */
+ fun goToEnterRecoveryKeyManually() {
+ internalStateFlow.update {
+ it.copy(stage = MessageBackupsStage.BACKUP_KEY_VERIFY)
+ }
+ }
+
+ /** The recovery key stored in the user's password manager matched the one on this device. */
+ fun onRecoveryKeyConfirmed() {
+ SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
+ goToNextStage()
+ }
+
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
internalStateFlow.update {
it.copy(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyVerifyScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyVerifyScreen.kt
index d5b6876951..51f5e2e6d9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyVerifyScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyVerifyScreen.kt
@@ -43,6 +43,18 @@ import kotlin.random.Random
import kotlin.random.nextInt
import org.signal.core.ui.R as CoreUiR
+enum class MessageBackupsKeyVerifyScreenMode {
+ /**
+ * Displayed when the user recorded their recovery key earlier in this same flow.
+ */
+ DEFAULT,
+
+ /**
+ * Displayed when the user's recovery key came with their Signal Login rather than being recorded in this flow.
+ */
+ SIGNAL_LOGIN
+}
+
/**
* Prompt user to re-enter backup key (AEP) to confirm they have it still.
*/
@@ -51,7 +63,8 @@ import org.signal.core.ui.R as CoreUiR
fun MessageBackupsKeyVerifyScreen(
backupKey: String,
onNavigationClick: () -> Unit = {},
- onNextClick: () -> Unit = {}
+ onNextClick: () -> Unit = {},
+ mode: MessageBackupsKeyVerifyScreenMode = MessageBackupsKeyVerifyScreenMode.DEFAULT
) {
val coroutineScope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(
@@ -72,7 +85,10 @@ fun MessageBackupsKeyVerifyScreen(
},
captionContent = {
Text(
- text = stringResource(R.string.MessageBackupsKeyVerifyScreen__enter_the_backup_key_that_you_just_recorded),
+ text = when (mode) {
+ MessageBackupsKeyVerifyScreenMode.DEFAULT -> stringResource(R.string.MessageBackupsKeyVerifyScreen__enter_the_backup_key_that_you_just_recorded)
+ MessageBackupsKeyVerifyScreenMode.SIGNAL_LOGIN -> stringResource(R.string.MessageBackupsKeyVerifyScreen__enter_the_recovery_key_from_your_signal_login)
+ },
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
)
},
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt
index fac97a4478..791c74ef6f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsStage.kt
@@ -17,6 +17,8 @@ enum class MessageBackupsStage(
BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD),
BACKUP_KEY_RECORD_MANUALLY(route = Route.BACKUP_KEY_RECORD_MANUALLY),
BACKUP_KEY_VERIFY(route = Route.BACKUP_KEY_VERIFY),
+ CONFIRM_RECOVERY_KEY(route = Route.CONFIRM_RECOVERY_KEY),
+ SIGNAL_LOGIN_VIEW_DETAILS(route = Route.SIGNAL_LOGIN_VIEW_DETAILS),
TYPE_SELECTION(route = Route.TYPE_SELECTION),
CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION),
CHECKOUT_SHEET(route = Route.TYPE_SELECTION),
@@ -32,6 +34,8 @@ enum class MessageBackupsStage(
CANCEL,
EDUCATION,
BACKUP_KEY_EDUCATION,
+ CONFIRM_RECOVERY_KEY,
+ SIGNAL_LOGIN_VIEW_DETAILS,
BACKUP_KEY_RECORD,
BACKUP_KEY_RECORD_MANUALLY,
BACKUP_KEY_VERIFY,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt
index e5f972553f..66bce4177b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
+import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.viewModel
/**
@@ -63,6 +64,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
initialTierSelection = MessageBackupTier.PAID,
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
isCredentialManagerSupported = SignalCredentialManager.isSupported(requireContext()),
+ isPhoneNumberless = SignalStore.account.isPhoneNumberless,
startScreen = MessageBackupsStage.TYPE_SELECTION
)
}
diff --git a/app/src/main/res/drawable-night/image_signal_backups_clock.xml b/app/src/main/res/drawable-night/image_signal_backups_clock.xml
new file mode 100644
index 0000000000..20181a2ab9
--- /dev/null
+++ b/app/src/main/res/drawable-night/image_signal_backups_clock.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-night/image_signal_backups_plans.xml b/app/src/main/res/drawable-night/image_signal_backups_plans.xml
index e95ef99b3e..ad883efc1f 100644
--- a/app/src/main/res/drawable-night/image_signal_backups_plans.xml
+++ b/app/src/main/res/drawable-night/image_signal_backups_plans.xml
@@ -1,73 +1,63 @@
+ android:width="86.5623dp"
+ android:height="81.6602dp"
+ android:viewportWidth="86.5623"
+ android:viewportHeight="81.6602">
-
-
-
-
-
-
-
+ android:fillColor="#FFC1C4FA"
+ android:pathData="M17.39 18.71C17.39 12.16 17.39 8.89 18.66 6.39 19.78 4.19 21.57 2.4 23.77 1.27 26.27 0 29.55 0 36.1 0H49.71C56.26 0 59.53 0 62.04 1.27 64.24 2.4 66.03 4.19 67.15 6.39 68.42 8.89 68.42 12.16 68.42 18.71V62.95C68.42 69.5 68.42 72.77 67.15 75.27 66.03 77.47 64.24 79.26 62.04 80.39 59.53 81.66 56.26 81.66 49.71 81.66H36.1C29.55 81.66 26.27 81.66 23.77 80.39 21.57 79.26 19.78 77.47 18.66 75.27 17.39 72.77 17.39 69.5 17.39 62.95V18.71Z"/>
+
+
+
+
+
+ android:strokeColor="#FF8C90CB"
+ android:strokeWidth="2"
+ android:pathData="M36.1 1H49.71C53 1 55.41 1 57.31 1.16 59.2 1.31 60.5 1.61 61.58 2.17 63.6 3.19 65.23 4.83 66.26 6.84 66.81 7.93 67.11 9.22 67.27 11.11 67.42 13.01 67.42 15.42 67.42 18.71V62.95C67.42 66.24 67.42 68.65 67.27 70.55 67.11 72.44 66.81 73.73 66.26 74.82 65.23 76.83 63.6 78.47 61.58 79.49 60.5 80.05 59.2 80.35 57.31 80.5 55.41 80.66 53 80.66 49.71 80.66H36.1C32.81 80.66 30.4 80.66 28.5 80.5 26.61 80.35 25.31 80.05 24.23 79.49 22.21 78.47 20.58 76.83 19.55 74.82 19 73.73 18.7 72.44 18.54 70.55 18.39 68.65 18.39 66.24 18.39 62.95V18.71C18.39 15.42 18.39 13.01 18.54 11.11 18.7 9.22 19 7.93 19.55 6.84 20.58 4.83 22.21 3.19 24.23 2.17 25.31 1.61 26.61 1.31 28.5 1.16 30.4 1 32.81 1 36.1 1Z"/>
+ android:fillColor="#FFC0CBE2"
+ android:strokeColor="#FF929AAC"
+ android:strokeWidth="1.5"
+ android:pathData="M17.3 35.84C26.05 35.84 33.14 42.93 33.14 51.67 33.14 60.42 26.05 67.51 17.3 67.51 14.77 67.51 12.37 66.92 10.25 65.86L9.83 65.64 9.75 65.6C9.7 65.58 9.65 65.57 9.6 65.56L9.52 65.55 9.43 65.54C9.42 65.55 9.41 65.55 9.4 65.55 9.4 65.55 9.4 65.55 9.39 65.55H9.38L9.38 65.55H9.38L9.29 65.56 9.2 65.59 2.03 68.17C1.27 68.44 0.54 67.71 0.81 66.95L3.39 59.78 3.42 59.69 3.43 59.6V59.59C3.43 59.58 3.43 59.58 3.43 59.58 3.43 59.57 3.44 59.56 3.44 59.55L3.43 59.46 3.42 59.38C3.41 59.3 3.38 59.23 3.34 59.15 2.15 56.93 1.47 54.38 1.47 51.67 1.47 42.93 8.56 35.84 17.3 35.84Z"/>
+
+
+
+
+
+ android:strokeColor="#FF929AAC"
+ android:strokeWidth="2"
+ android:pathData="M17.3 36.09C25.91 36.09 32.89 43.07 32.89 51.67 32.89 60.28 25.91 67.26 17.3 67.26 14.64 67.26 12.14 66.59 9.95 65.42L9.85 65.37C9.78 65.35 9.71 65.33 9.64 65.31L9.54 65.3 9.43 65.29C9.41 65.3 9.4 65.3 9.39 65.3 9.38 65.3 9.38 65.3 9.37 65.3H9.36L9.36 65.3H9.36L9.45 66.29 9.36 65.3 9.23 65.31 9.11 65.35 1.94 67.93C1.38 68.14 0.84 67.6 1.04 67.04L3.63 59.87 3.67 59.75 3.68 59.62V59.62H3.68V59.61C3.68 59.61 3.68 59.61 3.68 59.61 3.68 59.6 3.68 59.6 3.68 59.59 3.68 59.58 3.68 59.57 3.68 59.55 3.69 59.53 3.69 59.49 3.68 59.44 3.67 59.3 3.63 59.16 3.56 59.03 2.38 56.84 1.72 54.34 1.72 51.67 1.72 43.07 8.7 36.09 17.3 36.09Z"/>
+ android:fillColor="#FFD9E0EE"
+ android:strokeColor="#FF929AAC"
+ android:strokeWidth="1.5"
+ android:pathData="M60.56 21.41H79.56C83.01 21.41 85.81 24.21 85.81 27.66V40.66C85.81 44.11 83.01 46.91 79.56 46.91H60.56C57.11 46.91 54.31 44.11 54.31 40.66V27.66C54.31 24.21 57.11 21.41 60.56 21.41Z"/>
+
+
+
+
+ android:strokeColor="#FF929AAC"
+ android:strokeWidth="2"
+ android:pathData="M60.56 21.66H79.56C82.88 21.66 85.56 24.35 85.56 27.66V40.66C85.56 43.97 82.88 46.66 79.56 46.66H60.56C57.25 46.66 54.56 43.97 54.56 40.66V27.66C54.56 24.35 57.25 21.66 60.56 21.66Z"/>
-
-
-
-
-
-
-
+ android:fillColor="#FF929AAC"
+ android:pathData="M59.86 28.79C59.86 27.06 61.27 25.66 63 25.66 64.74 25.66 66.14 27.06 66.14 28.79 66.14 30.52 64.74 31.93 63 31.93 61.27 31.93 59.86 30.52 59.86 28.79Z"/>
diff --git a/app/src/main/res/drawable/image_signal_backups_clock.xml b/app/src/main/res/drawable/image_signal_backups_clock.xml
new file mode 100644
index 0000000000..f32a1969fb
--- /dev/null
+++ b/app/src/main/res/drawable/image_signal_backups_clock.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/image_signal_backups_plans.xml b/app/src/main/res/drawable/image_signal_backups_plans.xml
index 9e73d6c801..8cd0e6073c 100644
--- a/app/src/main/res/drawable/image_signal_backups_plans.xml
+++ b/app/src/main/res/drawable/image_signal_backups_plans.xml
@@ -1,73 +1,63 @@
+ android:width="86.5623dp"
+ android:height="81.6602dp"
+ android:viewportWidth="86.5623"
+ android:viewportHeight="81.6602">
-
-
-
-
-
-
-
+ android:fillColor="#FFC1C4FA"
+ android:pathData="M17.39 18.71C17.39 12.16 17.39 8.89 18.66 6.39 19.78 4.19 21.57 2.4 23.77 1.27 26.27 0 29.55 0 36.1 0H49.71C56.26 0 59.53 0 62.04 1.27 64.24 2.4 66.03 4.19 67.15 6.39 68.42 8.89 68.42 12.16 68.42 18.71V62.95C68.42 69.5 68.42 72.77 67.15 75.27 66.03 77.47 64.24 79.26 62.04 80.39 59.53 81.66 56.26 81.66 49.71 81.66H36.1C29.55 81.66 26.27 81.66 23.77 80.39 21.57 79.26 19.78 77.47 18.66 75.27 17.39 72.77 17.39 69.5 17.39 62.95V18.71Z"/>
+
+
+
+
+
+ android:strokeColor="#FF4046A9"
+ android:strokeWidth="2"
+ android:pathData="M36.1 1H49.71C53 1 55.41 1 57.31 1.16 59.2 1.31 60.5 1.61 61.58 2.17 63.6 3.19 65.23 4.83 66.26 6.84 66.81 7.93 67.11 9.22 67.27 11.11 67.42 13.01 67.42 15.42 67.42 18.71V62.95C67.42 66.24 67.42 68.65 67.27 70.55 67.11 72.44 66.81 73.73 66.26 74.82 65.23 76.83 63.6 78.47 61.58 79.49 60.5 80.05 59.2 80.35 57.31 80.5 55.41 80.66 53 80.66 49.71 80.66H36.1C32.81 80.66 30.4 80.66 28.5 80.5 26.61 80.35 25.31 80.05 24.23 79.49 22.21 78.47 20.58 76.83 19.55 74.82 19 73.73 18.7 72.44 18.54 70.55 18.39 68.65 18.39 66.24 18.39 62.95V18.71C18.39 15.42 18.39 13.01 18.54 11.11 18.7 9.22 19 7.93 19.55 6.84 20.58 4.83 22.21 3.19 24.23 2.17 25.31 1.61 26.61 1.31 28.5 1.16 30.4 1 32.81 1 36.1 1Z"/>
+ android:fillColor="#FFC0CBE2"
+ android:strokeColor="#FF4A5775"
+ android:strokeWidth="1.5"
+ android:pathData="M17.3 35.84C26.05 35.84 33.14 42.93 33.14 51.67 33.14 60.42 26.05 67.51 17.3 67.51 14.77 67.51 12.37 66.92 10.25 65.86L9.83 65.64 9.75 65.6C9.7 65.58 9.65 65.57 9.6 65.56L9.52 65.55 9.43 65.54C9.42 65.55 9.41 65.55 9.4 65.55 9.4 65.55 9.4 65.55 9.39 65.55H9.38L9.38 65.55H9.38L9.29 65.56 9.2 65.59 2.03 68.17C1.27 68.44 0.54 67.71 0.81 66.95L3.39 59.78 3.42 59.69 3.43 59.6V59.59C3.43 59.58 3.43 59.58 3.43 59.58 3.43 59.57 3.44 59.56 3.44 59.55L3.43 59.46 3.42 59.38C3.41 59.3 3.38 59.23 3.34 59.15 2.15 56.93 1.47 54.38 1.47 51.67 1.47 42.93 8.56 35.84 17.3 35.84Z"/>
+
+
+
+
+
+ android:strokeColor="#FF4A5775"
+ android:strokeWidth="2"
+ android:pathData="M17.3 36.09C25.91 36.09 32.89 43.07 32.89 51.67 32.89 60.28 25.91 67.26 17.3 67.26 14.64 67.26 12.14 66.59 9.95 65.42L9.85 65.37C9.78 65.35 9.71 65.33 9.64 65.31L9.54 65.3 9.43 65.29C9.41 65.3 9.4 65.3 9.39 65.3 9.38 65.3 9.38 65.3 9.37 65.3H9.36L9.36 65.3H9.36L9.45 66.29 9.36 65.3 9.23 65.31 9.11 65.35 1.94 67.93C1.38 68.14 0.84 67.6 1.04 67.04L3.63 59.87 3.67 59.75 3.68 59.62V59.62H3.68V59.61C3.68 59.61 3.68 59.61 3.68 59.61 3.68 59.6 3.68 59.6 3.68 59.59 3.68 59.58 3.68 59.57 3.68 59.55 3.69 59.53 3.69 59.49 3.68 59.44 3.67 59.3 3.63 59.16 3.56 59.03 2.38 56.84 1.72 54.34 1.72 51.67 1.72 43.07 8.7 36.09 17.3 36.09Z"/>
+ android:fillColor="#FFD9E0EE"
+ android:strokeColor="#FF4A5775"
+ android:strokeWidth="1.5"
+ android:pathData="M60.56 21.41H79.56C83.01 21.41 85.81 24.21 85.81 27.66V40.66C85.81 44.11 83.01 46.91 79.56 46.91H60.56C57.11 46.91 54.31 44.11 54.31 40.66V27.66C54.31 24.21 57.11 21.41 60.56 21.41Z"/>
+
+
+
+
+ android:strokeColor="#FF4A5775"
+ android:strokeWidth="2"
+ android:pathData="M60.56 21.66H79.56C82.88 21.66 85.56 24.35 85.56 27.66V40.66C85.56 43.97 82.88 46.66 79.56 46.66H60.56C57.25 46.66 54.56 43.97 54.56 40.66V27.66C54.56 24.35 57.25 21.66 60.56 21.66Z"/>
-
-
-
-
-
-
-
+ android:fillColor="#FF4A5775"
+ android:pathData="M59.86 28.79C59.86 27.06 61.27 25.66 63 25.66 64.74 25.66 66.14 27.06 66.14 28.79 66.14 30.52 64.74 31.93 63 31.93 61.27 31.93 59.86 30.52 59.86 28.79Z"/>
diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml
index 2bfbd89928..c3b73e60d4 100644
--- a/app/src/main/res/values-af/strings.xml
+++ b/app/src/main/res/values-af/strings.xml
@@ -9016,10 +9016,6 @@
Opsioneel, altyd
Skrap jou rugsteun te eniger tyd
-
- Aktiveer rugsteun
-
- Vind meer uit
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index f0c19ac1c4..6414bfbfd9 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -9808,10 +9808,6 @@
اختيارية، دائمًا
احذف نسختك الاحتياطية في أي وقت
-
- تفعيل النسخ الاحتياطية
-
- اعرف المزيد
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index 3cb5415057..719c3de802 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -9016,10 +9016,6 @@
Hər zaman ixtiyari
Ehtiyat nüsxəni istədiyiniz zaman silin
-
- Nüsxələməni fəallaşdır
-
- Daha ətraflı
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 52bf3db5bd..06791dd4e4 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -9412,10 +9412,6 @@
Неабавязкова, як заўсёды
Выдаліць рэзервовую копію ў любы час
-
- Уключыць рэзервовыя копіі
-
- Даведацца больш
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 15640bc4d3..c98c5afe44 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -9016,10 +9016,6 @@
По желание, винаги
Можете да изтриете резервното копие по всяко време
-
- Активиране на архиви
-
- Научете повече
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index c2b447e68f..88bcbb05f0 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -9016,10 +9016,6 @@
ঐচ্ছিক, সবসময়
যেকোনো সময় আপনার ব্যাকআপ মুছে ফেলুন
-
- ব্যাকআপ সক্ষম করুন
-
- আরো জানুন
diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml
index f337416d02..f6b06d0425 100644
--- a/app/src/main/res/values-bs/strings.xml
+++ b/app/src/main/res/values-bs/strings.xml
@@ -9412,10 +9412,6 @@
Nije obavezno
Izbrišite sigurnosnu kopiju bilo kada
-
- Aktiviraj
-
- Saznaj više
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 2f8ab38011..8a760c8fbe 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -9016,10 +9016,6 @@
Opcional, sempre
Elimina la teva còpia de seguretat en qualsevol moment
-
- Activa les còpies de seguretat
-
- Més informació
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 5b1517b7e9..2571d7518e 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -9412,10 +9412,6 @@
Volitelné, vždy
Zálohu můžete kdykoli odstranit
-
- Povolit zálohování
-
- Zjistit více
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 408531ad9f..f3c4e69815 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -9016,10 +9016,6 @@
Altid valgfrit
Slet din sikkerhedskopi når som helst
-
- Aktivér sikkerhedskopier
-
- Få mere at vide
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 9407f39e06..0868897b82 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -9016,10 +9016,6 @@
Optional
Lösche dein Backup jederzeit
-
- Backups aktivieren
-
- Mehr erfahren
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index d2fb2ba3c3..8e8b1c17ad 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -9016,10 +9016,6 @@
Προαιρετικά, πάντα
Διαγραφή αντιγράφων ασφαλείας ανά πάσα στιγμή
-
- Ενεργοποίηση αντίγραφων ασφαλείας
-
- Μάθε περισσότερα
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index cb13825f84..83049088aa 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -9016,10 +9016,6 @@
Servicio opcional, en todo momento
Copias de seguridad que puedes eliminar en cualquier momento
-
- Habilitar copias de seguridad
-
- Más información
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 318070f55b..ad4d8d239e 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -9016,10 +9016,6 @@
Alati vabatahtlik
Võid oma varukoopia igal ajal kustutada
-
- Luba varundamine
-
- Rohkem teavet
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 26f6f60ef8..84132e7f1b 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -9016,10 +9016,6 @@
Aukerakoa, beti
Ezabatu babeskopia nahi duzunean
-
- Gaitu babeskopiak
-
- Informazio gehiago
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index e1bc238ae3..7e2f4b2ae4 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -9016,10 +9016,6 @@
اختیاری، همیشه
هر موقع خواستید، نسخه پشتیبان خود را حذف کنید
-
- فعالسازی پشتیبانها
-
- اطلاعات بیشتر
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index cbb1867b29..17cf47aa46 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -9016,10 +9016,6 @@
Aina valinnainen
Poista varmuuskopio milloin tahansa
-
- Ota varmuuskopiot käyttöön
-
- Lue lisää
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 00ef522cd1..798993f587 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -9016,10 +9016,6 @@
La liberté de choisir un service en option
Des sauvegardes que vous pouvez supprimer à tout moment
-
- Activer les sauvegardes
-
- En savoir plus
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index 87c8c6dbf2..2fc0c44d2f 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -9610,10 +9610,6 @@
Roghnach, i gcónaí
Scrios do chúltaca ag am ar bith
-
- Cumasaigh cúltacaithe
-
- Tuilleadh faisnéise
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index d07360f3b1..e0a935f581 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -9016,10 +9016,6 @@
Escolle usalo cando queiras
Elimina a copia de seguranza cando queiras
-
- Activar copias de seguranza
-
- Máis información
diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml
index ff4dda6e98..84f9a00ea9 100644
--- a/app/src/main/res/values-gu/strings.xml
+++ b/app/src/main/res/values-gu/strings.xml
@@ -9016,10 +9016,6 @@
વૈકલ્પિક, હંમેશા
તમારા બેકઅપને કોઈ પણ સમયે ડિલીટ કરો
-
- બેકઅપ સક્ષમ કરો
-
- વધુ જાણો
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 1a45ce1a9e..cda5695bf8 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -9016,10 +9016,6 @@
वैकल्पिक, हमेशा
अपना बैकअप कभी भी डिलीट करें
-
- बैकअप की सुविधा चालू करें
-
- और जानें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 31cfd7c5f4..d31f7befa3 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -9412,10 +9412,6 @@
Neobavezno
Brisanje sigurnosne kopije moguće je u bilo kojem trenutku
-
- Omogući sigurnosne kopije
-
- Saznajte više
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index ede3dc2e16..0c60cc1cbe 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -9016,10 +9016,6 @@
Sosem kötelező
A biztonsági másolatok bármikor törölhetők
-
- Biztonsági mentések bekapcsolása
-
- Tudj meg többet
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index d85be5331f..4040674166 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -8818,10 +8818,6 @@
Opsional, selalu
Hapus cadangan Anda kapan saja
-
- Aktifkan pencadangan
-
- Pelajari selengkapnya
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 432708c709..de136fccce 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -9016,10 +9016,6 @@
Sempre facoltativo
Elimina il tuo backup quando vuoi
-
- Abilita i backup
-
- Scopri di più
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 8543a8e6a7..36e5161756 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -9412,10 +9412,6 @@
אופציונלי, תמיד
אפשר למחוק את הגיבוי בכל עת
-
- אפשר גיבויים
-
- למידע נוסף
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 972adfa041..3d001a7d26 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -8818,10 +8818,6 @@
任意でいつでも利用できます
バックアップはいつでも消去可能です
-
- バックアップを有効にする
-
- 詳しく見る
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 6844809e3b..f75a3a920d 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -9016,10 +9016,6 @@
ყოველთვის ნებაყოფლობითი
წაშალე შენი სარეზერვო კოპიები ნებისმიერ დროს
-
- სარეზერვო კოპირების ჩართვა
-
- გაიგე მეტი
diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml
index 5d76ad52cd..6237065805 100644
--- a/app/src/main/res/values-kk/strings.xml
+++ b/app/src/main/res/values-kk/strings.xml
@@ -9016,10 +9016,6 @@
Міндетті емес, әрдайым
Резервтік көшірмеңізді кез келген уақытты жойыңыз
-
- Резервтік көшірмелерді қосу
-
- Толық ақпарат
diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml
index fe6f301928..462376d538 100644
--- a/app/src/main/res/values-km/strings.xml
+++ b/app/src/main/res/values-km/strings.xml
@@ -8818,10 +8818,6 @@
ជាជម្រើសជានិច្ច
លុបការបម្រុងទុករបស់អ្នកនៅពេលណាក៏បាន
-
- បើកការបម្រុងទុក
-
- ស្វែងយល់បន្ថែម
diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml
index 9257638487..b8111daf4e 100644
--- a/app/src/main/res/values-kn/strings.xml
+++ b/app/src/main/res/values-kn/strings.xml
@@ -9016,10 +9016,6 @@
ಐಚ್ಛಿಕ, ಯಾವಾಗಲೂ
ನಿಮ್ಮ ಬ್ಯಾಕಪ್ ಅನ್ನು ಯಾವಾಗ ಬೇಕಾದರೂ ಅಳಿಸಿ
-
- ಬ್ಯಾಕಪ್ಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ
-
- ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 1a4162c775..774ea5328c 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -8818,10 +8818,6 @@
필수가 아닌 선택 사항
언제나 백업 삭제 가능
-
- 백업 활성화
-
- 자세히 알아보기
diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml
index 6790aadcce..2aa9132cb2 100644
--- a/app/src/main/res/values-ky/strings.xml
+++ b/app/src/main/res/values-ky/strings.xml
@@ -9016,10 +9016,6 @@
Ар дайым өзүңүз каалагандай
Камдык көчүрмөлөрдү каалаган учурда өчүрүп коесуз
-
- Камдык көчүрмөлөрдү иштетүү
-
- Кененирээк маалымат
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index f9d8b4b8ef..27590b72be 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -9412,10 +9412,6 @@
Neprivaloma
Atsarginę kopiją galima bet kada ištrinti
-
- Įjungti atsargines kopijas
-
- Sužinoti daugiau
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index a0d6f92685..a19d61b5a2 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -9214,10 +9214,6 @@
Pēc izvēles
Dzēsiet rezerves kopiju jebkurā laikā
-
- Iespējot rezerves kopēšanu
-
- Uzzināt vairāk
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 96d6fa8093..a4891ba6ba 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -9016,10 +9016,6 @@
Секогаш незадолжително
Можете да ја избришете вашата резерва копија во секое време
-
- Вклучи резервна копија
-
- Дознајте повеќе
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 7aa227a16b..a70b0bea62 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -9016,10 +9016,6 @@
ഓപ്ഷണൽ, എപ്പോഴും
എപ്പോൾ വേണമെങ്കിലും നിങ്ങളുടെ ബാക്കപ്പ് ഇല്ലാതാക്കുക
-
- ബാക്കപ്പുകൾ പ്രവർത്തനക്ഷമമാക്കുക
-
- കൂടുതലറിയുക
diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml
index f72da659db..bf26712e6b 100644
--- a/app/src/main/res/values-mr/strings.xml
+++ b/app/src/main/res/values-mr/strings.xml
@@ -9016,10 +9016,6 @@
वैकल्पिक, नेहमी
आपला बॅकअप कधीही हटवा
-
- बॅकअप सक्षम करा
-
- अधिक जाणून घ्या
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index 6699ec6225..96e278d5d6 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -8818,10 +8818,6 @@
Terpulang kepada anda
Padamkan sandaran anda pada bila-bila masa
-
- Mendayakan sandaran
-
- Ketahui lebih lanjut
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index a70d1fec9b..a2bd76933a 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -8818,10 +8818,6 @@
အမြဲတမ်း ရွေးချယ်နိုင်သည်
သင်၏ဘက်ခ်အပ်ကို အချိန်မရွေး ဖျက်နိုင်သည်
-
- ဘက်ခ်အပ် လုပ်ဆောင်မည်
-
- ပိုမိုလေ့လာရန်
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 09ec1fca91..bddf40ca78 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -9016,10 +9016,6 @@
Alltid valgfritt
Slett sikkerhetskopien når som helst
-
- Slå på sikkerhetskopiering
-
- Les mer
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 1fae4fb8c7..fc7d7d324e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -9016,10 +9016,6 @@
Optioneel
Verwijder je back-up wanneer je wilt
-
- Back-ups inschakelen
-
- Meer informatie
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 0d1413bcb8..90bc7c6f12 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -9016,10 +9016,6 @@
ਵਿਕਲਪਿਕ, ਹਮੇਸ਼ਾ
ਕਿਸੇ ਵੀ ਸਮੇਂ ਆਪਣਾ ਬੈਕਅੱਪ ਮਿਟਾਓ
-
- ਬੈਕਅੱਪ ਨੂੰ ਸਮਰੱਥ ਕਰੋ
-
- ਹੋਰ ਜਾਣੋ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index c3d58ec4ca..78b1115932 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -9412,10 +9412,6 @@
Dla chętnych (jak zwykle)
Możliwość usunięcia kopii zapasowej w dowolnym momencie
-
- Włącz kopie zapasowe
-
- Dowiedz się więcej
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index c1f738065a..86fee8e6b2 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -9016,10 +9016,6 @@
Sempre opcional
Exclua seu backup quando quiser
-
- Habilitar backups
-
- Saiba mais
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 48a5f47b0b..038c5aaf75 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -9016,10 +9016,6 @@
Opcional, sempre
Elimine a sua cópia de segurança a qualquer altura
-
- Ativar cópias de segurança
-
- Saber mais
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 1ebca18eed..0ac6f68ed0 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -9214,10 +9214,6 @@
Opțional, întotdeauna
Șterge oricând copia ta de rezervă
-
- Activează backup-urile
-
- Află mai multe
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index a547b67ead..89ec800a55 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -9412,10 +9412,6 @@
Всегда можно отключить
Удаляйте резервные копии в любое время
-
- Включить резервные копии
-
- Узнать больше
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index fd4581df95..3ff40102d1 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -9412,10 +9412,6 @@
Voliteľné, zakaždým
Kedykoľvek môžete zálohu vymazať
-
- Povoliť zálohovanie
-
- Zistiť viac
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index db65aa85e7..f74e5d0dae 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -9412,10 +9412,6 @@
Neobvezno, vedno
Kadarkoli izbrišite varnostno kopijo
-
- Dovoli varnostno kopiranje
-
- Preberite več
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 35400e6ed4..08df5b2200 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -9016,10 +9016,6 @@
Opsionale, gjithmonë
Fshij kopjeruajtjen në çdo kohë
-
- Aktivizoji kopjeruajtjet
-
- Mëso më shumë
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 459486a5b3..7f2c3963cd 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -9016,10 +9016,6 @@
Увек опционо
Избришите резервну копију када год пожелите
-
- Укључи резервне копије
-
- Сазнајте више
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 1a1090e430..1aa8b220f9 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -9016,10 +9016,6 @@
Alltid valfritt
Ta bort din säkerhetskopia när som helst
-
- Aktivera säkerhetskopior
-
- Läs mer
diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml
index 42a9578dfd..c0fa8d491b 100644
--- a/app/src/main/res/values-sw/strings.xml
+++ b/app/src/main/res/values-sw/strings.xml
@@ -9016,10 +9016,6 @@
Ya hiari, mara zote
Futa nakala yako muda wowote ule
-
- Wezesha nakalahifadhi
-
- Jifunze zaidi
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 2ff74d60c2..a83f46bb91 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -9016,10 +9016,6 @@
விருப்பத்தேர்வுக்குரியது, எப்போதும்
உங்கள் காப்புப்பிரதியை எப்போது வேண்டுமானாலும் அழிக்கலாம்
-
- காப்புப்பிரதிகளை இயக்கு
-
- மேலும் அறிக
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index 5fb8de70a9..bca8f5660a 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -9016,10 +9016,6 @@
ఐచ్ఛికం, ఎల్లప్పుడు
మీ బ్యాకప్ ఎప్పుడైనా తొలగించండి
-
- ప్రత్యామ్నాయ ప్రారంభించు
-
- మరింత తెలుసుకోండి
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 78145bba32..42d2904878 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -8818,10 +8818,6 @@
เลือกสมัครใช้บริการได้ตลอด
เลือกลบข้อมูลสำรองได้ทุกเมื่อ
-
- เปิดใช้การสำรองข้อมูล
-
- เรียนรู้เพิ่มเติม
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index 7157360958..5824b98d44 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -9016,10 +9016,6 @@
Palaging optional
Burahin ang backup mo anumang oras
-
- I-enable ang mga backup?
-
- Matuto pa
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 612ad6115b..da0999b7dc 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -9016,10 +9016,6 @@
İsteğe bağlı, daima
Yedeklemeni istediğin zaman sil
-
- Yedekleri etkinleştir
-
- Daha fazlasını öğren
diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml
index 8e332b7b0f..695bbc304c 100644
--- a/app/src/main/res/values-ug/strings.xml
+++ b/app/src/main/res/values-ug/strings.xml
@@ -8818,10 +8818,6 @@
ئىختىيارىي ، ھەرقاچان
زاپاس مەلۇماتلار خالىغان ۋاقىتتا ئۆچۈرەلەيسىز
-
- زاپاسلارنى قوزغات
-
- تەپسىلاتى
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 0af5e85bcb..85a4d50915 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -9412,10 +9412,6 @@
Ніколи не буде обов\'язковим
Резервну копію можна видалити коли завгодно
-
- Увімкнути
-
- Дізнатися більше
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 9fe282e98d..4623d70857 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -9016,10 +9016,6 @@
اختیاری، ہمیشہ
اپنا بیک اپ کسی بھی وقت حذف کریں
-
- بیک اپ فعال کریں
-
- مزید جانیں
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index a26d28342c..c7558777b4 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -8818,10 +8818,6 @@
Lựa chọn không bắt buộc
Xóa bản sao lưu bất kỳ lúc nào
-
- Bật sao lưu
-
- Tìm hiểu thêm
diff --git a/app/src/main/res/values-yue/strings.xml b/app/src/main/res/values-yue/strings.xml
index b1ac23d478..b7dd53f37d 100644
--- a/app/src/main/res/values-yue/strings.xml
+++ b/app/src/main/res/values-yue/strings.xml
@@ -8818,10 +8818,6 @@
幾時都有得揀
隨時刪除備份
-
- 啟用備份
-
- 了解詳情
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index ce18ec277a..c387cd9dd5 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -8818,10 +8818,6 @@
始终任选
可随时删除备份
-
- 启用备份
-
- 了解详情
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 844a53480d..a14af34f4f 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -8818,10 +8818,6 @@
永遠可自由選擇
隨時刪除你的備份
-
- 啟用備份
-
- 了解更多
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 123d75cd9d..90cd6a523c 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -8818,10 +8818,6 @@
永遠可自由選擇
隨時刪除你的備份
-
- 啟用備份
-
- 了解更多
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 466e4042a1..c13963df9b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9024,9 +9024,9 @@
Delete your backup anytime
- Enable backups
-
- Learn more
+ Continue
+
+ Not now
@@ -9160,6 +9160,26 @@
See key again
Next
+
+ Enter the recovery key from your Signal Login
+
+
+
+ Confirm your recovery key
+
+ Your recovery key is a 64-character code you received with your Signal Login. It\'s required to restore your backup. If you forget your key, you will not be able to recover your account.
+
+ You will be prompted with your saved password in the next step.
+
+ Confirm recovery key
+
+ Enter manually
+
+ Error confirming recovery key
+
+ Your recovery key could not be confirmed. Make sure you\'ve saved your Signal Login in your password manager, or enter your recovery key manually.
+
+ Recovery key confirmed
Enter your recovery key
diff --git a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreenTest.kt b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreenTest.kt
new file mode 100644
index 0000000000..7a90a5818c
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsConfirmRecoveryKeyScreenTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2.ui.subscription
+
+import android.app.Application
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
+import androidx.test.core.app.ApplicationProvider
+import assertk.assertThat
+import assertk.assertions.isFalse
+import assertk.assertions.isTrue
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.signal.core.models.AccountEntropyPool
+import org.signal.core.models.ServiceId
+import org.signal.core.ui.CoreUiDependenciesRule
+import org.signal.core.ui.compose.Dialogs
+import org.signal.core.ui.compose.theme.SignalTheme
+import org.signal.passwordmanager.SignalCredentialManager
+import org.signal.passwordmanager.UsernamePasswordCredential
+import org.signal.signallogin.SignalLoginTestTags
+import java.util.UUID
+
+/**
+ * Checks that the screen only advances when the password manager hands back the recovery key this device already has,
+ * and that every other path leaves the user a way to enter it manually.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class MessageBackupsConfirmRecoveryKeyScreenTest {
+
+ companion object {
+ private val ACI = ServiceId.ACI.from(UUID.fromString("a6b28482-2e32-83d0-7f23-91360a4c2b91"))
+ private val AEP = AccountEntropyPool("uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t")
+ private const val OTHER_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ }
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @get:Rule
+ val coreUiDependenciesRule = CoreUiDependenciesRule(ApplicationProvider.getApplicationContext())
+
+ private var confirmed = false
+ private var enteredManually = false
+ private var viewedDetails = false
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `confirming a matching saved key advances the flow`() {
+ stubCredentialManager(AEP.displayValue)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON).performClick()
+ composeTestRule.waitForIdle()
+
+ assertThat(confirmed).isTrue()
+ }
+
+ @Test
+ fun `confirming a key that does not match shows the failure dialog instead of advancing`() {
+ stubCredentialManager(OTHER_KEY)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON).performClick()
+ composeTestRule.waitForIdle()
+
+ assertThat(confirmed).isFalse()
+ composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).assertIsDisplayed()
+ }
+
+ @Test
+ fun `confirming with no saved credential shows the failure dialog instead of advancing`() {
+ stubCredentialManager(null)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON).performClick()
+ composeTestRule.waitForIdle()
+
+ assertThat(confirmed).isFalse()
+ composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).assertIsDisplayed()
+ }
+
+ @Test
+ fun `the failure dialog sends the user to manual entry`() {
+ stubCredentialManager(OTHER_KEY)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON).performClick()
+ composeTestRule.waitForIdle()
+ composeTestRule.onNodeWithTag(Dialogs.TEST_TAG_ALERT_DIALOG_CONFIRM_BUTTON).performClick()
+
+ assertThat(enteredManually).isTrue()
+ }
+
+ @Test
+ fun `enter manually skips the password manager entirely`() {
+ stubCredentialManager(AEP.displayValue)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.ENTER_MANUALLY_BUTTON).performClick()
+
+ assertThat(enteredManually).isTrue()
+ assertThat(confirmed).isFalse()
+ }
+
+ @Test
+ fun `view details on the card opens the full login`() {
+ setContent()
+
+ composeTestRule.onNodeWithTag(SignalLoginTestTags.CARD_VIEW_DETAILS_BUTTON).performScrollTo().performClick()
+
+ assertThat(viewedDetails).isTrue()
+ }
+
+ @Test
+ fun `the credential is requested under the account key`() {
+ stubCredentialManager(AEP.displayValue)
+ setContent()
+
+ composeTestRule.onNodeWithTag(MessageBackupsConfirmRecoveryKeyTestTags.CONFIRM_BUTTON).performClick()
+ composeTestRule.waitForIdle()
+
+ coVerify { SignalCredentialManager.getCredential(any(), ACI.toString().uppercase()) }
+ }
+
+ private fun stubCredentialManager(password: String?) {
+ mockkObject(SignalCredentialManager)
+ coEvery { SignalCredentialManager.getCredential(any(), any()) } returns password?.let {
+ UsernamePasswordCredential(username = ACI.toString().uppercase(), password = it)
+ }
+ }
+
+ private fun setContent() {
+ composeTestRule.setContent {
+ SignalTheme {
+ MessageBackupsConfirmRecoveryKeyScreen(
+ aci = ACI,
+ aep = AEP,
+ onViewDetailsClick = { viewedDetails = true },
+ onConfirmed = { confirmed = true },
+ onEnterManuallyClick = { enteredManually = true }
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModelTest.kt
new file mode 100644
index 0000000000..9741967d85
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2026 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2.ui.subscription
+
+import android.app.Application
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.signal.core.models.AccountEntropyPool
+import org.signal.core.models.ServiceId
+import org.thoughtcrime.securesms.backup.v2.BackupRepository
+import org.thoughtcrime.securesms.keyvalue.BackupValues
+import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
+import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
+import org.thoughtcrime.securesms.testutil.MockSignalStoreRule
+import java.util.UUID
+
+/**
+ * Covers the stage machine, and in particular how it forks for accounts that already have a Signal Login and therefore
+ * already know their recovery key.
+ */
+@RunWith(RobolectricTestRunner::class)
+@Config(manifest = Config.NONE, application = Application::class)
+class MessageBackupsFlowViewModelTest {
+
+ companion object {
+ private val ACI = ServiceId.ACI.from(UUID.fromString("a6b28482-2e32-83d0-7f23-91360a4c2b91"))
+ private val AEP = AccountEntropyPool("uy38jh2778hjjhj8lk19ga61s672jsj089r023s6a57809bap92j2yh5t326vv7t")
+ }
+
+ @get:Rule
+ val appDependencies = MockAppDependenciesRule()
+
+ @get:Rule
+ val signalStore = MockSignalStoreRule(relaxed = setOf(BackupValues::class))
+
+ @get:Rule
+ val dispatcherRule = CoroutineDispatcherRule(StandardTestDispatcher())
+
+ @Before
+ fun setUp() {
+ every { signalStore.backup.backupTier } returns null
+ every { signalStore.backup.deletionStateFlow } returns emptyFlow()
+ every { signalStore.account.accountEntropyPool } returns AEP
+ every { signalStore.account.requireAci() } returns ACI
+
+ mockkObject(BackupRepository)
+ coEvery { BackupRepository.getBackupTypes(any()) } returns emptyList()
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `an account with a Signal Login confirms its existing key instead of recording a new one`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+
+ viewModel.goToNextStage()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.CONFIRM_RECOVERY_KEY)
+ }
+
+ @Test
+ fun `an account with a phone number still records a new key`() {
+ val viewModel = createViewModel(isPhoneNumberless = false)
+
+ viewModel.goToNextStage()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.BACKUP_KEY_EDUCATION)
+ }
+
+ @Test
+ fun `confirming the recovery key goes straight to plan selection`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+
+ viewModel.onRecoveryKeyConfirmed()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.TYPE_SELECTION)
+ }
+
+ @Test
+ fun `entering the recovery key manually leads to plan selection by way of the verify screen`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+
+ viewModel.goToEnterRecoveryKeyManually()
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.BACKUP_KEY_VERIFY)
+
+ viewModel.goToNextStage()
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.TYPE_SELECTION)
+ }
+
+ @Test
+ fun `backing out of the verify screen returns to the confirmation screen`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+ viewModel.goToEnterRecoveryKeyManually()
+
+ viewModel.goToPreviousStage()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.CONFIRM_RECOVERY_KEY)
+ }
+
+ @Test
+ fun `viewing the full login details is a detour off the confirmation screen`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+
+ viewModel.goToSignalLoginViewDetails()
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.SIGNAL_LOGIN_VIEW_DETAILS)
+
+ viewModel.goToPreviousStage()
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.CONFIRM_RECOVERY_KEY)
+ }
+
+ @Test
+ fun `backing out of the confirmation screen returns to education`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+
+ viewModel.goToPreviousStage()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.EDUCATION)
+ }
+
+ @Test
+ fun `backing out of plan selection returns to the confirmation screen`() {
+ val viewModel = createViewModel(isPhoneNumberless = true)
+ viewModel.goToNextStage()
+ viewModel.onRecoveryKeyConfirmed()
+
+ viewModel.goToPreviousStage()
+
+ assertThat(viewModel.stateFlow.value.stage).isEqualTo(MessageBackupsStage.CONFIRM_RECOVERY_KEY)
+ }
+
+ private fun createViewModel(isPhoneNumberless: Boolean): MessageBackupsFlowViewModel {
+ return MessageBackupsFlowViewModel(
+ initialTierSelection = null,
+ googlePlayApiAvailability = 0,
+ isCredentialManagerSupported = true,
+ isPhoneNumberless = isPhoneNumberless,
+ startScreen = MessageBackupsStage.EDUCATION
+ )
+ }
+}
diff --git a/feature/registration/src/main/res/drawable-night/image_signal_login_keys.xml b/lib/signal-login/src/main/res/drawable-night/image_signal_login_keys.xml
similarity index 100%
rename from feature/registration/src/main/res/drawable-night/image_signal_login_keys.xml
rename to lib/signal-login/src/main/res/drawable-night/image_signal_login_keys.xml
diff --git a/feature/registration/src/main/res/drawable/image_signal_login_keys.xml b/lib/signal-login/src/main/res/drawable/image_signal_login_keys.xml
similarity index 100%
rename from feature/registration/src/main/res/drawable/image_signal_login_keys.xml
rename to lib/signal-login/src/main/res/drawable/image_signal_login_keys.xml