diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index 56ee6c4b..5b929627 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -455,7 +455,11 @@ fun MainScreen( .padding(horizontal = verticalPadding) .padding(top = verticalPadding * 1.5f) .imePadding(), - navController, + onShowQrCode = { + Amber.instance.applicationIOScope.launch(Dispatchers.Main) { + navController.navigate(Route.QrCode.route.replace("{content}", it)) + } + }, ) }, ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt index 4018655d..ba266435 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/AccountBackupScreen.kt @@ -8,26 +8,41 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.ExpandLess +import androidx.compose.material.icons.outlined.ExpandMore +import androidx.compose.material.icons.outlined.Lock import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.QrCode2 import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -35,6 +50,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -67,11 +83,11 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavHostController import coil3.compose.SubcomposeAsyncImage import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.BuildFlavorChecker @@ -83,10 +99,8 @@ import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.CenterCircularProgressIndicator import com.greenart7c3.nostrsigner.ui.InnerQrCodeDrawer import com.greenart7c3.nostrsigner.ui.QrCodeDrawer -import com.greenart7c3.nostrsigner.ui.components.AmberButton import com.greenart7c3.nostrsigner.ui.components.CloseButton import com.greenart7c3.nostrsigner.ui.components.SeedWordsPage -import com.greenart7c3.nostrsigner.ui.navigation.Route import com.greenart7c3.nostrsigner.ui.theme.Size35dp import com.greenart7c3.nostrsigner.ui.theme.fromHex import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions @@ -103,211 +117,328 @@ import kotlinx.coroutines.launch @Composable fun AccountBackupScreen( modifier: Modifier, - navController: NavHostController, + onShowQrCode: (String) -> Unit, ) { var isLoading by remember { mutableStateOf(false) } Surface( - modifier = - modifier - .fillMaxSize(), + modifier = modifier.fillMaxSize(), ) { Column( - modifier = - Modifier + modifier = Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize(), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (isLoading) { - CenterCircularProgressIndicator(Modifier, text = stringResource(R.string.do_not_leave_the_app_until_the_key_is_generated)) - } else { - val content = stringResource(R.string.account_backup_tips_md) + if (isLoading) { + CenterCircularProgressIndicator(Modifier, text = stringResource(R.string.do_not_leave_the_app_until_the_key_is_generated)) + } else { + // Collapsible tips section + var tipsExpanded by remember { mutableStateOf(false) } + TipsCard(expanded = tipsExpanded, onToggle = { tipsExpanded = !tipsExpanded }) - val astNode = - remember { - CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content) + Spacer(modifier = Modifier.height(16.dp)) + + // Password section + val password = remember { mutableStateOf(TextFieldValue("")) } + var errorMessage by remember { mutableStateOf("") } + var showCharsPassword by remember { mutableStateOf(false) } + val keyboardController = LocalSoftwareKeyboardController.current + + PasswordCard( + password = password.value, + errorMessage = errorMessage, + showCharsPassword = showCharsPassword, + onPasswordChange = { + password.value = it + if (errorMessage.isNotEmpty()) errorMessage = "" + }, + onToggleVisibility = { showCharsPassword = !showCharsPassword }, + onDone = { + if (password.value.text.isBlank()) { + errorMessage = Amber.instance.getString(R.string.password_is_required) } + keyboardController?.hide() + }, + ) + Spacer(modifier = Modifier.height(16.dp)) + + LocalPreferences.allSavedAccounts(Amber.instance).forEach { accountInfo -> + var localAccount by remember { mutableStateOf(null) } + var seedWords by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + localAccount = LocalPreferences.loadFromEncryptedStorage(Amber.instance, accountInfo.npub) + localAccount?.let { acc -> + seedWords = acc.seedWords() + } + } + } + + localAccount?.let { account -> + AccountBackupCard( + account = account, + password = password.value.text, + seedWords = seedWords, + onLoading = { isLoading = it }, + onShowQrCode = onShowQrCode, + ) + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + } + } +} + +@Composable +private fun TipsCard(expanded: Boolean, onToggle: () -> Unit) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.account_backup), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + TextButton(onClick = onToggle) { + Text(if (expanded) stringResource(R.string.hide) else stringResource(R.string.show_tips)) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + + AnimatedVisibility(visible = expanded) { + Column { + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + + val content = stringResource(R.string.account_backup_tips_md) + val astNode = remember { + CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content) + } RichText( style = RichTextStyle().resolveDefaults(), - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier.padding(bottom = 4.dp), ) { BasicMarkdown(astNode) } + } + } + } + } +} - Spacer(modifier = Modifier.height(30.dp)) - - val content1 = stringResource(R.string.account_backup_tips3_md_new) - - val astNode1 = - remember { - CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1) - } - - RichText( - style = RichTextStyle().resolveDefaults(), - ) { - BasicMarkdown(astNode1) - } - - Spacer(modifier = Modifier.height(10.dp)) - - val password = remember { mutableStateOf(TextFieldValue("")) } - var errorMessage by remember { mutableStateOf("") } - var showCharsPassword by remember { mutableStateOf(false) } - val keyboardController = LocalSoftwareKeyboardController.current - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .semantics { - contentType = ContentType.Password +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun PasswordCard( + password: TextFieldValue, + errorMessage: String, + showCharsPassword: Boolean, + onPasswordChange: (TextFieldValue) -> Unit, + onToggleVisibility: () -> Unit, + onDone: () -> Unit, +) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.encryption_password), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + val tipsContent = stringResource(R.string.account_backup_tips3_md) + val tipsAstNode = remember { + CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(tipsContent) + } + RichText( + style = RichTextStyle().resolveDefaults(), + modifier = Modifier.fillMaxWidth(), + ) { + BasicMarkdown(tipsAstNode) + } + Spacer(modifier = Modifier.height(12.dp)) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .semantics { contentType = ContentType.Password }, + value = password, + onValueChange = onPasswordChange, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { onDone() }), + placeholder = { Text(text = stringResource(R.string.encryption_password)) }, + trailingIcon = { + IconButton(onClick = onToggleVisibility) { + Icon( + imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = if (showCharsPassword) { + stringResource(R.string.show_password) + } else { + stringResource(R.string.hide_password) }, - value = password.value, - onValueChange = { - password.value = it - if (errorMessage.isNotEmpty()) { - errorMessage = "" - } - }, - keyboardOptions = KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { - if (password.value.text.isBlank()) { - errorMessage = Amber.instance.getString(R.string.password_is_required) - } - keyboardController?.hide() - }, - ), - placeholder = { - Text( - text = stringResource(R.string.encryption_password), - ) - }, - trailingIcon = { - Row { - IconButton(onClick = { showCharsPassword = !showCharsPassword }) { - Icon( - imageVector = if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - contentDescription = if (showCharsPassword) { - stringResource(R.string.show_password) - } else { - stringResource( - R.string.hide_password, - ) - }, - ) - } - } - }, - visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), - ) - - if (errorMessage.isNotBlank()) { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, ) } + }, + visualTransformation = if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), + isError = errorMessage.isNotBlank(), + supportingText = if (errorMessage.isNotBlank()) { + { Text(text = errorMessage, color = MaterialTheme.colorScheme.error) } + } else { + null + }, + singleLine = true, + ) + } + } +} - Spacer(modifier = Modifier.height(10.dp)) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AccountBackupCard( + account: Account, + password: String, + seedWords: String, + onLoading: (Boolean) -> Unit, + onShowQrCode: (String) -> Unit, +) { + val profileUrl = account.picture.collectAsState() + val name = account.name.collectAsState() + var didBackup by remember { mutableStateOf(account.didBackup) } - LocalPreferences.allSavedAccounts(Amber.instance).forEach { - var localAccount by remember { mutableStateOf(null) } - var seedWords by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - localAccount = LocalPreferences.loadFromEncryptedStorage(Amber.instance, it.npub) - localAccount?.let { acc -> - seedWords = acc.seedWords() - } - } - } - - localAccount?.let { localAccount -> - Column( + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + // Account header row + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + if (profileUrl.value.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) { + SubcomposeAsyncImage( + model = profileUrl.value, + contentDescription = "profile picture", + modifier = Modifier + .size(48.dp) + .clip(CircleShape), + loading = { CenterCircularProgressIndicator(Modifier) }, + error = { + Icon( + Icons.Outlined.Person, + contentDescription = "profile picture", modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .border( - 2.dp, - Color.LightGray, - RoundedCornerShape(8.dp), - ) - .padding(8.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .fillMaxWidth() - .padding(4.dp), - ) { - val profileUrl = localAccount.picture.collectAsState() - val name = localAccount.name.collectAsState() - if (profileUrl.value.isNotBlank() && !BuildFlavorChecker.isOfflineFlavor()) { - SubcomposeAsyncImage( - profileUrl, - "profile picture", - Modifier - .clip( - RoundedCornerShape(50), - ) - .height(40.dp) - .width(40.dp), - loading = { - CenterCircularProgressIndicator(Modifier) - }, - error = { _ -> - Icon( - Icons.Outlined.Person, - "profile picture", - modifier = Modifier.border( - 2.dp, - Color.fromHex(localAccount.hexKey.slice(0..5)), - CircleShape, - ), - ) - }, - ) - } else { - Icon( - Icons.Outlined.Person, - "profile picture", - modifier = Modifier - .height(40.dp) - .width(40.dp) - .border( - 2.dp, - Color.fromHex(localAccount.hexKey.slice(0..5)), - CircleShape, - ), - ) - } - Text( - modifier = Modifier - .padding(start = 4.dp), - text = name.value.ifBlank { it.npub.toShortenHex() }, - fontWeight = FontWeight.Bold, - ) - } - NSecCopyButton(localAccount, password.value.text, onLoading = { value -> isLoading = value }) - NSecQrButton(localAccount, password.value.text, onLoading = { value -> isLoading = value }, navController = navController) - if (Nip06().isValidMnemonic(seedWords)) { - SeedWordsButton(localAccount, seedWords) - } - } - } - } + .size(48.dp) + .border(2.dp, Color.fromHex(account.hexKey.slice(0..5)), CircleShape), + ) + }, + ) + } else { + Icon( + Icons.Outlined.Person, + contentDescription = "profile picture", + modifier = Modifier + .size(48.dp) + .border(2.dp, Color.fromHex(account.hexKey.slice(0..5)), CircleShape) + .clip(CircleShape), + ) } + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = name.value.ifBlank { account.hexKey.toShortenHex() }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + + // Backup status badge + if (didBackup) { + Icon( + imageVector = Icons.Outlined.CheckCircle, + contentDescription = "Backed up", + tint = Color(0xFF4CAF50), + modifier = Modifier.size(24.dp), + ) + } else { + Icon( + imageVector = Icons.Outlined.Warning, + contentDescription = "Not backed up", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(24.dp), + ) + } + } + + if (!didBackup) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.make_backup_message), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) + + // Copy and QR buttons side by side + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + NSecCopyButton( + account = account, + password = password, + modifier = Modifier.weight(1f), + onLoading = onLoading, + onBackupDone = { didBackup = true }, + ) + NSecQrButton( + account = account, + password = password, + modifier = Modifier.weight(1f), + onLoading = onLoading, + onBackupDone = { didBackup = true }, + onShowQrCode = onShowQrCode, + ) + } + + // Seed words button (full width, shown only if valid mnemonic) + if (Nip06().isValidMnemonic(seedWords)) { + Spacer(modifier = Modifier.height(4.dp)) + SeedWordsButton( + account = account, + seedWords = seedWords, + onBackupDone = { didBackup = true }, + ) } } } @@ -317,58 +448,56 @@ fun AccountBackupScreen( private fun NSecQrButton( account: Account, password: String, + modifier: Modifier = Modifier, onLoading: (Boolean) -> Unit, - navController: NavHostController, + onBackupDone: () -> Unit, + onShowQrCode: (String) -> Unit, ) { val context = LocalContext.current val scope = rememberCoroutineScope() - val showDialog = remember { - mutableStateOf(false) - } + val showDialog = remember { mutableStateOf(false) } if (showDialog.value) { var nsec by remember { mutableStateOf("") } - LaunchedEffect(Unit) { launch(Dispatchers.IO) { nsec = account.getNsec() } } - if (nsec.isNotBlank()) { - QrCodeDialog( - content = nsec, - ) { + QrCodeDialog(content = nsec) { Amber.instance.applicationIOScope.launch { account.didBackup = true + onBackupDone() showDialog.value = false } } } } - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - if (password.isNotBlank()) { - Amber.instance.applicationIOScope.launch { - onLoading(true) - val ncryptsec = account.nip49Encrypt(password) - account.didBackup = true - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec)) - } - onLoading(false) - } - } else { - showDialog.value = true + val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + if (password.isNotBlank()) { + Amber.instance.applicationIOScope.launch { + onLoading(true) + val ncryptsec = account.nip49Encrypt(password) + account.didBackup = true + onBackupDone() + onShowQrCode(ncryptsec) + onLoading(false) } + } else { + showDialog.value = true } } + } val text = stringResource(if (password.isBlank()) R.string.show_qr_code else R.string.encrypt_and_show_qr_code) - AmberButton( + BackupIconButton( + modifier = modifier, + icon = { Icon(Icons.Outlined.QrCode2, contentDescription = null, modifier = Modifier.size(18.dp)) }, + text = text, onClick = { authenticate( title = text, @@ -380,9 +509,8 @@ private fun NSecQrButton( onLoading(true) val ncryptsec = account.nip49Encrypt(password) account.didBackup = true - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - navController.navigate(Route.QrCode.route.replace("{content}", ncryptsec)) - } + onBackupDone() + onShowQrCode(ncryptsec) onLoading(false) } } else { @@ -391,17 +519,11 @@ private fun NSecQrButton( }, onError = { _, message -> scope.launch { - Toast.makeText( - context, - message, - Toast.LENGTH_SHORT, - ).show() + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }, ) }, - text = text, - ) } @@ -411,12 +533,10 @@ fun QrCodeScreen( content: String, ) { Column( - modifier = modifier - .fillMaxSize(), + modifier = modifier.fillMaxSize(), ) { Column( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -446,7 +566,6 @@ fun QrCodeDialog( window?.attributes = window.attributes?.apply { screenBrightness = 1f } - onDispose { window?.attributes = window.attributes?.apply { screenBrightness = originalBrightness ?: WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE @@ -473,7 +592,6 @@ fun QrCodeDialog( ) { CloseButton(onCancel = onClose) } - Column( modifier = Modifier .fillMaxSize() @@ -491,34 +609,28 @@ fun QrCodeDialog( private fun SeedWordsButton( account: Account, seedWords: String, + onBackupDone: () -> Unit, ) { val showSeedWords = remember { mutableStateOf(false) } val context = LocalContext.current - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - showSeedWords.value = true - } + val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + showSeedWords.value = true } + } if (showSeedWords.value) { ModalBottomSheet( - onDismissRequest = { - showSeedWords.value = false - }, + onDismissRequest = { showSeedWords.value = false }, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), ) { Column( - modifier = - Modifier + modifier = Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize(), ) { CompositionLocalProvider( - LocalDensity provides Density( - LocalDensity.current.density, - 1f, - ), + LocalDensity provides Density(LocalDensity.current.density, 1f), ) { SeedWordsPage( seedWords = seedWords.split(" ").toSet(), @@ -531,7 +643,11 @@ private fun SeedWordsButton( val text = stringResource(R.string.show_seed_words) - AmberButton( + BackupIconButton( + modifier = Modifier.fillMaxWidth(), + icon = { Icon(Icons.Outlined.Lock, contentDescription = null, modifier = Modifier.size(18.dp)) }, + text = text, + colors = ButtonDefaults.outlinedButtonColors(), onClick = { authenticate( title = text, @@ -540,21 +656,17 @@ private fun SeedWordsButton( onApproved = { Amber.instance.applicationIOScope.launch { account.didBackup = true + onBackupDone() showSeedWords.value = true } }, onError = { _, message -> Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - Toast.makeText( - context, - message, - Toast.LENGTH_SHORT, - ).show() + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }, ) }, - text = text, ) } @@ -562,26 +674,30 @@ private fun SeedWordsButton( private fun NSecCopyButton( account: Account, password: String, + modifier: Modifier = Modifier, onLoading: (Boolean) -> Unit, + onBackupDone: () -> Unit, ) { val clipboardManager = LocalClipboard.current val context = LocalContext.current val scope = rememberCoroutineScope() - val keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - if (password.isNotBlank()) { - encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading) - } else { - copyNSec(account, clipboardManager) - } + val keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + if (password.isNotBlank()) { + encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading, onBackupDone) + } else { + copyNSec(account, clipboardManager, onBackupDone) } } + } val text = stringResource(if (password.isBlank()) R.string.copy_to_clipboard else R.string.encrypt_and_copy_to_clipboard) - AmberButton( + BackupIconButton( + modifier = modifier, + icon = { Icon(Icons.Outlined.ContentCopy, contentDescription = null, modifier = Modifier.size(18.dp)) }, + text = text, onClick = { authenticate( title = text, @@ -590,33 +706,63 @@ private fun NSecCopyButton( onApproved = { scope.launch(Dispatchers.IO) { account.didBackup = true + onBackupDone() if (password.isNotBlank()) { - encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading) + encryptCopyNSec(password, Amber.instance, account, clipboardManager, onLoading, onBackupDone) } else { - copyNSec(account, clipboardManager) + copyNSec(account, clipboardManager, onBackupDone) } } }, onError = { _, message -> scope.launch { - Toast.makeText( - context, - message, - Toast.LENGTH_SHORT, - ).show() + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } }, ) }, - text = text, ) } +@Composable +private fun BackupIconButton( + modifier: Modifier = Modifier, + icon: @Composable () -> Unit, + text: String, + colors: ButtonColors = ButtonDefaults.buttonColors().copy(contentColor = Color.Black), + onClick: () -> Unit, +) { + Button( + onClick = onClick, + modifier = modifier + .height(52.dp), + shape = RoundedCornerShape(12.dp), + colors = colors, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + ) { + CompositionLocalProvider( + LocalDensity provides Density(LocalDensity.current.density, 1f), + ) { + icon() + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + maxLines = 2, + ) + } + } +} + private fun copyNSec( account: Account, clipboardManager: Clipboard, + onBackupDone: () -> Unit, ) { account.copyToClipboard(clipboardManager) + onBackupDone() } private fun encryptCopyNSec( @@ -625,15 +771,11 @@ private fun encryptCopyNSec( account: Account, clipboardManager: Clipboard, onLoading: (Boolean) -> Unit, + onBackupDone: () -> Unit, ) { if (password.isBlank()) { Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - Toast.makeText( - context, - context.getString(R.string.password_is_required), - Toast.LENGTH_SHORT, - ) - .show() + Toast.makeText(context, context.getString(R.string.password_is_required), Toast.LENGTH_SHORT).show() } } else { Amber.instance.applicationIOScope.launch(Dispatchers.IO) { @@ -641,28 +783,16 @@ private fun encryptCopyNSec( try { val key = account.nip49Encrypt(password) account.didBackup = true + onBackupDone() Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - clipboardManager.setClipEntry( - ClipEntry( - ClipData.newPlainText("", key), - ), - ) - - Toast.makeText( - context, - context.getString(R.string.secret_key_copied_to_clipboard), - Toast.LENGTH_SHORT, - ).show() + clipboardManager.setClipEntry(ClipEntry(ClipData.newPlainText("", key))) + Toast.makeText(context, context.getString(R.string.secret_key_copied_to_clipboard), Toast.LENGTH_SHORT).show() } onLoading(false) } catch (_: Exception) { onLoading(false) Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - Toast.makeText( - context, - context.getString(R.string.failed_to_encrypt_key), - Toast.LENGTH_SHORT, - ).show() + Toast.makeText(context, context.getString(R.string.failed_to_encrypt_key), Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdedc7fe..832e0b43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,8 @@ Nsec / private key Show password Hide password + Hide + Tips Invalid key Key is required Login