Adds special private key for Wallet Connect

This commit is contained in:
Vitor Pamplona
2023-03-27 09:54:01 -04:00
parent 7a50f97773
commit 56e83d3fe3
7 changed files with 394 additions and 274 deletions

View File

@@ -8,10 +8,10 @@ import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.toByteArray 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.ContactListEvent
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import com.vitorpamplona.amethyst.ui.note.Nip47URI
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.toHex import nostr.postr.toHex
@@ -215,7 +215,7 @@ object LocalPreferences {
val zapPaymentRequestServer = try { val zapPaymentRequestServer = try {
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let { getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
gson.fromJson(it, Contact::class.java) gson.fromJson(it, Nip47URI::class.java)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTrace() e.printStackTrace()

View File

@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPool
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.Nip47URI
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -56,7 +57,7 @@ class Account(
var languagePreferences: Map<String, String> = mapOf(), var languagePreferences: Map<String, String> = mapOf(),
var translateTo: String = Locale.getDefault().language, var translateTo: String = Locale.getDefault().language,
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L), var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
var zapPaymentRequest: Contact? = null, var zapPaymentRequest: Nip47URI? = null,
var hideDeleteRequestDialog: Boolean = false, var hideDeleteRequestDialog: Boolean = false,
var hideBlockAlertDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false,
var backupContactList: ContactListEvent? = null var backupContactList: ContactListEvent? = null
@@ -169,7 +170,7 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
zapPaymentRequest?.let { 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) Client.send(event, it.relayUri)
} }
@@ -572,7 +573,7 @@ class Account(
saveable.invalidateData() saveable.invalidateData()
} }
fun changeZapPaymentRequest(newServer: Contact?) { fun changeZapPaymentRequest(newServer: Nip47URI?) {
zapPaymentRequest = newServer zapPaymentRequest = newServer
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()

View File

@@ -3,11 +3,12 @@ package com.vitorpamplona.amethyst.ui.note
import android.net.Uri import android.net.Uri
import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey 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. // Rename to the corect nip number when ready.
object Nip47 { 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 // nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D
val url = Uri.parse(uri) val url = Uri.parse(uri)
@@ -25,7 +26,8 @@ object Nip47 {
} }
val relay = url.getQueryParameter("relay") val relay = url.getQueryParameter("relay")
val secret = url.getQueryParameter("secret")
return Contact(pubkeyHex, relay) return Nip47URI(pubkeyHex, relay, secret)
} }
} }

View File

@@ -1,6 +1,17 @@
package com.vitorpamplona.amethyst.ui.note 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 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.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider import androidx.compose.material.Divider
@@ -23,12 +36,14 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType 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.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -48,11 +66,15 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account 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.CloseButton
import com.vitorpamplona.amethyst.ui.actions.SaveButton import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
class UpdateZapAmountViewModel : ViewModel() { class UpdateZapAmountViewModel : ViewModel() {
private var account: Account? = null private var account: Account? = null
@@ -61,12 +83,14 @@ class UpdateZapAmountViewModel : ViewModel() {
var amountSet by mutableStateOf(listOf<Long>()) var amountSet by mutableStateOf(listOf<Long>())
var walletConnectRelay by mutableStateOf(TextFieldValue("")) var walletConnectRelay by mutableStateOf(TextFieldValue(""))
var walletConnectPubkey by mutableStateOf(TextFieldValue("")) var walletConnectPubkey by mutableStateOf(TextFieldValue(""))
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
fun load(account: Account) { fun load(account: Account) {
this.account = account this.account = account
this.amountSet = account.zapAmountChoices this.amountSet = account.zapAmountChoices
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.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<Long> { fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> {
@@ -90,7 +114,16 @@ class UpdateZapAmountViewModel : ViewModel() {
account?.changeZapAmounts(amountSet) account?.changeZapAmounts(amountSet)
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { 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 { } else {
account?.changeZapPaymentRequest(null) account?.changeZapPaymentRequest(null)
} }
@@ -106,7 +139,8 @@ class UpdateZapAmountViewModel : ViewModel() {
return ( return (
amountSet != account?.zapAmountChoices || amountSet != account?.zapAmountChoices ||
walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") || walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") ||
walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") ||
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
) )
} }
} }
@@ -152,6 +186,9 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.animateContentSize()) { Column(modifier = Modifier.animateContentSize()) {
FlowRow( FlowRow(
@@ -170,7 +207,11 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
} }
) { ) {
Text( Text(
"${showAmount(amountInSats.toBigDecimal().setScale(1))}", "${
showAmount(
amountInSats.toBigDecimal().setScale(1)
)
} ",
color = Color.White, color = Color.White,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -232,7 +273,10 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text(stringResource(id = R.string.wallet_connect_service), Modifier.weight(1f)) Text(
stringResource(id = R.string.wallet_connect_service),
Modifier.weight(1f)
)
IconButton(onClick = { IconButton(onClick = {
qrScanning = true qrScanning = true
}) { }) {
@@ -266,12 +310,17 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
try { try {
val contact = Nip47.parse(it) val contact = Nip47.parse(it)
if (contact != null) { if (contact != null) {
postViewModel.walletConnectPubkey = TextFieldValue(contact.pubKeyHex) postViewModel.walletConnectPubkey =
postViewModel.walletConnectRelay = TextFieldValue(contact.relayUri ?: "") TextFieldValue(contact.pubKeyHex)
postViewModel.walletConnectRelay =
TextFieldValue(contact.relayUri ?: "")
postViewModel.walletConnectSecret =
TextFieldValue(contact.secret ?: "")
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
scope.launch { scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
.show()
} }
} }
} }
@@ -325,7 +374,156 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
singleLine = true 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
)
}
)
}
},
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation()
)
} }
} }
} }
} }
}
}
fun authenticate(
title: String,
context: Context,
scope: CoroutineScope,
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
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()
}
}

View File

@@ -1,20 +1,12 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.app.Activity import android.app.Activity
import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent
import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.note.authenticate
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.toNsec import nostr.postr.toNsec
@@ -122,7 +115,14 @@ private fun NSecCopyButton(
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { 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), shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -151,90 +151,6 @@ fun Context.getFragmentActivity(): FragmentActivity? {
return null return null
} }
private fun authenticatedCopyNSec(
context: Context,
scope: CoroutineScope,
account: Account,
clipboardManager: ClipboardManager,
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>
) {
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( private fun copyNSec(
context: Context, context: Context,
scope: CoroutineScope, scope: CoroutineScope,

View File

@@ -77,7 +77,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
// Awaits for the event to come back to LocalCache. // Awaits for the event to come back to LocalCache.
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
delay(1000) delay(5000)
onProgress(0f) onProgress(0f)
} }
} else { } else {

View File

@@ -258,9 +258,12 @@
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string> <string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
<string name="wallet_connect_service">Wallet Connect Service</string> <string name="wallet_connect_service">Wallet Connect Service</string>
<string name="wallet_connect_service_explainer">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.</string> <string name="wallet_connect_service_explainer">Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string> <string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
<string name="wallet_connect_service_relay">Wallet Connect Relay</string> <string name="wallet_connect_service_relay">Wallet Connect Relay</string>
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
<string name="wallet_connect_service_show_secret">Show secret key</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex private key</string>
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string> <string name="pledge_amount_in_sats">Pledge Amount in Sats</string>