mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add ability to save remote backup key to device password manager.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
committed by
Cody Henthorne
parent
7616ec1fd2
commit
015fc9be2c
@@ -585,6 +585,8 @@ dependencies {
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.rxdogtag)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.compat)
|
||||
|
||||
"playImplementation"(project(":billing"))
|
||||
"nightlyImplementation"(project(":billing"))
|
||||
|
||||
@@ -44,4 +44,7 @@
|
||||
|
||||
<issue id="OptionalUsedAsFieldOrParameterType" severity="ignore" />
|
||||
<issue id="SameParameterValue" severity="ignore" />
|
||||
|
||||
<!-- Disables check for digital asset linking in manifest. It's not needed, since we are not using CredentialManager for passkey support. -->
|
||||
<issue id="CredManMissingDal" severity="ignore" />
|
||||
</lint>
|
||||
|
||||
@@ -142,11 +142,15 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = state.accountEntropyPool.displayValue,
|
||||
keySaveState = state.backupKeySaveState,
|
||||
onNavigationClick = viewModel::goToPreviousStage,
|
||||
onNextClick = viewModel::goToNextStage,
|
||||
onCopyToClipboardClick = {
|
||||
Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS)
|
||||
}
|
||||
},
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted
|
||||
)
|
||||
}
|
||||
|
||||
@@ -191,6 +195,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
requireActivity().setResult(Activity.RESULT_OK, MessageBackupsCheckoutActivity.createResultData())
|
||||
requireActivity().finishAfterTransition()
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
@@ -21,7 +22,8 @@ data class MessageBackupsFlowState(
|
||||
val stage: MessageBackupsStage = startScreen,
|
||||
val accountEntropyPool: AccountEntropyPool = SignalStore.account.accountEntropyPool,
|
||||
val failure: Throwable? = null,
|
||||
val paymentReadyState: PaymentReadyState = PaymentReadyState.NOT_READY
|
||||
val paymentReadyState: PaymentReadyState = PaymentReadyState.NOT_READY,
|
||||
val backupKeySaveState: BackupKeySaveState? = null
|
||||
) {
|
||||
enum class PaymentReadyState {
|
||||
NOT_READY,
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
@@ -50,7 +52,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class MessageBackupsFlowViewModel(
|
||||
initialTierSelection: MessageBackupTier?,
|
||||
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
||||
) : ViewModel() {
|
||||
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MessageBackupsFlowViewModel::class)
|
||||
@@ -342,4 +344,8 @@ class MessageBackupsFlowViewModel(
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
|
||||
internalStateFlow.update { it.copy(backupKeySaveState = newState) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -16,11 +18,14 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -29,12 +34,22 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CredentialManager
|
||||
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
||||
import androidx.credentials.exceptions.CreateCredentialInterruptedException
|
||||
import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
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.SignalPreview
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.CredentialManagerError
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.CredentialManagerResult
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -45,10 +60,15 @@ import org.signal.core.ui.R as CoreUiR
|
||||
@Composable
|
||||
fun MessageBackupsKeyRecordScreen(
|
||||
backupKey: String,
|
||||
keySaveState: BackupKeySaveState?,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onCopyToClipboardClick: (String) -> Unit = {},
|
||||
onRequestSaveToPasswordManager: () -> Unit = {},
|
||||
onConfirmSaveToPasswordManager: () -> Unit = {},
|
||||
onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {},
|
||||
onNextClick: () -> Unit = {}
|
||||
) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val backupKeyString = remember(backupKey) {
|
||||
backupKey.chunked(4).joinToString(" ")
|
||||
}
|
||||
@@ -56,99 +76,182 @@ fun MessageBackupsKeyRecordScreen(
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
|
||||
onNavigationClick = onNavigationClick
|
||||
onNavigationClick = onNavigationClick,
|
||||
snackbarHost = { Snackbars.Host(snackbarHostState = snackbarHostState) }
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.testTag("message-backups-key-record-screen-lazy-column")
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.image_signal_backups_lock),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.size(80.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Buttons.Small(
|
||||
onClick = { onCopyToClipboardClick(backupKeyString) }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClick,
|
||||
modifier = Modifier.align(Alignment.BottomEnd)
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.testTag("message-backups-key-record-screen-lazy-column")
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__next)
|
||||
)
|
||||
item {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.image_signal_backups_lock),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.size(80.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Buttons.Small(
|
||||
onClick = { onCopyToClipboardClick(backupKeyString) }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
item {
|
||||
Buttons.Small(
|
||||
onClick = { onRequestSaveToPasswordManager() }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClick,
|
||||
modifier = Modifier.align(Alignment.BottomEnd)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__next)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (keySaveState) {
|
||||
is BackupKeySaveState.RequestingConfirmation -> {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_save_to_password_manager_title),
|
||||
body = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_save_to_password_manager_body),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onSaveToPasswordManagerComplete(CredentialManagerResult.UserCanceled) },
|
||||
confirm = stringResource(R.string.MessageBackupsKeyRecordScreen__continue),
|
||||
onConfirm = onConfirmSaveToPasswordManager
|
||||
)
|
||||
}
|
||||
|
||||
is BackupKeySaveState.AwaitingCredentialManager -> {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(keySaveState) {
|
||||
val result = saveKeyToCredentialManager(context, backupKey)
|
||||
onSaveToPasswordManagerComplete(result)
|
||||
}
|
||||
}
|
||||
|
||||
is BackupKeySaveState.Success -> {
|
||||
val snackbarMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager_success)
|
||||
LaunchedEffect(keySaveState) {
|
||||
snackbarHostState.showSnackbar(snackbarMessage)
|
||||
}
|
||||
}
|
||||
|
||||
is BackupKeySaveState.Error -> {
|
||||
if (keySaveState.errorType is CredentialManagerError.MissingCredentialManager) {
|
||||
Dialogs.SimpleMessageDialog(
|
||||
title = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_title),
|
||||
message = stringResource(R.string.MessageBackupsKeyRecordScreen__missing_password_manager_message),
|
||||
dismiss = stringResource(R.string.MessageBackupsKeyRecordScreen__dismiss),
|
||||
onDismiss = { onSaveToPasswordManagerComplete(CredentialManagerResult.UserCanceled) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveKeyToCredentialManager(
|
||||
context: Context,
|
||||
backupKey: String
|
||||
): CredentialManagerResult {
|
||||
return try {
|
||||
CredentialManager.create(context)
|
||||
.createCredential(
|
||||
context = context,
|
||||
request = CreatePasswordRequest(
|
||||
id = context.getString(R.string.RemoteBackupsSettingsFragment__signal_backups),
|
||||
password = backupKey,
|
||||
preferImmediatelyAvailableCredentials = false,
|
||||
isAutoSelectAllowed = false
|
||||
)
|
||||
)
|
||||
CredentialManagerResult.Success
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is CreateCredentialCancellationException -> CredentialManagerResult.UserCanceled
|
||||
is CreateCredentialInterruptedException -> CredentialManagerResult.Interrupted(e)
|
||||
is CreateCredentialNoCreateOptionException -> CredentialManagerError.MissingCredentialManager(e)
|
||||
else -> CredentialManagerError.Unexpected(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,7 +261,19 @@ fun MessageBackupsKeyRecordScreen(
|
||||
private fun MessageBackupsKeyRecordScreenPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0"
|
||||
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
|
||||
keySaveState = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun SaveKeyConfirmationDialogPreview() {
|
||||
Previews.Preview {
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
|
||||
keySaveState = BackupKeySaveState.RequestingConfirmation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.logging.logW
|
||||
|
||||
/**
|
||||
* Handles the process of storing a backup key to the device password manager.
|
||||
*/
|
||||
interface BackupKeyCredentialManagerHandler {
|
||||
companion object {
|
||||
private val TAG = Log.tag(BackupKeyCredentialManagerHandler::class)
|
||||
}
|
||||
|
||||
/** Updates the [BackupKeySaveState]. Implementers must update their associated state to match [newState]. */
|
||||
fun updateBackupKeySaveState(newState: BackupKeySaveState?)
|
||||
|
||||
/** Called when the user initiates the backup key save flow. */
|
||||
fun onBackupKeySaveRequested() = updateBackupKeySaveState(BackupKeySaveState.RequestingConfirmation)
|
||||
|
||||
/** Called when the user confirms they want to save the backup key to the password manager. */
|
||||
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
|
||||
|
||||
/** Handles the password manager save operation response. */
|
||||
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
|
||||
when (result) {
|
||||
is CredentialManagerResult.Success -> {
|
||||
Log.d(TAG, "Successfully saved backup key to credential manager.")
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.Success)
|
||||
}
|
||||
|
||||
is CredentialManagerResult.UserCanceled -> {
|
||||
Log.d(TAG, "User canceled saving backup key to credential manager.")
|
||||
updateBackupKeySaveState(newState = null)
|
||||
}
|
||||
|
||||
is CredentialManagerResult.Interrupted -> {
|
||||
Log.i(TAG, "Retry saving backup key to credential manager after interruption.", result.exception)
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.AwaitingCredentialManager(isRetry = true))
|
||||
}
|
||||
|
||||
is CredentialManagerError.MissingCredentialManager -> {
|
||||
Log.w(TAG, "Error saving backup key to credential manager: no credential manager is configured.", result.exception)
|
||||
updateBackupKeySaveState(newState = BackupKeySaveState.Error(result))
|
||||
}
|
||||
|
||||
is CredentialManagerError.Unexpected -> {
|
||||
throw result.exception.logW(TAG, "Unexpected error when saving backup key to credential manager.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents state related to saving a backup key to the device password manager. */
|
||||
sealed interface BackupKeySaveState {
|
||||
/** Awaiting the user to confirm they want to save the backup key. */
|
||||
data object RequestingConfirmation : BackupKeySaveState
|
||||
|
||||
/** Awaiting the password manager's response for the backup key save operation. */
|
||||
data class AwaitingCredentialManager(val isRetry: Boolean) : BackupKeySaveState
|
||||
data object Success : BackupKeySaveState
|
||||
data class Error(val errorType: CredentialManagerError) : BackupKeySaveState
|
||||
}
|
||||
|
||||
sealed interface CredentialManagerResult {
|
||||
data object Success : CredentialManagerResult
|
||||
data object UserCanceled : CredentialManagerResult
|
||||
|
||||
/** The backup key save operation was interrupted and should be retried. */
|
||||
data class Interrupted(val exception: Exception) : CredentialManagerResult
|
||||
}
|
||||
|
||||
sealed class CredentialManagerError : CredentialManagerResult {
|
||||
abstract val exception: Exception
|
||||
|
||||
/** No password manager is configured on the device. */
|
||||
data class MissingCredentialManager(override val exception: Exception) : CredentialManagerError()
|
||||
data class Unexpected(override val exception: Exception) : CredentialManagerError()
|
||||
}
|
||||
@@ -6,11 +6,14 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Fragment which only displays the backup key to the user.
|
||||
@@ -21,12 +24,20 @@ class BackupKeyDisplayFragment : ComposeFragment() {
|
||||
const val CLIPBOARD_TIMEOUT_SECONDS = 60
|
||||
}
|
||||
|
||||
private val viewModel: BackupKeyDisplayViewModel by viewModel { BackupKeyDisplayViewModel() }
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
MessageBackupsKeyRecordScreen(
|
||||
backupKey = SignalStore.account.accountEntropyPool.displayValue,
|
||||
keySaveState = state.keySaveState,
|
||||
onNavigationClick = { findNavController().popBackStack() },
|
||||
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
|
||||
onNextClick = { findNavController().popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups.remote
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class BackupKeyDisplayViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
|
||||
val uiState: StateFlow<BackupKeyDisplayUiState> = _uiState.asStateFlow()
|
||||
|
||||
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
|
||||
_uiState.update { it.copy(keySaveState = newState) }
|
||||
}
|
||||
}
|
||||
|
||||
data class BackupKeyDisplayUiState(
|
||||
val keySaveState: BackupKeySaveState? = null
|
||||
)
|
||||
@@ -8279,6 +8279,20 @@
|
||||
<string name="MessageBackupsKeyRecordScreen__this_key_is_required_to_recover">This key is required to recover your account and data. Store this key somewhere safe. If you lose it, you won’t be able to recover your account.</string>
|
||||
<!-- Copy to clipboard button label -->
|
||||
<string name="MessageBackupsKeyRecordScreen__copy_to_clipboard">Copy to clipboard</string>
|
||||
<!-- Label for the button to save a backup key to the device password manager. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__save_to_password_manager">Save to password manager</string>
|
||||
<!-- Dialog title confirming that the user wants to save a backup key to the device password manager. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__confirm_save_to_password_manager_title">Save to password manager?</string>
|
||||
<!-- Dialog message confirming that the user wants to save a backup key to the device password manager. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__confirm_save_to_password_manager_body">Only store your backup key in a password manager that you trust is secure. Signal does not make a recommendation on which password manager is right for you.</string>
|
||||
<!-- Message shown after the backup key is successfully saved to the device password manager. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__save_to_password_manager_success">Saved to your password manager</string>
|
||||
<!-- Dialog title shown when there is an error saving the backup key because no compatible password manager was detected. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__missing_password_manager_title">Password Manager Unavailable</string>
|
||||
<!-- Dialog message shown when there is an error saving the backup key because no compatible password manager was detected. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__missing_password_manager_message">Unable to save your backup key. Check your device settings to install or enable a compatible password manager.</string>
|
||||
<!-- Action button to dismiss a pop up dialog. -->
|
||||
<string name="MessageBackupsKeyRecordScreen__dismiss">Dismiss</string>
|
||||
<!-- Action button label -->
|
||||
<string name="MessageBackupsKeyRecordScreen__next">Next</string>
|
||||
<!-- Bottom sheet title -->
|
||||
|
||||
@@ -117,6 +117,8 @@ androidx-asynclayoutinflater = "androidx.asynclayoutinflater:asynclayoutinflater
|
||||
androidx-asynclayoutinflater-appcompat = "androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01"
|
||||
androidx-emoji2 = "androidx.emoji2:emoji2:1.5.0"
|
||||
androidx-documentfile = "androidx.documentfile:documentfile:1.0.1"
|
||||
androidx-credentials = "androidx.credentials:credentials:1.5.0"
|
||||
androidx-credentials-compat = "androidx.credentials:credentials-play-services-auth:1.5.0"
|
||||
android-billing = "com.android.billingclient:billing-ktx:7.1.1"
|
||||
|
||||
# Billing
|
||||
|
||||
@@ -1295,6 +1295,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="4d46646066c794f2812d5b33a9422d37e4f918c4d809cd1fb2579c7022ef2818" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.credentials" name="credentials" version="1.5.0">
|
||||
<artifact name="credentials-1.5.0.aar">
|
||||
<sha256 value="00162f3d0244a42a8b9527f3cc6fb3bf36d655960c65b979bef250e5116f1453" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="credentials-1.5.0.module">
|
||||
<sha256 value="8c66e6e20db02c8e8fc2e795e2f7ecb5340d76e87f42f4d49e512e45be140511" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.credentials" name="credentials-play-services-auth" version="1.5.0">
|
||||
<artifact name="credentials-play-services-auth-1.5.0.aar">
|
||||
<sha256 value="713644485b1cb44cc4e4f1aa76a9835cdfaaf855e11739b8b25c34d3f06b5ee4" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="credentials-play-services-auth-1.5.0.module">
|
||||
<sha256 value="e7ebcef9890c76dbdafc124a9e9a8848ba9e798a02635bd59b5fe7682f11207e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.cursoradapter" name="cursoradapter" version="1.0.0">
|
||||
<artifact name="cursoradapter-1.0.0.aar">
|
||||
<sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/>
|
||||
@@ -3949,6 +3965,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="d8bff2c98d239633fb550229e527779425bfb51ff04b96ac0f12d3acba0b1f42" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-auth-blockstore" version="16.4.0">
|
||||
<artifact name="play-services-auth-blockstore-16.4.0.aar">
|
||||
<sha256 value="7e70f20ca8710a783bfbdb268b35603105089e55134313c977e5ba6a0fae4938" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-base" version="18.5.0">
|
||||
<artifact name="play-services-base-18.5.0.aar">
|
||||
<sha256 value="59a5c0c2da12311d75d965ce1f419498536b1a167fb28ff7dfc2dfd9cefa4157" origin="Generated by Gradle"/>
|
||||
@@ -3969,11 +3990,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="b3526f0caedaa96c526cc4c9ba94b229dad5afe31f32f46f6cd668a03624fd42" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-fido" version="21.0.0">
|
||||
<artifact name="play-services-fido-21.0.0.aar">
|
||||
<sha256 value="e5c10dad0192a37768a412650567e2e3457c7092b00dd8c11ec97bc36f8fa860" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-identity" version="18.1.0">
|
||||
<artifact name="play-services-identity-18.1.0.aar">
|
||||
<sha256 value="6a0a3ed6b662399e5be3b3e0a7008ccc4c6b0fbd3f35fa235e03db133d6efa40" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-identity-credentials" version="16.0.0-alpha02">
|
||||
<artifact name="play-services-identity-credentials-16.0.0-alpha02.aar">
|
||||
<sha256 value="3c335a99839f86ea11b6fac7b0eebf60fdc428b038df3326c930c4ce6262504b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.gms" name="play-services-location" version="19.0.0">
|
||||
<artifact name="play-services-location-19.0.0.aar">
|
||||
<sha256 value="6b205c43ba5df751eca8ce9dae7a58effafac7d637fb4fc708a7522d1b99cf80" origin="Generated by Gradle"/>
|
||||
@@ -4009,6 +4040,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
||||
<sha256 value="c4327257a478febf00da8b921e39052b6aa44db9cf61e00c3d4bf77ec8091ce7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.libraries.identity.googleid" name="googleid" version="1.1.0">
|
||||
<artifact name="googleid-1.1.0.aar">
|
||||
<sha256 value="194ac1fc1986dd1f62046fae37ddf77e63770fdc1f3d34baaa397cfbf4d191a2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="com.google.android.material" name="material" version="1.12.0">
|
||||
<artifact name="material-1.12.0.aar">
|
||||
<sha256 value="4a672941b626b9ab91ae893ed22598ea53ad69125c858c0a59fa9b90daa5cb08" origin="Generated by Gradle"/>
|
||||
|
||||
Reference in New Issue
Block a user