mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
finish drawer screen
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.21" />
|
||||
<option name="version" value="1.9.0" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -107,4 +107,12 @@ dependencies {
|
||||
|
||||
// Websockets API
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
|
||||
// Markdown (With fix for full-image bleeds)
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
|
||||
|
||||
// Biometrics
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
}
|
||||
@@ -1,35 +1,99 @@
|
||||
package com.greenart7c3.nostrsigner.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.OnBackPressedCallback
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DrawerValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.ManageAccounts
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.rememberDrawerState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.greenart7c3.nostrsigner.MainActivity
|
||||
import com.greenart7c3.nostrsigner.R
|
||||
import com.greenart7c3.nostrsigner.models.Account
|
||||
import com.greenart7c3.nostrsigner.models.IntentData
|
||||
import com.greenart7c3.nostrsigner.models.toHexKey
|
||||
import com.greenart7c3.nostrsigner.service.model.Event
|
||||
import com.greenart7c3.nostrsigner.service.relays.Relay
|
||||
import com.greenart7c3.nostrsigner.service.toNpub
|
||||
import com.greenart7c3.nostrsigner.service.toNsec
|
||||
import com.greenart7c3.nostrsigner.ui.actions.ButtonBorder
|
||||
import com.greenart7c3.nostrsigner.ui.actions.CloseButton
|
||||
import com.greenart7c3.nostrsigner.ui.actions.RelaySelectionDialog
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
@Composable
|
||||
@@ -50,16 +114,17 @@ fun AccountScreen(
|
||||
LoginPage(accountStateViewModel)
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
MainScreen(state.account, event, mainActivity)
|
||||
MainScreen(state.account, accountStateViewModel, event, mainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
|
||||
@Composable
|
||||
fun MainScreen(account: Account, json: IntentData?, mainActivity: MainActivity) {
|
||||
fun MainScreen(account: Account, accountStateViewModel: AccountStateViewModel, json: IntentData?, mainActivity: MainActivity) {
|
||||
val event = remember {
|
||||
mutableStateOf<Event?>(null)
|
||||
}
|
||||
@@ -70,8 +135,109 @@ fun MainScreen(account: Account, json: IntentData?, mainActivity: MainActivity)
|
||||
}
|
||||
val relaysToPost = remember { mutableListOf<Relay>() }
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
if (logoutDialog) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = "Logout")
|
||||
},
|
||||
text = {
|
||||
Text(text = "Logging out deletes all your local information. Make sure to have your private keys backed up to avoid losing your account. Do you want to continue?")
|
||||
},
|
||||
onDismissRequest = {
|
||||
logoutDialog = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
accountStateViewModel.logOff(account.keyPair.pubKey.toNpub())
|
||||
}
|
||||
) {
|
||||
Text(text = "Logout")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
}
|
||||
) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconRow(
|
||||
title = "Backup Keys",
|
||||
icon = Icons.Default.Key,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
)
|
||||
Divider(
|
||||
thickness = 0.25.dp,
|
||||
modifier = Modifier.padding(top = 20.dp)
|
||||
)
|
||||
IconRow(
|
||||
title = "Logout",
|
||||
icon = Icons.Default.ManageAccounts,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
logoutDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
BackHandler(enabled = scaffoldState.drawerState.isOpen) {
|
||||
coroutineScope.launch { scaffoldState.drawerState.close() }
|
||||
}
|
||||
},
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.open()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundColor = Color.Transparent,
|
||||
elevation = 0.dp
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Row(
|
||||
Modifier
|
||||
@@ -95,6 +261,17 @@ fun MainScreen(account: Account, json: IntentData?, mainActivity: MainActivity)
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (event.value!!.pubKey != account.keyPair.pubKey.toHexKey()) {
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@Button
|
||||
}
|
||||
|
||||
val signedEvent = Event.create(
|
||||
account.keyPair.privKey!!,
|
||||
event.value!!.kind,
|
||||
@@ -115,6 +292,18 @@ fun MainScreen(account: Account, json: IntentData?, mainActivity: MainActivity)
|
||||
if (event.value == null) {
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (event.value!!.pubKey != account.keyPair.pubKey.toHexKey()) {
|
||||
coroutineScope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
return@Button
|
||||
}
|
||||
|
||||
val signedEvent = Event.create(
|
||||
account.keyPair.privKey!!,
|
||||
event.value!!.kind,
|
||||
@@ -188,3 +377,308 @@ fun MainScreen(account: Account, json: IntentData?, mainActivity: MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun IconRow(title: String, icon: ImageVector, tint: Color, onClick: () -> Unit, onLongClick: (() -> Unit)? = null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 15.dp, horizontal = 25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = tint
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
text = title,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("MissingJvmstatic")
|
||||
@Composable
|
||||
fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
|
||||
// Safely update the current `onBack` lambda when a new one is provided
|
||||
val currentOnBack by rememberUpdatedState(onBack)
|
||||
// Remember in Composition a back callback that calls the `onBack` lambda
|
||||
val backCallback = remember {
|
||||
object : OnBackPressedCallback(enabled) {
|
||||
override fun handleOnBackPressed() {
|
||||
currentOnBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
// On every successful composition, update the callback with the `enabled` value
|
||||
SideEffect {
|
||||
backCallback.isEnabled = enabled
|
||||
}
|
||||
val backDispatcher = checkNotNull(LocalOnBackPressedDispatcherOwner.current) {
|
||||
"No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
|
||||
}.onBackPressedDispatcher
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner, backDispatcher) {
|
||||
// Add callback to the backDispatcher
|
||||
backDispatcher.addCallback(lifecycleOwner, backCallback)
|
||||
// When the effect leaves the Composition, remove the callback
|
||||
onDispose {
|
||||
backCallback.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloseButton(onCancel = onClose)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 30.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
MaterialRichText(
|
||||
style = RichTextStyle().resolveDefaults()
|
||||
) {
|
||||
Markdown(
|
||||
content = stringResource(R.string.account_backup_tips_md)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
NSecCopyButton(account)
|
||||
NPubCopyButton(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NSecCopyButton(
|
||||
account: Account
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
copyNSec(context, scope, account, clipboardManager)
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
authenticate(
|
||||
title = context.getString(R.string.copy_my_secret_key),
|
||||
context = context,
|
||||
scope = scope,
|
||||
keyguardLauncher = keyguardLauncher
|
||||
) {
|
||||
copyNSec(context, scope, account, clipboardManager)
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup),
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(stringResource(id = R.string.copy_my_secret_key), color = MaterialTheme.colors.onPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NPubCopyButton(
|
||||
account: Account
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
clipboardManager.setText(AnnotatedString(account.keyPair.pubKey.toNpub()))
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.public_key_npub_copied_to_clipboard),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onPrimary,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription = stringResource(R.string.copies_the_npub_id_your_user_to_the_clipboard_for_backup),
|
||||
modifier = Modifier.padding(end = 5.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.copy_my_public_key),
|
||||
color = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyNSec(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
account: Account,
|
||||
clipboardManager: ClipboardManager
|
||||
) {
|
||||
account.keyPair.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getAppCompatActivity(): AppCompatActivity? {
|
||||
var currentContext = this
|
||||
while (currentContext is ContextWrapper) {
|
||||
if (currentContext is AppCompatActivity) {
|
||||
return currentContext
|
||||
}
|
||||
currentContext = currentContext.baseContext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
title: String,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||
onApproved: () -> Unit
|
||||
) {
|
||||
val fragmentContext = context.getAppCompatActivity()!!
|
||||
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),
|
||||
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))
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,26 @@ class AccountStateViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun prepareLogoutOrSwitch() {
|
||||
when (val state = accountContent.value) {
|
||||
is AccountState.LoggedIn -> {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
state.account.saveable.removeObserver(saveListener)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
_accountContent.update { AccountState.LoggedOff }
|
||||
}
|
||||
|
||||
fun logOff(npub: String) {
|
||||
prepareLogoutOrSwitch()
|
||||
LocalPreferences.updatePrefsForLogout(npub)
|
||||
tryLoginExistingAccount()
|
||||
}
|
||||
|
||||
fun startUI(key: String) {
|
||||
if (!key.startsWith("nsec")) {
|
||||
throw Exception("Not nsec key")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
</vector>
|
||||
@@ -1,3 +1,19 @@
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name">Amber</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## Key Backup and Safety Tips
|
||||
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
||||
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||
\n- Amber developers will **never** ask you for your secret key.
|
||||
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
|
||||
</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>
|
||||
<string name="biometric_error">Error</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="public_key_npub_copied_to_clipboard">Public key (npub) copied to clipboard</string>
|
||||
<string name="copies_the_npub_id_your_user_to_the_clipboard_for_backup">Copies the Npub ID (your user) to the clipboard for backup</string>
|
||||
<string name="copy_my_public_key">Copy my public key</string>
|
||||
<string name="event_pubkey_is_not_equal_to_current_logged_in_user">Event pubkey is not equal to current logged in user</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user