mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-09-21 18:10:33 +02:00
Merge pull request #191 from maxmoney21m/feature/biometric-backup-nsec
Require biometric with device lock fallback to copy nsec
This commit is contained in:
@@ -73,6 +73,9 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
|
||||||
|
|
||||||
|
// Biometrics
|
||||||
|
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||||
|
|
||||||
// Swipe Refresh
|
// Swipe Refresh
|
||||||
implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha'
|
implementation 'com.google.accompanist:accompanist-swiperefresh:0.29.1-alpha'
|
||||||
|
|
||||||
|
@@ -2,12 +2,12 @@ package com.vitorpamplona.amethyst.ui
|
|||||||
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
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.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : FragmentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.widget.Toast
|
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.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
|
||||||
@@ -24,6 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
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.stringResource
|
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.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import com.halilibo.richtext.markdown.Markdown
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
import com.halilibo.richtext.ui.RichTextStyle
|
||||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
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.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 kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.toNsec
|
import nostr.postr.toNsec
|
||||||
|
|
||||||
@@ -98,16 +107,7 @@ private fun NSecCopyButton(
|
|||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
account.loggedIn.privKey?.let {
|
authenticatedCopyNSec(context, scope, account, clipboardManager)
|
||||||
clipboardManager.setText(AnnotatedString(it.toNsec()))
|
|
||||||
scope.launch {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.secret_key_copied_to_clipboard),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
|
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
|
||||||
backgroundColor = MaterialTheme.colors.primary
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
@@ -121,3 +121,77 @@ private fun NSecCopyButton(
|
|||||||
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -181,4 +181,6 @@
|
|||||||
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
||||||
</string>
|
</string>
|
||||||
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
|
||||||
|
<string name="copy_my_secret_key">Copy my secret key</string>
|
||||||
|
<string name="biometric_authentication_failed">Authentication failed</string>
|
||||||
</resources>
|
</resources>
|
Reference in New Issue
Block a user