From 56e83d3fe334d065027355bee4f2452162cd2ff7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 27 Mar 2023 09:54:01 -0400 Subject: [PATCH] Adds special private key for Wallet Connect --- .../amethyst/LocalPreferences.kt | 4 +- .../vitorpamplona/amethyst/model/Account.kt | 7 +- .../vitorpamplona/amethyst/service/Nip47.kt | 8 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 540 ++++++++++++------ .../ui/screen/loggedIn/AccountBackupDialog.kt | 102 +--- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 5 +- 7 files changed, 394 insertions(+), 274 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c4c661ece..172647486 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -8,10 +8,10 @@ import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.toByteArray -import com.vitorpamplona.amethyst.service.model.Contact import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent +import com.vitorpamplona.amethyst.ui.note.Nip47URI import fr.acinq.secp256k1.Hex import nostr.postr.Persona import nostr.postr.toHex @@ -215,7 +215,7 @@ object LocalPreferences { val zapPaymentRequestServer = try { getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let { - gson.fromJson(it, Contact::class.java) + gson.fromJson(it, Nip47URI::class.java) } } catch (e: Throwable) { e.printStackTrace() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0e2c36ccd..2fa0088c7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.note.Nip47URI import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -56,7 +57,7 @@ class Account( var languagePreferences: Map = mapOf(), var translateTo: String = Locale.getDefault().language, var zapAmountChoices: List = listOf(500L, 1000L, 5000L), - var zapPaymentRequest: Contact? = null, + var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, var backupContactList: ContactListEvent? = null @@ -169,7 +170,7 @@ class Account( if (!isWriteable()) return zapPaymentRequest?.let { - val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, loggedIn.privKey!!) + val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!) Client.send(event, it.relayUri) } @@ -572,7 +573,7 @@ class Account( saveable.invalidateData() } - fun changeZapPaymentRequest(newServer: Contact?) { + fun changeZapPaymentRequest(newServer: Nip47URI?) { zapPaymentRequest = newServer live.invalidateData() saveable.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt index 0be86829c..8b38638b3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/Nip47.kt @@ -3,11 +3,12 @@ package com.vitorpamplona.amethyst.ui.note import android.net.Uri import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.toHexKey -import com.vitorpamplona.amethyst.service.model.Contact + +data class Nip47URI(val pubKeyHex: String, val relayUri: String?, val secret: String?) // Rename to the corect nip number when ready. object Nip47 { - fun parse(uri: String): Contact? { + fun parse(uri: String): Nip47URI { // nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D val url = Uri.parse(uri) @@ -25,7 +26,8 @@ object Nip47 { } val relay = url.getQueryParameter("relay") + val secret = url.getQueryParameter("secret") - return Contact(pubkeyHex, relay) + return Nip47URI(pubkeyHex, relay, secret) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 3f9b2b5fb..68817ce59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -1,6 +1,17 @@ package com.vitorpamplona.amethyst.ui.note +import android.app.Activity +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent +import android.os.Build import android.widget.Toast +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -12,8 +23,10 @@ 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.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider @@ -23,12 +36,14 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.Modifier @@ -36,9 +51,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.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.dp import androidx.compose.ui.unit.sp @@ -48,11 +66,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.model.Contact +import com.vitorpamplona.amethyst.model.decodePublicKey +import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner +import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope class UpdateZapAmountViewModel : ViewModel() { private var account: Account? = null @@ -61,12 +83,14 @@ class UpdateZapAmountViewModel : ViewModel() { var amountSet by mutableStateOf(listOf()) var walletConnectRelay by mutableStateOf(TextFieldValue("")) var walletConnectPubkey by mutableStateOf(TextFieldValue("")) + var walletConnectSecret by mutableStateOf(TextFieldValue("")) fun load(account: Account) { this.account = account this.amountSet = account.zapAmountChoices this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("") + this.walletConnectSecret = account.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("") } fun toListOfAmounts(commaSeparatedAmounts: String): List { @@ -90,7 +114,16 @@ class UpdateZapAmountViewModel : ViewModel() { account?.changeZapAmounts(amountSet) if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { - account?.changeZapPaymentRequest(Contact(walletConnectPubkey.text, walletConnectRelay.text)) + val unverifiedPrivKey = walletConnectSecret.text.ifBlank { null } + val privKey = unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() } + + account?.changeZapPaymentRequest( + Nip47URI( + walletConnectPubkey.text, + walletConnectRelay.text.ifBlank { null }, + privKey + ) + ) } else { account?.changeZapPaymentRequest(null) } @@ -106,7 +139,8 @@ class UpdateZapAmountViewModel : ViewModel() { return ( amountSet != account?.zapAmountChoices || walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") || - walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") + walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") || + walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "") ) } } @@ -152,180 +186,344 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) { Spacer(modifier = Modifier.height(10.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.animateContentSize()) { - FlowRow( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - postViewModel.amountSet.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.primary - ), - onClick = { - postViewModel.removeAmount(amountInSats) + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.animateContentSize()) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + postViewModel.amountSet.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ), + onClick = { + postViewModel.removeAmount(amountInSats) + } + ) { + Text( + "⚡ ${ + showAmount( + amountInSats.toBigDecimal().setScale(1) + ) + } ✖", + color = Color.White, + textAlign = TextAlign.Center + ) } - ) { - Text( - "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))} ✖", - color = Color.White, - textAlign = TextAlign.Center + } + } + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + label = { Text(text = stringResource(R.string.new_amount_in_sats)) }, + value = postViewModel.nextAmount, + onValueChange = { + postViewModel.nextAmount = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Number + ), + placeholder = { + Text( + text = "100, 1000, 5000", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier + .padding(end = 10.dp) + .weight(1f) + ) + + Button( + onClick = { postViewModel.addAmount() }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text(text = stringResource(R.string.add), color = Color.White) + } + } + + Divider( + modifier = Modifier.padding(vertical = 10.dp), + thickness = 0.25.dp + ) + + var qrScanning by remember { mutableStateOf(false) } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(id = R.string.wallet_connect_service), + Modifier.weight(1f) + ) + IconButton(onClick = { + qrScanning = true + }) { + Icon( + painter = painterResource(R.drawable.ic_qrcode), + null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary + ) + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(id = R.string.wallet_connect_service_explainer), + Modifier.weight(1f), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + fontSize = 14.sp + ) + } + + if (qrScanning) { + SimpleQrCodeScanner { + qrScanning = false + if (!it.isNullOrEmpty()) { + try { + val contact = Nip47.parse(it) + if (contact != null) { + postViewModel.walletConnectPubkey = + TextFieldValue(contact.pubKeyHex) + postViewModel.walletConnectRelay = + TextFieldValue(contact.relayUri ?: "") + postViewModel.walletConnectSecret = + TextFieldValue(contact.secret ?: "") + } + } catch (e: IllegalArgumentException) { + scope.launch { + Toast.makeText(context, e.message, Toast.LENGTH_SHORT) + .show() + } + } + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + label = { Text(text = stringResource(R.string.wallet_connect_service_pubkey)) }, + value = postViewModel.walletConnectPubkey, + onValueChange = { + postViewModel.walletConnectPubkey = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None + ), + placeholder = { + Text( + text = "npub, hex", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier.weight(1f) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + label = { Text(text = stringResource(R.string.wallet_connect_service_relay)) }, + modifier = Modifier.weight(1f), + value = postViewModel.walletConnectRelay, + onValueChange = { postViewModel.walletConnectRelay = it }, + placeholder = { + Text( + text = "relay.server.com", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + maxLines = 1 + ) + }, + singleLine = true + ) + } + + var showPassword by remember { + mutableStateOf(false) + } + + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val keyguardLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + showPassword = true + } + } + + val authTitle = stringResource(id = R.string.wallet_connect_service_show_secret) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + label = { Text(text = stringResource(R.string.wallet_connect_service_secret)) }, + modifier = Modifier.weight(1f), + value = postViewModel.walletConnectSecret, + onValueChange = { postViewModel.walletConnectSecret = it }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go + ), + placeholder = { + Text( + text = stringResource(R.string.wallet_connect_service_secret_placeholder), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + trailingIcon = { + IconButton(onClick = { + if (!showPassword) { + authenticate(authTitle, context, scope, keyguardLauncher) { + showPassword = true + } + } else { + showPassword = false + } + }) { + Icon( + imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = if (showPassword) { + stringResource(R.string.show_password) + } else { + stringResource( + R.string.hide_password + ) + } ) } - } - } - } - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - label = { Text(text = stringResource(R.string.new_amount_in_sats)) }, - value = postViewModel.nextAmount, - onValueChange = { - postViewModel.nextAmount = it - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - keyboardType = KeyboardType.Number - ), - placeholder = { - Text( - text = "100, 1000, 5000", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - singleLine = true, - modifier = Modifier - .padding(end = 10.dp) - .weight(1f) - ) - - Button( - onClick = { postViewModel.addAmount() }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.primary - ) - ) { - Text(text = stringResource(R.string.add), color = Color.White) - } - } - - Divider( - modifier = Modifier.padding(vertical = 10.dp), - thickness = 0.25.dp - ) - - var qrScanning by remember { mutableStateOf(false) } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(id = R.string.wallet_connect_service), Modifier.weight(1f)) - IconButton(onClick = { - qrScanning = true - }) { - Icon( - painter = painterResource(R.drawable.ic_qrcode), - null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colors.primary + }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation() ) } } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - stringResource(id = R.string.wallet_connect_service_explainer), - Modifier.weight(1f), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - fontSize = 14.sp - ) - } - - if (qrScanning) { - SimpleQrCodeScanner { - qrScanning = false - if (!it.isNullOrEmpty()) { - try { - val contact = Nip47.parse(it) - if (contact != null) { - postViewModel.walletConnectPubkey = TextFieldValue(contact.pubKeyHex) - postViewModel.walletConnectRelay = TextFieldValue(contact.relayUri ?: "") - } - } catch (e: IllegalArgumentException) { - scope.launch { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - } - } - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - label = { Text(text = stringResource(R.string.wallet_connect_service_pubkey)) }, - value = postViewModel.walletConnectPubkey, - onValueChange = { - postViewModel.walletConnectPubkey = it - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None - ), - placeholder = { - Text( - text = "npub, hex", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - singleLine = true, - modifier = Modifier.weight(1f) - ) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - label = { Text(text = stringResource(R.string.wallet_connect_service_relay)) }, - modifier = Modifier.weight(1f), - value = postViewModel.walletConnectRelay, - onValueChange = { postViewModel.walletConnectRelay = it }, - placeholder = { - Text( - text = "relay.server.com", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - maxLines = 1 - ) - }, - singleLine = true - ) - } } } } } + +fun authenticate( + title: String, + context: Context, + scope: CoroutineScope, + keyguardLauncher: ManagedActivityResultLauncher, + onApproved: () -> Unit +) { + val fragmentContext = context.getFragmentActivity()!! + val keyguardManager = + fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + + if (!keyguardManager.isDeviceSecure) { + onApproved() + return + } + + @Suppress("DEPRECATION") + fun keyguardPrompt() { + val intent = keyguardManager.createConfirmDeviceCredentialIntent( + context.getString(R.string.app_name_release), + title + ) + + keyguardLauncher.launch(intent) + } + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + keyguardPrompt() + return + } + + val biometricManager = BiometricManager.from(context) + val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(context.getString(R.string.app_name_release)) + .setSubtitle(title) + .setAllowedAuthenticators(authenticators) + .build() + + val biometricPrompt = BiometricPrompt( + fragmentContext, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + + when (errorCode) { + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt() + BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt() + else -> + scope.launch { + Toast.makeText( + context, + "${context.getString(R.string.biometric_error)}: $errString", + Toast.LENGTH_SHORT + ).show() + } + } + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + scope.launch { + Toast.makeText( + context, + context.getString(R.string.biometric_authentication_failed), + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + onApproved() + } + } + ) + + when (biometricManager.canAuthenticate(authenticators)) { + BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo) + else -> keyguardPrompt() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt index 1ae127357..5567625e3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountBackupDialog.kt @@ -1,20 +1,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.app.Activity -import android.app.KeyguardManager import android.content.Context import android.content.ContextWrapper -import android.content.Intent -import android.os.Build import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricPrompt import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -54,6 +46,7 @@ import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.note.authenticate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import nostr.postr.toNsec @@ -122,7 +115,14 @@ private fun NSecCopyButton( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - authenticatedCopyNSec(context, scope, account, clipboardManager, keyguardLauncher) + authenticate( + title = context.getString(R.string.copy_my_secret_key), + context = context, + scope = scope, + keyguardLauncher = keyguardLauncher + ) { + copyNSec(context, scope, account, clipboardManager) + } }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors( @@ -151,90 +151,6 @@ fun Context.getFragmentActivity(): FragmentActivity? { return null } -private fun authenticatedCopyNSec( - context: Context, - scope: CoroutineScope, - account: Account, - clipboardManager: ClipboardManager, - keyguardLauncher: ManagedActivityResultLauncher -) { - val fragmentContext = context.getFragmentActivity()!! - val keyguardManager = - fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - - if (!keyguardManager.isDeviceSecure) { - copyNSec(context, scope, account, clipboardManager) - return - } - - @Suppress("DEPRECATION") - fun keyguardPrompt() { - val intent = keyguardManager.createConfirmDeviceCredentialIntent( - context.getString(R.string.app_name_release), - context.getString(R.string.copy_my_secret_key) - ) - - keyguardLauncher.launch(intent) - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - keyguardPrompt() - return - } - - val biometricManager = BiometricManager.from(context) - val authenticators = BIOMETRIC_STRONG or DEVICE_CREDENTIAL - - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.app_name_release)) - .setSubtitle(context.getString(R.string.copy_my_secret_key)) - .setAllowedAuthenticators(authenticators) - .build() - - val biometricPrompt = BiometricPrompt( - fragmentContext, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - - when (errorCode) { - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt() - BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt() - else -> - scope.launch { - Toast.makeText( - context, - "${context.getString(R.string.biometric_error)}: $errString", - Toast.LENGTH_SHORT - ).show() - } - } - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - scope.launch { - Toast.makeText( - context, - context.getString(R.string.biometric_authentication_failed), - Toast.LENGTH_SHORT - ).show() - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - copyNSec(context, scope, account, clipboardManager) - } - } - ) - - when (biometricManager.canAuthenticate(authenticators)) { - BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo) - else -> keyguardPrompt() - } -} - private fun copyNSec( context: Context, scope: CoroutineScope, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index aee875037..30ebf2b30 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -77,7 +77,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { // Awaits for the event to come back to LocalCache. viewModelScope.launch(Dispatchers.IO) { - delay(1000) + delay(5000) onProgress(0f) } } else { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e0428a04..0e8a60f8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -258,9 +258,12 @@ Remove from Public Bookmarks Wallet Connect Service - Uses your private key to pay zaps without leaving the app. Anyone with access to your Nostr private key will be able to spend your wallet\'s balance. Only keep funds you are ok to lose and use a private relay if possible. The relay operator can see your payments metadata. + Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible Wallet Connect Pubkey Wallet Connect Relay + Wallet Connect Secret + Show secret key + nsec / hex private key Pledge Amount in Sats