mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 13:31:18 +01:00
Present the backup key as a passphrase.
This commit is contained in:
committed by
jeffrey-signal
parent
66b6c1656c
commit
5c64a91864
+85
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -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,
|
||||
|
||||
+12
-3
@@ -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(
|
||||
|
||||
+243
-37
@@ -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() {
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
|
||||
+2
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
+3
@@ -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) {
|
||||
|
||||
+32
-1
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user