mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-07-17 12:02:53 +02:00
Merge pull request #1229 from davotoula/account-backup-qr-code
Account backup qr codes
This commit is contained in:
@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@ -71,6 +72,7 @@ import androidx.compose.ui.platform.LocalAutofill
|
|||||||
import androidx.compose.ui.platform.LocalAutofillTree
|
import androidx.compose.ui.platform.LocalAutofillTree
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
@ -92,10 +94,13 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.authenticate
|
import com.vitorpamplona.amethyst.ui.note.authenticate
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
@ -123,8 +128,7 @@ fun DialogContentsPreview() {
|
|||||||
ThemeComparisonRow {
|
ThemeComparisonRow {
|
||||||
DialogContents(
|
DialogContents(
|
||||||
mockAccountViewModel(),
|
mockAccountViewModel(),
|
||||||
{},
|
) {}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +187,15 @@ private fun DialogContents(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
NSecCopyButton(accountViewModel)
|
Row {
|
||||||
|
Column {
|
||||||
|
NSecCopyButton(accountViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
QrCodeButton(accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(30.dp))
|
Spacer(modifier = Modifier.height(30.dp))
|
||||||
|
|
||||||
@ -238,7 +250,7 @@ private fun DialogContents(
|
|||||||
},
|
},
|
||||||
keyboardOptions =
|
keyboardOptions =
|
||||||
KeyboardOptions(
|
KeyboardOptions(
|
||||||
autoCorrect = false,
|
autoCorrectEnabled = false,
|
||||||
keyboardType = KeyboardType.Password,
|
keyboardType = KeyboardType.Password,
|
||||||
imeAction = ImeAction.Go,
|
imeAction = ImeAction.Go,
|
||||||
),
|
),
|
||||||
@ -349,30 +361,38 @@ private fun EncryptNSecCopyButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
Row {
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
Column {
|
||||||
onClick = {
|
OutlinedButton(
|
||||||
authenticate(
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
title = stringRes(context, R.string.copy_my_secret_key),
|
onClick = {
|
||||||
context = context,
|
authenticate(
|
||||||
keyguardLauncher = keyguardLauncher,
|
title = stringRes(context, R.string.copy_my_secret_key),
|
||||||
onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) },
|
context = context,
|
||||||
onError = { title, message -> accountViewModel.toast(title, message) },
|
keyguardLauncher = keyguardLauncher,
|
||||||
)
|
onApproved = { encryptCopyNSec(password, context, scope, accountViewModel, clipboardManager) },
|
||||||
},
|
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||||
shape = ButtonBorder,
|
)
|
||||||
contentPadding = ButtonPadding,
|
},
|
||||||
enabled = password.value.text.isNotBlank(),
|
shape = ButtonBorder,
|
||||||
) {
|
contentPadding = ButtonPadding,
|
||||||
Icon(
|
enabled = password.value.text.isNotBlank(),
|
||||||
imageVector = Icons.Default.Key,
|
) {
|
||||||
contentDescription =
|
Icon(
|
||||||
stringRes(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
imageVector = Icons.Default.Key,
|
||||||
modifier = Modifier.padding(end = 5.dp),
|
contentDescription =
|
||||||
)
|
stringRes(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
||||||
Text(
|
modifier = Modifier.padding(end = 5.dp),
|
||||||
stringRes(id = R.string.encrypt_and_copy_my_secret_key),
|
)
|
||||||
)
|
Text(
|
||||||
|
stringRes(id = R.string.encrypt_and_copy_my_secret_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
QrCodeButtonEncrypted(accountViewModel, password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -448,3 +468,119 @@ private fun encryptCopyNSec(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QrCodeButtonBase(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
contentDescription: Int,
|
||||||
|
onDialogShow: () -> String?,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// store the dialog open or close state
|
||||||
|
var dialogOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val keyguardLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
dialogOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
enabled = isEnabled,
|
||||||
|
onClick = {
|
||||||
|
authenticate(
|
||||||
|
title = stringRes(context, R.string.copy_my_secret_key),
|
||||||
|
context = context,
|
||||||
|
keyguardLauncher = keyguardLauncher,
|
||||||
|
onApproved = { dialogOpen = true },
|
||||||
|
onError = { title, message -> accountViewModel.toast(title, message) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_qrcode),
|
||||||
|
contentDescription = stringRes(id = contentDescription),
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = if (isEnabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.grayText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogOpen) {
|
||||||
|
ShowKeyQRDialog(
|
||||||
|
onDialogShow(),
|
||||||
|
onClose = { dialogOpen = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QrCodeButton(accountViewModel: AccountViewModel) {
|
||||||
|
QrCodeButtonBase(
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
contentDescription = R.string.show_private_key_qr_code,
|
||||||
|
onDialogShow = {
|
||||||
|
accountViewModel.account.settings.keyPair.privKey
|
||||||
|
?.toNsec()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun QrCodeButtonEncrypted(
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
password: MutableState<TextFieldValue>,
|
||||||
|
) {
|
||||||
|
QrCodeButtonBase(
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
isEnabled = password.value.text.isNotBlank(),
|
||||||
|
contentDescription = R.string.show_encrypted_private_key_qr_code,
|
||||||
|
onDialogShow = {
|
||||||
|
accountViewModel.account.settings.keyPair.privKey
|
||||||
|
?.toHexKey()
|
||||||
|
?.let { CryptoUtils.encryptNIP49(it, password.value.text) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShowKeyQRDialog(
|
||||||
|
qrCode: String?,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onClose,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Surface {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(10.dp),
|
||||||
|
) {
|
||||||
|
// Back button at the top
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
BackButton(onPress = onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR Code content
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
QrCodeDrawer(qrCode ?: "error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -153,6 +153,8 @@
|
|||||||
<string name="lightning_address">Lightning Address</string>
|
<string name="lightning_address">Lightning Address</string>
|
||||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
|
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
|
||||||
<string name="copy_private_key_to_the_clipboard">Copy Secret Key to the Clipboard</string>
|
<string name="copy_private_key_to_the_clipboard">Copy Secret Key to the Clipboard</string>
|
||||||
|
<string name="show_private_key_qr_code">Show private key QR code</string>
|
||||||
|
<string name="show_encrypted_private_key_qr_code">Show encrypted private key QR code</string>
|
||||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
|
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
|
||||||
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
|
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
|
||||||
<string name="send_a_direct_message">Send a Direct Message</string>
|
<string name="send_a_direct_message">Send a Direct Message</string>
|
||||||
|
Reference in New Issue
Block a user