diff --git a/app/build.gradle b/app/build.gradle
index 4c2169e58..ae3cb087f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -73,6 +73,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
+ // Biometrics
+ implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
+
// Swipe Refresh
implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha'
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt
index 3b2b81287..cd622f88c 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt
@@ -2,12 +2,12 @@ package com.vitorpamplona.amethyst.ui
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.Coil
import coil.ImageLoader
@@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
-class MainActivity : ComponentActivity() {
+class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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 e56431e08..eedfc1355 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,6 +1,12 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
+import android.content.Context
+import android.content.ContextWrapper
import android.widget.Toast
+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
@@ -24,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -31,6 +38,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import androidx.fragment.app.FragmentActivity
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
@@ -38,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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import nostr.postr.toNsec
@@ -98,16 +107,7 @@ private fun NSecCopyButton(
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
- account.loggedIn.privKey?.let {
- clipboardManager.setText(AnnotatedString(it.toNsec()))
- scope.launch {
- Toast.makeText(
- context,
- context.getString(R.string.secret_key_copied_to_clipboard),
- Toast.LENGTH_SHORT
- ).show()
- }
- }
+ authenticatedCopyNSec(context, scope, account, clipboardManager)
},
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
@@ -121,3 +121,77 @@ private fun NSecCopyButton(
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary)
}
}
+
+fun Context.getFragmentActivity(): FragmentActivity? {
+ var currentContext = this
+ while (currentContext is ContextWrapper) {
+ if (currentContext is FragmentActivity) {
+ return currentContext
+ }
+ currentContext = currentContext.baseContext
+ }
+ return null
+}
+
+private fun authenticatedCopyNSec(
+ context: Context,
+ scope: CoroutineScope,
+ account: Account,
+ clipboardManager: ClipboardManager,
+) {
+ val fragmentContext = context.getFragmentActivity()!!
+ val authenticators = BIOMETRIC_STRONG or DEVICE_CREDENTIAL
+ val biometricManager = BiometricManager.from(context)
+
+ 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 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)
+ }
+ }
+ )
+
+ val canAuth = biometricManager.canAuthenticate(authenticators)
+ if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) {
+ biometricPrompt.authenticate(promptInfo)
+ } else {
+ copyNSec(context, scope, account, clipboardManager)
+ }
+}
+
+private fun copyNSec(
+ context: Context,
+ scope: CoroutineScope,
+ account: Account,
+ clipboardManager: ClipboardManager,
+) {
+ account.loggedIn.privKey?.let {
+ clipboardManager.setText(AnnotatedString(it.toNsec()))
+ scope.launch {
+ Toast.makeText(
+ context,
+ context.getString(R.string.secret_key_copied_to_clipboard),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 205ab9cb2..876d4103d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -181,4 +181,6 @@
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
Secret key (nsec) copied to clipboard
+ Copy my secret key
+ Authentication failed
\ No newline at end of file