diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab79d38ed4..5f0ef077a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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")) diff --git a/app/lint.xml b/app/lint.xml index 822499eace..20134c9334 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -44,4 +44,7 @@ + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index b8ece27212..0791c7ba00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt index 155e64d2a7..9cca6c2585 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription import androidx.compose.runtime.Immutable import org.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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index d54f897918..b102990fce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -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) } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index 2e98c779ca..441693f7a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -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 ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt new file mode 100644 index 0000000000..bc99ccb00a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyCredentialManagerHandler.kt @@ -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() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt index 1930d7fe97..da93b4af88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt @@ -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() } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt new file mode 100644 index 0000000000..0f6e2c012f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt @@ -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 = _uiState.asStateFlow() + + override fun updateBackupKeySaveState(newState: BackupKeySaveState?) { + _uiState.update { it.copy(keySaveState = newState) } + } +} + +data class BackupKeyDisplayUiState( + val keySaveState: BackupKeySaveState? = null +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed481f8162..6f79abe8d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8279,6 +8279,20 @@ 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. Copy to clipboard + + Save to password manager + + Save to password manager? + + 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. + + Saved to your password manager + + Password Manager Unavailable + + Unable to save your backup key. Check your device settings to install or enable a compatible password manager. + + Dismiss Next diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7be1aee1e6..76dded12d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 292271aaa4..e6af11f62d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1295,6 +1295,22 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + + + + + + + @@ -3949,6 +3965,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3969,11 +3990,21 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + + + @@ -4009,6 +4040,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + +