Add verify key megaphone.

This commit is contained in:
Michelle Tang
2025-07-10 13:22:30 -04:00
committed by Jeffrey Starke
parent c6afa17330
commit 359f473b59
13 changed files with 605 additions and 148 deletions

View File

@@ -0,0 +1,173 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.fonts.MonoTypeface
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
import org.whispersystems.signalservice.api.AccountEntropyPool
/**
* Screen to enter backup key with an option to view the backup key again
*/
@Composable
fun EnterKeyScreen(
paddingValues: PaddingValues,
backupKey: String,
onNextClick: () -> Unit,
captionContent: @Composable () -> Unit,
seeKeyButton: @Composable () -> Unit
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
.fillMaxSize()
) {
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }
val visualTransform = remember { BackupKeyVisualTransformation(chunkSize = 4) }
val keyboardController = LocalSoftwareKeyboardController.current
var enteredBackupKey by remember { mutableStateOf("") }
var isBackupKeyValid by remember { mutableStateOf(false) }
var showError by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(weight = 1f, fill = false)
.horizontalGutters(),
horizontalAlignment = Alignment.Start
) {
captionContent()
Spacer(modifier = Modifier.height(48.dp))
val updateEnteredBackupKey = { input: String ->
enteredBackupKey = AccountEntropyPool.removeIllegalCharacters(input).uppercase()
isBackupKeyValid = enteredBackupKey == backupKey
showError = !isBackupKeyValid && enteredBackupKey.length >= backupKey.length
}
var requestFocus: Boolean by remember { mutableStateOf(true) }
val autoFillHelper = backupKeyAutoFillHelper { updateEnteredBackupKey(it) }
TextField(
value = enteredBackupKey,
onValueChange = {
updateEnteredBackupKey(it)
autoFillHelper.onValueChanged(it)
},
label = {
Text(text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__backup_key))
},
textStyle = LocalTextStyle.current.copy(
fontFamily = MonoTypeface.fontFamily(),
lineHeight = 36.sp
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Next,
autoCorrectEnabled = false
),
keyboardActions = KeyboardActions(
onNext = {
if (isBackupKeyValid) {
keyboardController?.hide()
onNextClick()
}
}
),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
),
supportingText = { if (showError) Text(text = stringResource(R.string.MessageBackupsKeyVerifyScreen__incorrect_backup_key)) },
isError = showError,
minLines = 4,
visualTransformation = visualTransform,
modifier = Modifier
.testTag("message-backups-key-verify-screen-backup-key-input-field")
.fillMaxWidth()
.focusRequester(focusRequester)
.attachBackupKeyAutoFillHelper(autoFillHelper)
.onGloballyPositioned {
if (requestFocus) {
focusRequester.requestFocus()
requestFocus = false
}
}
)
}
Surface(
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(top = 8.dp, bottom = 24.dp)
.horizontalGutters()
.fillMaxWidth()
) {
seeKeyButton()
Buttons.LargeTonal(
enabled = isBackupKeyValid,
onClick = onNextClick
) {
Text(
text = stringResource(id = R.string.RegistrationActivity_next)
)
}
}
}
}
}

View File

@@ -6,71 +6,40 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
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.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.fonts.MonoTypeface
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
import org.whispersystems.signalservice.api.AccountEntropyPool
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.random.Random
import kotlin.random.nextInt
import org.signal.core.ui.R as CoreUiR
@@ -78,7 +47,7 @@ import org.signal.core.ui.R as CoreUiR
/**
* Prompt user to re-enter backup key (AEP) to confirm they have it still.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsKeyVerifyScreen(
backupKey: String,
@@ -96,127 +65,28 @@ fun MessageBackupsKeyVerifyScreen(
onNavigationClick = onNavigationClick
) { paddingValues ->
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(paddingValues)
.imePadding()
.fillMaxSize()
) {
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }
val visualTransform = remember { BackupKeyVisualTransformation(chunkSize = 4) }
val keyboardController = LocalSoftwareKeyboardController.current
var enteredBackupKey by remember { mutableStateOf("") }
var isBackupKeyValid by remember { mutableStateOf(false) }
var showError by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(weight = 1f, fill = false)
.horizontalGutters(),
horizontalAlignment = Alignment.Start
) {
EnterKeyScreen(
paddingValues = paddingValues,
backupKey = backupKey,
onNextClick = {
coroutineScope.launch { sheetState.show() }
},
captionContent = {
Text(
text = stringResource(R.string.MessageBackupsKeyVerifyScreen__enter_the_backup_key_that_you_just_recorded),
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
)
Spacer(modifier = Modifier.height(48.dp))
val updateEnteredBackupKey = { input: String ->
enteredBackupKey = AccountEntropyPool.removeIllegalCharacters(input).uppercase()
isBackupKeyValid = enteredBackupKey == backupKey
showError = !isBackupKeyValid && enteredBackupKey.length >= backupKey.length
}
var requestFocus: Boolean by remember { mutableStateOf(true) }
val autoFillHelper = backupKeyAutoFillHelper { updateEnteredBackupKey(it) }
TextField(
value = enteredBackupKey,
onValueChange = {
updateEnteredBackupKey(it)
autoFillHelper.onValueChanged(it)
},
label = {
Text(text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__backup_key))
},
textStyle = LocalTextStyle.current.copy(
fontFamily = MonoTypeface.fontFamily(),
lineHeight = 36.sp
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
capitalization = KeyboardCapitalization.None,
imeAction = ImeAction.Next,
autoCorrectEnabled = false
),
keyboardActions = KeyboardActions(
onNext = {
if (isBackupKeyValid) {
keyboardController?.hide()
coroutineScope.launch { sheetState.show() }
}
}
),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
),
supportingText = { if (showError) Text(text = stringResource(R.string.MessageBackupsKeyVerifyScreen__incorrect_backup_key)) },
isError = showError,
minLines = 4,
visualTransformation = visualTransform,
modifier = Modifier
.testTag("message-backups-key-verify-screen-backup-key-input-field")
.fillMaxWidth()
.focusRequester(focusRequester)
.attachBackupKeyAutoFillHelper(autoFillHelper)
.onGloballyPositioned {
if (requestFocus) {
focusRequester.requestFocus()
requestFocus = false
}
}
)
}
Surface(
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
modifier = Modifier.fillMaxWidth()
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(top = 8.dp, bottom = 24.dp)
.horizontalGutters()
.fillMaxWidth()
},
seeKeyButton = {
TextButton(
onClick = onNavigationClick
) {
TextButton(
onClick = onNavigationClick
) {
Text(
text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__see_key_again)
)
}
Buttons.LargeTonal(
enabled = isBackupKeyValid,
onClick = {
coroutineScope.launch { sheetState.show() }
}
) {
Text(
text = stringResource(id = R.string.RegistrationActivity_next)
)
}
Text(
text = stringResource(id = R.string.MessageBackupsKeyVerifyScreen__see_key_again)
)
}
}
}
)
if (sheetState.isVisible) {
ModalBottomSheet(
@@ -225,6 +95,7 @@ fun MessageBackupsKeyVerifyScreen(
containerColor = SignalTheme.colors.colorSurface1,
onDismissRequest = {
coroutineScope.launch {
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
sheetState.hide()
}
}
@@ -232,6 +103,7 @@ fun MessageBackupsKeyVerifyScreen(
BottomSheetContent(
onContinueClick = {
coroutineScope.launch {
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
sheetState.hide()
}
onNextClick()

View File

@@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.backup.v2.ui.verify
import android.app.Activity.RESULT_OK
import androidx.compose.runtime.Composable
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyVerifyScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Fragment to confirm the backup key just shown after users forget it.
*/
class ConfirmBackupKeyDisplayFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
MessageBackupsKeyVerifyScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
onNavigationClick = {
requireActivity().supportFragmentManager.popBackStack()
},
onNextClick = {
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
SignalStore.backup.hasVerifiedBefore = true
SignalStore.backup.hasSnoozedVerified = false
requireActivity().setResult(RESULT_OK)
requireActivity().finish()
}
)
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.backup.v2.ui.verify
import android.R
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.viewModel
/**
* Fragment which displays the backup key to the user after users forget it.
*/
class ForgotBackupKeyFragment : ComposeFragment() {
companion object {
const val CLIPBOARD_TIMEOUT_SECONDS = 60
}
private val viewModel: ForgotBackupKeyViewModel by viewModel { ForgotBackupKeyViewModel() }
@Composable
override fun FragmentContent() {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
keySaveState = state.keySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = { requireActivity().supportFragmentManager.popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onNextClick = {
requireActivity()
.supportFragmentManager
.beginTransaction()
.add(R.id.content, ConfirmBackupKeyDisplayFragment())
.addToBackStack(null)
.commit()
},
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
)
}
}

View File

@@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.backup.v2.ui.verify
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
/**
* View model for [ForgotBackupKeyFragment]
*/
class ForgotBackupKeyViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
val uiState: StateFlow<BackupKeyDisplayUiState> = _uiState
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
_uiState.update { it.copy(keySaveState = newState) }
}
}
data class BackupKeyDisplayUiState(
val keySaveState: BackupKeySaveState? = null
)

View File

@@ -0,0 +1,191 @@
package org.thoughtcrime.securesms.backup.v2.ui.verify
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
import org.thoughtcrime.securesms.BiometricDeviceLockContract
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.EnterKeyScreen
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions
import kotlin.random.Random
import kotlin.random.nextInt
/**
* Screen to verify the backup key
*/
class VerifyBackupKeyActivity : PassphraseRequiredActivity() {
companion object {
private val TAG = Log.tag(VerifyBackupKeyActivity::class)
@JvmStatic
fun createIntent(context: Context): Intent {
return Intent(context, VerifyBackupKeyActivity::class.java)
}
const val REQUEST_CODE = 1204
}
private lateinit var biometricDeviceAuthentication: BiometricDeviceAuthentication
private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableEdgeToEdge()
setContent {
SignalTheme {
VerifyBackupPinScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
onForgotKeyClick = {
if (!biometricDeviceAuthentication.authenticate(this, true) { biometricDeviceLockLauncher.launch(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key)) }) {
displayBackupKey()
}
},
onNextClick = {
SignalStore.backup.lastVerifyKeyTime = System.currentTimeMillis()
SignalStore.backup.hasVerifiedBefore = true
SignalStore.backup.hasSnoozedVerified = false
setResult(RESULT_OK)
finish()
}
)
}
}
initializeBiometricAuth()
}
private fun initializeBiometricAuth() {
val biometricPrompt = BiometricPrompt(this, AuthListener())
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key))
.build()
biometricDeviceAuthentication = BiometricDeviceAuthentication(BiometricManager.from(this), biometricPrompt, promptInfo)
biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int ->
if (result == BiometricDeviceAuthentication.AUTHENTICATED) {
displayBackupKey()
}
}
}
private fun displayBackupKey() {
supportFragmentManager
.beginTransaction()
.add(android.R.id.content, ForgotBackupKeyFragment())
.addToBackStack(null)
.commit()
}
private inner class AuthListener : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationFailed() {
Log.w(TAG, "onAuthenticationFailed")
Toast.makeText(this@VerifyBackupKeyActivity, R.string.RemoteBackupsSettingsFragment__authenticatino_required, Toast.LENGTH_SHORT).show()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.i(TAG, "onAuthenticationSucceeded")
displayBackupKey()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.w(TAG, "onAuthenticationError: $errorCode, $errString")
onAuthenticationFailed()
}
}
}
@Composable
fun VerifyBackupPinScreen(
backupKey: String,
onForgotKeyClick: () -> Unit = {},
onNextClick: () -> Unit = {}
) {
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
val text = buildAnnotatedString {
append(stringResource(id = R.string.VerifyBackupPinScreen__enter_the_backup_key_that_you_recorded))
append(" ")
withLink(
LinkAnnotation.Clickable(tag = "learn-more") {
CommunicationActions.openBrowserLink(context, context.getString(R.string.backup_failed_support_url))
}
) {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
append(stringResource(id = R.string.BackupAlertBottomSheet__learn_more))
}
}
}
Scaffold { paddingValues ->
EnterKeyScreen(
paddingValues = paddingValues,
backupKey = backupKey,
onNextClick = onNextClick,
captionContent = {
Text(
text = stringResource(R.string.VerifyBackupPinScreen__enter_your_backup_key),
style = MaterialTheme.typography.headlineMedium.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = Modifier.padding(top = 40.dp, bottom = 16.dp)
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant)
)
},
seeKeyButton = {
TextButton(
onClick = {
keyboardController?.hide()
onForgotKeyClick()
}
) {
Text(text = stringResource(id = R.string.VerifyBackupPinScreen__forgot_key))
}
}
)
}
}
@SignalPreview
@Composable
private fun VerifyBackupKeyScreen() {
Previews.Preview {
VerifyBackupPinScreen(
backupKey = (0 until 64).map { Random.nextInt(65..90).toChar() }.joinToString("").uppercase()
)
}
}