Skip to content

Commit

Permalink
Merge pull request #1229 from davotoula/account-backup-qr-code
Browse files Browse the repository at this point in the history
Account backup qr codes
  • Loading branch information
vitorpamplona authored Dec 18, 2024
2 parents d101397 + 9f1b5a3 commit e002e5c
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
Expand Down Expand Up @@ -71,6 +72,7 @@ import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
Expand All @@ -92,10 +94,13 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
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.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey
Expand Down Expand Up @@ -123,8 +128,7 @@ fun DialogContentsPreview() {
ThemeComparisonRow {
DialogContents(
mockAccountViewModel(),
{},
)
) {}
}
}

Expand Down Expand Up @@ -183,7 +187,15 @@ private fun DialogContents(

Spacer(modifier = Modifier.height(20.dp))

NSecCopyButton(accountViewModel)
Row {
Column {
NSecCopyButton(accountViewModel)
}

Column {
QrCodeButton(accountViewModel)
}
}

Spacer(modifier = Modifier.height(30.dp))

Expand Down Expand Up @@ -238,7 +250,7 @@ private fun DialogContents(
},
keyboardOptions =
KeyboardOptions(
autoCorrect = false,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Go,
),
Expand Down Expand Up @@ -349,30 +361,38 @@ private fun EncryptNSecCopyButton(
}
}

OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
authenticate(
title = stringRes(context, R.string.copy_my_secret_key),
context = context,
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(),
) {
Icon(
imageVector = Icons.Default.Key,
contentDescription =
stringRes(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
modifier = Modifier.padding(end = 5.dp),
)
Text(
stringRes(id = R.string.encrypt_and_copy_my_secret_key),
)
Row {
Column {
OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
authenticate(
title = stringRes(context, R.string.copy_my_secret_key),
context = context,
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(),
) {
Icon(
imageVector = Icons.Default.Key,
contentDescription =
stringRes(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
modifier = Modifier.padding(end = 5.dp),
)
Text(
stringRes(id = R.string.encrypt_and_copy_my_secret_key),
)
}
}

Column {
QrCodeButtonEncrypted(accountViewModel, password)
}
}
}

Expand Down Expand Up @@ -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")
}
}
}
}
}
2 changes: 2 additions & 0 deletions amethyst/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@
<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="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="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>
Expand Down

0 comments on commit e002e5c

Please sign in to comment.