Present the backup key as a passphrase.

This commit is contained in:
Michelle Tang
2026-06-24 13:01:19 -04:00
committed by jeffrey-signal
parent 66b6c1656c
commit 5c64a91864
11 changed files with 450 additions and 42 deletions
@@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
/**
* Bottom sheet shown when confirming your recovery key after saving to password manager
*/
@Composable
fun ConfirmRecoveryKeySheet(
onConfirm: () -> Unit = {},
onSeeAgain: () -> Unit = {},
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.horizontalGutters(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.backup_confirm_80),
tint = Color.Unspecified,
contentDescription = null,
modifier = Modifier.padding(top = 24.dp, bottom = 16.dp)
)
Text(
text = stringResource(R.string.MessageBackupsKeyVerifyScreen__confirm_your_backup_key),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 12.dp)
)
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_that_your_recovery),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.size(60.dp))
Buttons.LargeTonal(onClick = onConfirm) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_recovery))
}
TextButton(
onClick = onSeeAgain,
modifier = Modifier.padding(vertical = 16.dp)
) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_key_again))
}
}
}
@DayNightPreviews
@Composable
private fun ConfirmRecoveryKeyPreview() {
Previews.BottomSheetContentPreview {
ConfirmRecoveryKeySheet(
onConfirm = {},
onSeeAgain = {},
modifier = Modifier.fillMaxSize()
)
}
}
@@ -68,7 +68,8 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
private val viewModel: MessageBackupsFlowViewModel by viewModel {
MessageBackupsFlowViewModel(
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext())
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext())
)
}
@@ -155,6 +156,32 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
val context = LocalContext.current
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
MessageBackupsKeyRecordScreen(
backupKey = state.accountEntropyPool.displayValue,
keySaveState = state.backupKeySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = viewModel::goToPreviousStage,
mode = remember {
MessageBackupsKeyRecordMode.Passkey(
onSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onSaveManually = viewModel::goToRecordManually,
onSaveSuccessful = viewModel::goToNextStage
)
},
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveStateCleared = viewModel::onBackupKeySaveStateCleared,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) },
notifyKeyIsSameAsOnDeviceBackupKey = SignalStore.backup.newLocalBackupsEnabled
)
}
composable(route = MessageBackupsStage.Route.BACKUP_KEY_RECORD_MANUALLY.name) {
val context = LocalContext.current
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
MessageBackupsKeyRecordScreen(
backupKey = state.accountEntropyPool.displayValue,
keySaveState = state.backupKeySaveState,
@@ -53,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
class MessageBackupsFlowViewModel(
private val initialTierSelection: MessageBackupTier?,
googlePlayApiAvailability: Int,
private val isCredentialManagerSupported: Boolean,
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
) : ViewModel(), BackupKeyCredentialManagerHandler {
@@ -238,8 +239,9 @@ class MessageBackupsFlowViewModel(
when (it.stage) {
MessageBackupsStage.CANCEL -> error("Unsupported state transition from terminal state CANCEL")
MessageBackupsStage.EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_EDUCATION)
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD)
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_VERIFY)
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.TYPE_SELECTION -> validateTypeAndUpdateState(it)
MessageBackupsStage.CHECKOUT_SHEET -> it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT)
@@ -262,7 +264,8 @@ class MessageBackupsFlowViewModel(
MessageBackupsStage.EDUCATION -> MessageBackupsStage.CANCEL
MessageBackupsStage.BACKUP_KEY_EDUCATION -> MessageBackupsStage.EDUCATION
MessageBackupsStage.BACKUP_KEY_RECORD -> MessageBackupsStage.BACKUP_KEY_EDUCATION
MessageBackupsStage.BACKUP_KEY_VERIFY -> MessageBackupsStage.BACKUP_KEY_RECORD
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.CHECKOUT_SHEET -> MessageBackupsStage.TYPE_SELECTION
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> MessageBackupsStage.CREATING_IN_APP_PAYMENT
@@ -277,6 +280,12 @@ class MessageBackupsFlowViewModel(
}
}
fun goToRecordManually() {
internalStateFlow.update {
it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY)
}
}
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
internalStateFlow.update {
it.copy(
@@ -6,14 +6,22 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.content.Context
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.annotation.UiContext
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -22,6 +30,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarHostState
@@ -34,9 +43,16 @@ import androidx.compose.runtime.Stable
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.draw.drawWithContent
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
@@ -48,6 +64,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
@@ -85,6 +102,11 @@ sealed interface MessageBackupsKeyRecordMode {
val isOptimizedStorageEnabled: Boolean,
val canRotateKey: Boolean
) : MessageBackupsKeyRecordMode
data class Passkey(
val onSaveToPasswordManager: () -> Unit,
val onSaveManually: () -> Unit,
val onSaveSuccessful: () -> Unit
) : MessageBackupsKeyRecordMode
}
/**
@@ -136,18 +158,24 @@ fun MessageBackupsKeyRecordScreen(
onRequestSaveToPasswordManager: () -> Unit = {},
onConfirmSaveToPasswordManager: () -> Unit = {},
onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {},
onSaveStateCleared: () -> Unit = {},
onGoToPasswordManagerSettingsClick: () -> Unit = {},
mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {}),
notifyKeyIsSameAsOnDeviceBackupKey: Boolean = false
) {
TemporaryScreenshotSecurity.bind()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val backupKeyString = remember(backupKey) {
backupKey.chunked(4).joinToString(" ")
}
if (mode is MessageBackupsKeyRecordMode.Next) {
val showAsPasskey = mode is MessageBackupsKeyRecordMode.Passkey
var showExpandedPasskey by remember { mutableStateOf(false) }
if (mode is MessageBackupsKeyRecordMode.Next || mode is MessageBackupsKeyRecordMode.Passkey) {
RecordScreenBackHandler()
}
@@ -183,6 +211,41 @@ fun MessageBackupsKeyRecordScreen(
}
}
var displayKeyVerificationError by remember { mutableStateOf(false) }
if (displayKeyVerificationError) {
ConfirmationFailureDialog(mode) {
displayKeyVerificationError = false
}
}
var displayConfirmKey by remember { mutableStateOf(false) }
if (displayConfirmKey) {
val context = LocalContext.current
val credentialId = stringResource(R.string.MessageBackupsKeyRecordScreen__backup_key_password_manager_id)
val successMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_confirmed)
ModalBottomSheet(
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
containerColor = SignalTheme.colors.colorSurface1,
onDismissRequest = { displayConfirmKey = false }
) {
ConfirmRecoveryKeySheet(
onConfirm = {
coroutineScope.launch {
val retrieved = getKeyFromCredentialManager(context, credentialId)
if (retrieved == backupKey) {
Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveSuccessful()
} else {
displayKeyVerificationError = true
}
}
displayConfirmKey = false
},
onSeeAgain = { displayConfirmKey = false }
)
}
}
Scaffolds.Settings(
title = "",
navigationIcon = SignalIcons.ArrowStart.imageVector,
@@ -216,7 +279,11 @@ fun MessageBackupsKeyRecordScreen(
item {
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key),
text = if (showAsPasskey) {
stringResource(R.string.MessageBackupsKeyRecordScreen__save_your_recovery_key)
} else {
stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key)
},
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 16.dp)
)
@@ -225,6 +292,8 @@ fun MessageBackupsKeyRecordScreen(
item {
val text = if (notifyKeyIsSameAsOnDeviceBackupKey) {
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_the_same_as_your_on_device_recovery_key)
} else if (showAsPasskey) {
stringResource(R.string.MessageBackupsKeyRecordScreen__your_recovery_key)
} else {
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover)
}
@@ -239,47 +308,113 @@ fun MessageBackupsKeyRecordScreen(
}
item {
Box(
modifier = Modifier
.padding(top = 24.dp, bottom = 16.dp)
.background(
color = SignalTheme.colors.colorSurface1,
shape = RoundedCornerShape(10.dp)
)
.padding(24.dp)
) {
Text(
text = backupKeyString,
style = MaterialTheme.typography.bodyLarge
.copy(
fontSize = 18.sp,
fontWeight = FontWeight(400),
letterSpacing = 1.44.sp,
lineHeight = 36.sp,
textAlign = TextAlign.Center,
fontFamily = MonoTypeface.fontFamily()
AnimatedContent(
targetState = showAsPasskey && !showExpandedPasskey,
transitionSpec = { fadeIn() togetherWith fadeOut() using SizeTransform(clip = false) },
label = "passkey",
modifier = Modifier.padding(top = 24.dp, bottom = 16.dp)
) { isCollapsed ->
if (isCollapsed) {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.background(
color = SignalTheme.colors.colorSurface1,
shape = RoundedCornerShape(50.dp)
)
) {
Text(
text = backupKeyString,
maxLines = 1,
style = MaterialTheme.typography.bodyLarge
.copy(
fontSize = 18.sp,
fontWeight = FontWeight(400),
letterSpacing = 1.44.sp,
lineHeight = 36.sp,
textAlign = TextAlign.Center,
fontFamily = MonoTypeface.fontFamily()
),
modifier = Modifier
.padding(10.dp)
.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
.drawWithContent {
drawContent()
drawRect(
brush = Brush.horizontalGradient(
0f to Color.Black,
0.75f to Color.Transparent
),
blendMode = BlendMode.DstIn
)
}
)
)
}
}
item {
Buttons.Small(
onClick = {
if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
displayRecoveryKeyCopyWarning = true
} else {
onCopyToClipboardClick(backupKeyString)
Row(
modifier = Modifier
.background(
color = SignalTheme.colors.colorSurface1,
shape = RoundedCornerShape(50.dp)
)
.padding(horizontal = 12.dp)
.clickable(onClick = { showExpandedPasskey = true }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_tap_20),
contentDescription = stringResource(R.string.MessageBackupsKeyRecordScreen__see_full_key)
)
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_full_key),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(start = 4.dp, end = 12.dp)
)
}
}
} else {
Box(
modifier = Modifier
.background(
color = SignalTheme.colors.colorSurface1,
shape = RoundedCornerShape(10.dp)
)
.padding(24.dp)
) {
Text(
text = backupKeyString,
style = MaterialTheme.typography.bodyLarge
.copy(
fontSize = 18.sp,
fontWeight = FontWeight(400),
letterSpacing = 1.44.sp,
lineHeight = 36.sp,
textAlign = TextAlign.Center,
fontFamily = MonoTypeface.fontFamily()
)
)
}
}
) {
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
)
}
}
if (AndroidCredentialRepository.isCredentialManagerSupported) {
if (!showAsPasskey || showExpandedPasskey) {
item {
Buttons.Small(
onClick = {
if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
displayRecoveryKeyCopyWarning = true
} else {
onCopyToClipboardClick(backupKeyString)
}
}
) {
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
)
}
}
}
if (!showAsPasskey && AndroidCredentialRepository.isCredentialManagerSupported(context)) {
item {
Buttons.Small(
onClick = { onRequestSaveToPasswordManager() }
@@ -300,6 +435,10 @@ fun MessageBackupsKeyRecordScreen(
is MessageBackupsKeyRecordMode.CreateNewKey -> {
CreateNewKeyButton(mode)
}
is MessageBackupsKeyRecordMode.Passkey -> {
SaveButtons(mode)
}
}
}
@@ -326,7 +465,12 @@ fun MessageBackupsKeyRecordScreen(
is BackupKeySaveState.Success -> {
val snackbarMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager_success)
LaunchedEffect(keySaveState) {
snackbarHostState.showSnackbar(snackbarMessage)
if (showAsPasskey) {
displayConfirmKey = true
} else {
snackbarHostState.showSnackbar(snackbarMessage)
}
onSaveStateCleared()
}
}
@@ -343,6 +487,24 @@ fun MessageBackupsKeyRecordScreen(
}
}
@Composable
private fun SaveButtons(mode: MessageBackupsKeyRecordMode.Passkey) {
Buttons.LargeTonal(
onClick = mode.onSaveToPasswordManager
) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager))
}
TextButton(
onClick = mode.onSaveManually,
modifier = Modifier
.padding(vertical = 24.dp)
.horizontalGutters()
) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__save_key_manually))
}
}
@Composable
private fun NextButton(onNextClick: () -> Unit) {
Box(
@@ -578,6 +740,26 @@ private fun KeyLimitExceededDialog(
)
}
@Composable
private fun ConfirmationFailureDialog(mode: MessageBackupsKeyRecordMode, onDismiss: () -> Unit) {
Dialogs.AdvancedAlertDialog(
title = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_error),
body = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_error_body),
positive = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager),
onPositive = {
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveToPasswordManager()
onDismiss()
},
neutral = stringResource(R.string.MessageBackupsKeyRecordScreen__save_key_manually),
onNeutral = {
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveManually()
onDismiss()
},
negative = stringResource(android.R.string.cancel),
onNegative = onDismiss
)
}
private suspend fun saveKeyToCredentialManager(
@UiContext activityContext: Context,
backupKey: String
@@ -589,6 +771,13 @@ private suspend fun saveKeyToCredentialManager(
)
}
private suspend fun getKeyFromCredentialManager(
@UiContext activityContext: Context,
id: String
): String? {
return AndroidCredentialRepository.getCredential(activityContext, id)
}
@DayNightPreviews
@Composable
private fun MessageBackupsKeyRecordScreenPreview() {
@@ -621,6 +810,23 @@ private fun MessageBackupsKeyRecordScreenSameAsOnDeviceKeyPreview() {
}
}
@DayNightPreviews
@Composable
private fun MessageBackupsKeySaveScreenPreview() {
Previews.Preview {
MessageBackupsKeyRecordScreen(
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
keySaveState = null,
canOpenPasswordManagerSettings = true,
mode = MessageBackupsKeyRecordMode.Passkey(
onSaveToPasswordManager = {},
onSaveManually = {},
onSaveSuccessful = {}
)
)
}
}
@DayNightPreviews
@Composable
private fun SaveKeyConfirmationDialogPreview() {
@@ -15,6 +15,7 @@ enum class MessageBackupsStage(
EDUCATION(route = Route.EDUCATION),
BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION),
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),
TYPE_SELECTION(route = Route.TYPE_SELECTION),
CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION),
@@ -32,6 +33,7 @@ enum class MessageBackupsStage(
EDUCATION,
BACKUP_KEY_EDUCATION,
BACKUP_KEY_RECORD,
BACKUP_KEY_RECORD_MANUALLY,
BACKUP_KEY_VERIFY,
TYPE_SELECTION;
@@ -34,6 +34,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.util.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.viewModel
/**
@@ -61,6 +62,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
MessageBackupsFlowViewModel(
initialTierSelection = MessageBackupTier.PAID,
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext()),
startScreen = MessageBackupsStage.TYPE_SELECTION
)
}
@@ -27,6 +27,9 @@ interface BackupKeyCredentialManagerHandler {
/** Called when the user confirms they want to save the backup key to the password manager. */
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
/** Called to clear the key save state. */
fun onBackupKeySaveStateCleared() = updateBackupKeySaveState(null)
/** Handles the password manager save operation response. */
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
when (result) {
@@ -14,11 +14,15 @@ import android.view.autofill.AutofillManager
import androidx.core.content.getSystemService
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPasswordOption
import androidx.credentials.PasswordCredential
import androidx.credentials.exceptions.CreateCredentialCancellationException
import androidx.credentials.exceptions.CreateCredentialInterruptedException
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialException
import org.signal.core.util.logging.Log
/**
@@ -31,7 +35,13 @@ object AndroidCredentialRepository {
private const val ERROR_CODE_MISSING_CREDENTIAL_MANAGER = "[28434]"
private const val ERROR_CODE_SAVE_PROMPT_DISABLED = "[28435]"
val isCredentialManagerSupported: Boolean = Build.VERSION.SDK_INT >= 19
fun isCredentialManagerSupported(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= 26) {
context.getSystemService<AutofillManager>()?.isEnabled == true
} else {
true
}
}
suspend fun saveCredential(
activityContext: Context,
@@ -81,6 +91,27 @@ object AndroidCredentialRepository {
}
}
/**
* Retrieves a previously saved password credential by username. Returns null if the credential
* cannot be found or if retrieval fails.
*/
suspend fun getCredential(
activityContext: Context,
id: String
): String? = try {
val result = CredentialManager.create(activityContext).getCredential(activityContext, GetCredentialRequest(listOf(GetPasswordOption())))
val credential = result.credential
if (credential is PasswordCredential && credential.id == id) {
credential.password
} else {
Log.w(TAG, "Failed to find credential from password manager")
null
}
} catch (e: GetCredentialException) {
Log.w(TAG, "Failed to find credential from password manager.", e)
null
}
/**
* Returns an [Intent] that can be used to launch the device's password manager settings.
*/
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="80"
android:viewportHeight="80">
<path
android:pathData="M40,40m-40,0a40,40 0,1 1,80 0a40,40 0,1 1,-80 0"
android:fillColor="#E0E5FF"/>
<path
android:pathData="M39.99,24.073C39.001,24.073 38.168,23.256 38.168,22.287C38.168,21.298 39.001,20.5 39.99,20.5C40.999,20.5 41.813,21.298 41.813,22.287C41.813,23.256 40.999,24.073 39.99,24.073ZM45.574,24.966C44.586,24.966 43.752,24.149 43.752,23.18C43.752,22.191 44.586,21.393 45.574,21.393C46.583,21.393 47.397,22.191 47.397,23.18C47.397,24.149 46.583,24.966 45.574,24.966ZM34.406,24.966C33.417,24.966 32.584,24.149 32.584,23.18C32.584,22.191 33.417,21.393 34.406,21.393C35.414,21.393 36.229,22.191 36.229,23.18C36.229,24.149 35.414,24.966 34.406,24.966ZM50.654,27.475C49.646,27.475 48.832,26.658 48.832,25.689C48.832,24.7 49.646,23.902 50.654,23.902C51.643,23.902 52.477,24.7 52.477,25.689C52.477,26.658 51.643,27.475 50.654,27.475ZM29.346,27.475C28.337,27.475 27.523,26.658 27.523,25.689C27.523,24.7 28.337,23.902 29.346,23.902C30.354,23.902 31.168,24.7 31.168,25.689C31.168,26.658 30.354,27.475 29.346,27.475ZM54.649,31.39C53.66,31.39 52.826,30.592 52.826,29.604C52.826,28.635 53.66,27.817 54.649,27.817C55.657,27.817 56.471,28.635 56.471,29.604C56.471,30.592 55.657,31.39 54.649,31.39ZM25.332,31.39C24.343,31.39 23.51,30.592 23.51,29.604C23.51,28.635 24.343,27.817 25.332,27.817C26.34,27.817 27.155,28.635 27.155,29.604C27.155,30.592 26.34,31.39 25.332,31.39ZM37.916,49.028C37.198,49.028 36.636,48.743 36.112,48.077L31.537,42.604C31.207,42.205 31.032,41.787 31.032,41.33C31.032,40.38 31.789,39.639 32.739,39.639C33.301,39.639 33.766,39.848 34.232,40.437L37.838,44.942L45.497,32.892C45.904,32.246 46.428,31.941 47.009,31.941C47.921,31.941 48.774,32.569 48.774,33.5C48.774,33.937 48.541,34.393 48.289,34.792L39.622,48.077C39.215,48.686 38.614,49.028 37.916,49.028ZM57.208,36.294C56.2,36.294 55.385,35.496 55.385,34.507C55.385,33.538 56.2,32.721 57.208,32.721C58.216,32.721 59.03,33.538 59.03,34.507C59.03,35.496 58.216,36.294 57.208,36.294ZM22.792,36.294C21.784,36.294 20.969,35.496 20.969,34.507C20.969,33.538 21.784,32.721 22.792,32.721C23.781,32.721 24.615,33.538 24.615,34.507C24.615,35.496 23.781,36.294 22.792,36.294ZM58.158,41.786C57.169,41.786 56.335,40.988 56.335,40C56.335,39.012 57.169,38.213 58.158,38.213C59.166,38.213 60,39.012 60,40C60,40.988 59.166,41.786 58.158,41.786ZM21.823,41.786C20.814,41.786 20,40.988 20,40C20,39.012 20.814,38.213 21.823,38.213C22.831,38.213 23.645,39.012 23.645,40C23.645,40.988 22.831,41.786 21.823,41.786ZM57.208,47.279C56.2,47.279 55.385,46.462 55.385,45.493C55.385,44.504 56.2,43.706 57.208,43.706C58.216,43.706 59.03,44.504 59.03,45.493C59.03,46.462 58.216,47.279 57.208,47.279ZM22.792,47.279C21.784,47.279 20.969,46.462 20.969,45.493C20.969,44.504 21.784,43.706 22.792,43.706C23.781,43.706 24.615,44.504 24.615,45.493C24.615,46.462 23.781,47.279 22.792,47.279ZM54.649,52.183C53.66,52.183 52.826,51.366 52.826,50.396C52.826,49.408 53.66,48.61 54.649,48.61C55.657,48.61 56.471,49.408 56.471,50.396C56.471,51.366 55.657,52.183 54.649,52.183ZM25.332,52.183C24.343,52.183 23.51,51.366 23.51,50.396C23.51,49.408 24.343,48.61 25.332,48.61C26.34,48.61 27.155,49.408 27.155,50.396C27.155,51.366 26.34,52.183 25.332,52.183ZM50.654,56.117C49.646,56.117 48.832,55.3 48.832,54.33C48.832,53.342 49.646,52.525 50.654,52.525C51.643,52.525 52.477,53.342 52.477,54.33C52.477,55.3 51.643,56.117 50.654,56.117ZM29.346,56.117C28.337,56.117 27.523,55.3 27.523,54.33C27.523,53.342 28.337,52.525 29.346,52.525C30.354,52.525 31.168,53.342 31.168,54.33C31.168,55.3 30.354,56.117 29.346,56.117ZM45.574,58.607C44.586,58.607 43.752,57.808 43.752,56.82C43.752,55.851 44.586,55.034 45.574,55.034C46.583,55.034 47.397,55.851 47.397,56.82C47.397,57.808 46.583,58.607 45.574,58.607ZM34.406,58.607C33.417,58.607 32.584,57.808 32.584,56.82C32.584,55.851 33.417,55.034 34.406,55.034C35.414,55.034 36.229,55.851 36.229,56.82C36.229,57.808 35.414,58.607 34.406,58.607ZM39.99,59.5C39.001,59.5 38.168,58.702 38.168,57.714C38.168,56.744 39.001,55.927 39.99,55.927C40.999,55.927 41.813,56.744 41.813,57.714C41.813,58.702 40.999,59.5 39.99,59.5Z"
android:fillColor="#3B45FD"/>
</vector>
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M2.813,5.416C2.813,3.518 4.352,1.979 6.25,1.979C7.912,1.979 9.3,3.159 9.619,4.728C9.699,5.123 10.084,5.378 10.478,5.298C10.873,5.217 11.128,4.833 11.048,4.438C10.594,2.203 8.619,0.521 6.25,0.521C3.546,0.521 1.354,2.712 1.354,5.416C1.354,6.592 1.769,7.672 2.46,8.516C2.715,8.828 3.175,8.873 3.486,8.618C3.798,8.363 3.844,7.904 3.589,7.592C3.103,6.999 2.813,6.243 2.813,5.416Z"
android:fillColor="#000000"/>
<path
android:pathData="M5.597,3.557C4.57,3.931 4.041,5.067 4.415,6.094L5.789,9.88C6.079,10.666 6.285,11.099 6.694,11.861C6.442,11.551 6.258,11.35 6.001,11.178C5.124,10.64 4.035,10.933 3.393,11.514C3.228,11.663 3.051,11.852 2.917,12.035C2.449,12.677 2.564,13.496 3.175,14.03L6.944,17.5C8.887,19.284 11.871,20.119 14.395,19.2C16.25,18.525 17.582,17.33 18.256,15.775C18.927,14.226 18.907,12.408 18.23,10.549L17.605,8.831C17.259,7.882 16.62,7.218 15.788,7.004C15.236,6.863 14.676,6.94 14.167,7.178C13.801,6.715 13.323,6.4 12.776,6.29C12.158,6.166 11.548,6.323 11.042,6.697C10.756,6.512 10.429,6.397 10.084,6.365C9.65,6.326 9.217,6.42 8.826,6.64L8.134,4.74C7.76,3.713 6.625,3.183 5.597,3.557ZM15.113,9.295L14.818,8.483C15.047,8.384 15.257,8.374 15.425,8.417C15.681,8.483 16.01,8.715 16.234,9.33L16.859,11.048C17.436,12.631 17.412,14.055 16.917,15.195C16.426,16.329 15.432,17.271 13.896,17.83C11.973,18.53 9.653,17.933 7.925,16.421C6.649,15.246 5.361,14.082 4.095,12.895C4.184,12.793 4.271,12.686 4.372,12.595C4.84,12.171 5.386,12.376 5.789,12.739L7.477,14.259C7.726,14.483 8.095,14.509 8.373,14.322C8.65,14.134 8.765,13.782 8.65,13.468L5.785,5.595C5.687,5.325 5.826,5.026 6.096,4.928C6.367,4.829 6.665,4.969 6.764,5.239L7.83,8.169C7.832,8.174 7.834,8.178 7.835,8.183L9.044,11.503C9.182,11.882 9.6,12.077 9.979,11.939C10.357,11.802 10.552,11.383 10.414,11.005L9.342,8.059C9.559,7.852 9.783,7.802 9.952,7.818C10.191,7.839 10.429,7.998 10.538,8.299L11.393,10.649C11.531,11.027 11.949,11.222 12.328,11.084C12.706,10.946 12.901,10.528 12.764,10.15L11.928,7.854C12.13,7.713 12.329,7.688 12.488,7.72C12.736,7.77 13.09,8 13.316,8.621L13.742,9.793C13.88,10.172 14.299,10.367 14.677,10.229C15.055,10.092 15.25,9.673 15.113,9.295Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>
+18
View File
@@ -9114,6 +9114,24 @@
<string name="MessageBackupsKeyRecordScreen__you_have_not_finished_setting_up_backups">You have not finished setting up backups, and no backup has been created. You can enable backups in Settings.</string>
<!-- Dialog confirmation button to exit backup setup -->
<string name="MessageBackupsKeyRecordScreen__exit_backup_setup_confirm">Exit backup setup</string>
<!-- Screen title when saving your recovery key -->
<string name="MessageBackupsKeyRecordScreen__save_your_recovery_key">Save your recovery key</string>
<!-- Screen subtitle for the recovery key screen -->
<string name="MessageBackupsKeyRecordScreen__your_recovery_key">Your recovery key is a 64-character code that lets you restore your backup. If you forget your key, you will not be able to recover your account.</string>
<!-- Hint text to see the full recovery key -->
<string name="MessageBackupsKeyRecordScreen__see_full_key">See full key</string>
<!-- Bottom sheet caption to confirm your recovery key -->
<string name="MessageBackupsKeyRecordScreen__confirm_that_your_recovery">Confirm that your recovery key was recorded correctly. You will be prompted to fill your saved password in the next step.</string>
<!-- Button to confirm the recovery key -->
<string name="MessageBackupsKeyRecordScreen__confirm_recovery">Confirm recovery key</string>
<!-- Toast shown when the key is confirmed -->
<string name="MessageBackupsKeyRecordScreen__recover_key_confirmed">Recovery key confirmed</string>
<!-- Dialog title shown when the recovery key fails to confirm -->
<string name="MessageBackupsKeyRecordScreen__recover_key_error">Error confirming recovery key</string>
<!-- Dialog body shown when the recovery key fails to confirm -->
<string name="MessageBackupsKeyRecordScreen__recover_key_error_body">Your recovery key could not be confirmed. Make sure youve saved it in your password manager, or save it manually.</string>
<!-- Button option to save the key manually -->
<string name="MessageBackupsKeyRecordScreen__save_key_manually">Save key manually</string>
<!-- BackupKeyDisplayFragment -->
<!-- Dialog title when exiting before confirming new key -->