Compare commits
5 Commits
master
...
fix-nostr-
| Author | SHA1 | Date | |
|---|---|---|---|
| f49279fd10 | |||
| a48c0c0e7c | |||
| d23b15df6f | |||
| 8750eaa225 | |||
| 0ad872d186 |
@@ -626,7 +626,7 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
val secondRoom = requests.getOrNull(1)
|
val secondRoom = requests.getOrNull(1)
|
||||||
|
|
||||||
val firstRoomState by (firstRoom as Room).rememberUiState(nostrViewModel)
|
val firstRoomState by (firstRoom as Room).rememberUiState(nostrViewModel)
|
||||||
val secondRoomState by (secondRoom as Room).rememberUiState(nostrViewModel)
|
val secondRoomState by (secondRoom ?: firstRoom).rememberUiState(nostrViewModel)
|
||||||
|
|
||||||
val supportingText = when {
|
val supportingText = when {
|
||||||
total == 1 -> {
|
total == 1 -> {
|
||||||
|
|||||||
@@ -48,23 +48,18 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
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.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import coop.composeapp.generated.resources.Res
|
import coop.composeapp.generated.resources.Res
|
||||||
import coop.composeapp.generated.resources.ic_arrow_back
|
import coop.composeapp.generated.resources.ic_arrow_back
|
||||||
import coop.composeapp.generated.resources.ic_scanner
|
import coop.composeapp.generated.resources.ic_scanner
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
import rust.nostr.sdk.PublicKey
|
|
||||||
import su.reya.coop.LocalAuthViewModel
|
import su.reya.coop.LocalAuthViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalNostrViewModel
|
|
||||||
import su.reya.coop.LocalScanResult
|
import su.reya.coop.LocalScanResult
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.shared.Avatar
|
|
||||||
import su.reya.coop.shared.getExpressiveFontFamily
|
import su.reya.coop.shared.getExpressiveFontFamily
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@@ -74,19 +69,13 @@ fun ImportScreen() {
|
|||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val qrScanResult = LocalScanResult.current
|
val qrScanResult = LocalScanResult.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val nostrViewModel = LocalNostrViewModel.current
|
|
||||||
val authViewModel = LocalAuthViewModel.current
|
val authViewModel = LocalAuthViewModel.current
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
|
||||||
val isBusy = authState.isBusy
|
|
||||||
var secret by remember { mutableStateOf("") }
|
var secret by remember { mutableStateOf("") }
|
||||||
var pubkey by remember { mutableStateOf<PublicKey?>(null) }
|
var password by remember { mutableStateOf("") }
|
||||||
|
var requirePassword by remember { mutableStateOf(false) }
|
||||||
val profile by remember(pubkey) {
|
var loading by remember { mutableStateOf(false) }
|
||||||
pubkey?.let(nostrViewModel::getMetadata) ?: flowOf(null)
|
|
||||||
}.collectAsStateWithLifecycle(null)
|
|
||||||
|
|
||||||
LaunchedEffect(qrScanResult.content) {
|
LaunchedEffect(qrScanResult.content) {
|
||||||
qrScanResult.content?.let { result ->
|
qrScanResult.content?.let { result ->
|
||||||
@@ -101,13 +90,19 @@ fun ImportScreen() {
|
|||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
secret = result
|
secret = result
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
snackbarHostState.showSnackbar("Invalid secret: ${e.message}")
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
}
|
}
|
||||||
// Clear the nav state
|
// Clear the nav state
|
||||||
qrScanResult.clear()
|
qrScanResult.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(secret) {
|
||||||
|
if (secret.startsWith("ncryptsec1")) {
|
||||||
|
requirePassword = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
@@ -160,21 +155,14 @@ fun ImportScreen() {
|
|||||||
.clip(MaterialShapes.Cookie9Sided.toShape()),
|
.clip(MaterialShapes.Cookie9Sided.toShape()),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Text(
|
||||||
picture = profile?.picture,
|
text = "",
|
||||||
description = "Profile picture",
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxSize(),
|
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
||||||
shape = MaterialShapes.Cookie9Sided.toShape(),
|
fontFamily = getExpressiveFontFamily()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
|
||||||
Text(
|
|
||||||
text = profile?.name ?: "",
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
style = MaterialTheme.typography.titleLargeEmphasized.copy(
|
|
||||||
fontFamily = getExpressiveFontFamily()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -186,7 +174,7 @@ fun ImportScreen() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(24.dp)
|
.padding(24.dp),
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -203,9 +191,9 @@ fun ImportScreen() {
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = secret,
|
value = secret,
|
||||||
onValueChange = { secret = it },
|
onValueChange = { secret = it },
|
||||||
enabled = !isBusy,
|
enabled = !loading,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
maxLines = 4,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
@@ -237,32 +225,68 @@ fun ImportScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
if (requirePassword) {
|
||||||
|
Text(
|
||||||
|
text = "Decrypt Password:",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
BasicTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
enabled = !loading && requirePassword,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
visualTransformation = PasswordVisualTransformation('*'),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMediumEmphasized.copy(
|
||||||
|
color = MaterialTheme.colorScheme.tertiaryFixedDim,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.tertiaryContainer),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(contentAlignment = Alignment.CenterStart) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (pubkey == null) {
|
loading = true
|
||||||
authViewModel.verifyIdentity(secret).let { pubkey = it }
|
try {
|
||||||
} else {
|
|
||||||
// Import the identity
|
// Import the identity
|
||||||
authViewModel.importIdentity(secret)
|
authViewModel.importIdentity(secret, password)
|
||||||
// Navigate to the home screen
|
// Navigate to the home screen
|
||||||
navigator.navigate(Screen.Home)
|
navigator.navigate(Screen.Home)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackbarHostState.showSnackbar(e.message ?: "Error")
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(ButtonDefaults.MediumContainerHeight),
|
.height(ButtonDefaults.MediumContainerHeight),
|
||||||
enabled = secret.isNotBlank() && !isBusy,
|
enabled = secret.isNotBlank() && !loading,
|
||||||
) {
|
) {
|
||||||
if (isBusy) {
|
if (loading) {
|
||||||
LoadingIndicator()
|
LoadingIndicator()
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = if (pubkey == null) "Verify" else "Click again to Continue",
|
text = "Continue",
|
||||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package su.reya.coop.screens
|
package su.reya.coop.screens
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import su.reya.coop.LocalAuthViewModel
|
import su.reya.coop.LocalAuthViewModel
|
||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
@@ -16,13 +14,9 @@ fun NewIdentityScreen() {
|
|||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val authState by authViewModel.state.collectAsStateWithLifecycle()
|
|
||||||
val isBusy = authState.isBusy
|
|
||||||
|
|
||||||
ProfileEditor(
|
ProfileEditor(
|
||||||
title = "Create a new identity",
|
title = "Create a new identity",
|
||||||
buttonLabel = "Continue",
|
buttonLabel = "Continue",
|
||||||
isBusy = isBusy,
|
|
||||||
onBack = { navigator.goBack() },
|
onBack = { navigator.goBack() },
|
||||||
onConfirm = { name, bio, bytes, type ->
|
onConfirm = { name, bio, bytes, type ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -160,7 +160,10 @@ fun OnboardingScreen() {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
if (authViewModel.isExternalSignerAvailable()) {
|
if (authViewModel.isExternalSignerAvailable()) {
|
||||||
try {
|
try {
|
||||||
|
// Connect to the external signer
|
||||||
|
// TODO: show all available signers?
|
||||||
authViewModel.connectExternalSigner()
|
authViewModel.connectExternalSigner()
|
||||||
|
// Navigate to the home screen
|
||||||
navigator.navigate(Screen.Home)
|
navigator.navigate(Screen.Home)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.message?.let { snackbarHostState.showSnackbar(it) }
|
e.message?.let { snackbarHostState.showSnackbar(it) }
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ fun ProfileEditor(
|
|||||||
initialName: String = "",
|
initialName: String = "",
|
||||||
initialBio: String = "",
|
initialBio: String = "",
|
||||||
initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL)
|
initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL)
|
||||||
isBusy: Boolean = false,
|
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
|
onConfirm: (name: String, bio: String, pictureBytes: ByteArray?, contentType: String?) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -80,6 +79,7 @@ fun ProfileEditor(
|
|||||||
var name by remember(initialName) { mutableStateOf(initialName) }
|
var name by remember(initialName) { mutableStateOf(initialName) }
|
||||||
var bio by remember(initialBio) { mutableStateOf(initialBio) }
|
var bio by remember(initialBio) { mutableStateOf(initialBio) }
|
||||||
var picture by remember(initialPicture) { mutableStateOf(initialPicture) }
|
var picture by remember(initialPicture) { mutableStateOf(initialPicture) }
|
||||||
|
var isBusy by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val hasPicture = remember(picture) {
|
val hasPicture = remember(picture) {
|
||||||
when (picture) {
|
when (picture) {
|
||||||
@@ -267,14 +267,20 @@ fun ProfileEditor(
|
|||||||
.size(ButtonDefaults.MediumContainerHeight),
|
.size(ButtonDefaults.MediumContainerHeight),
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val bytes = withContext(Dispatchers.IO) {
|
isBusy = true
|
||||||
(picture as? Uri)?.let {
|
try {
|
||||||
context.contentResolver.openInputStream(it)?.readBytes()
|
val bytes = withContext(Dispatchers.IO) {
|
||||||
|
(picture as? Uri)?.let {
|
||||||
|
context.contentResolver.openInputStream(it)?.readBytes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val type =
|
||||||
|
(picture as? Uri)?.let { context.contentResolver.getType(it) }
|
||||||
|
onConfirm(name, bio, bytes, type)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
snackbarHostState.showSnackbar(e.message ?: "Error")
|
||||||
}
|
}
|
||||||
val type =
|
isBusy = false
|
||||||
(picture as? Uri)?.let { context.contentResolver.getType(it) }
|
|
||||||
onConfirm(name, bio, bytes, type)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = name.isNotBlank() && !isBusy
|
enabled = name.isNotBlank() && !isBusy
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.AsyncNostrSigner
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
|
import rust.nostr.sdk.EncryptedSecretKey
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
@@ -20,7 +21,6 @@ import su.reya.coop.storage.SecretStorage
|
|||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
data class AuthState(
|
data class AuthState(
|
||||||
val isBusy: Boolean = false,
|
|
||||||
val signerRequired: Boolean? = null,
|
val signerRequired: Boolean? = null,
|
||||||
val isNotificationBannerDismissed: Boolean = false,
|
val isNotificationBannerDismissed: Boolean = false,
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ class AuthViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
val signer = createSigner(secret)
|
val (signer, _) = createSigner(secret)
|
||||||
nostr.setSigner(signer)
|
nostr.setSigner(signer)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
_state.update { it.copy(signerRequired = false) }
|
_state.update { it.copy(signerRequired = false) }
|
||||||
@@ -87,8 +87,6 @@ class AuthViewModel(
|
|||||||
fun logout(onLogout: () -> Unit = {}) {
|
fun logout(onLogout: () -> Unit = {}) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
_state.update { it.copy(isBusy = true) }
|
|
||||||
|
|
||||||
// Reset the nostr signer and prune the database
|
// Reset the nostr signer and prune the database
|
||||||
nostr.signer.switch(Keys.generate())
|
nostr.signer.switch(Keys.generate())
|
||||||
nostr.prune()
|
nostr.prune()
|
||||||
@@ -98,11 +96,10 @@ class AuthViewModel(
|
|||||||
// Clear credentials from persistent storage
|
// Clear credentials from persistent storage
|
||||||
secretStore.clear(KEY_USER_SIGNER)
|
secretStore.clear(KEY_USER_SIGNER)
|
||||||
secretStore.clear(KEY_BANNER_DISMISSED)
|
secretStore.clear(KEY_BANNER_DISMISSED)
|
||||||
|
|
||||||
// Call cleanup callback (e.g. to reset other ViewModels)
|
// Call cleanup callback (e.g. to reset other ViewModels)
|
||||||
onLogout()
|
onLogout()
|
||||||
|
// Reset local states
|
||||||
_state.update { it.copy(isBusy = false, signerRequired = true) }
|
_state.update { it.copy(signerRequired = true) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,28 +113,35 @@ class AuthViewModel(
|
|||||||
|
|
||||||
private suspend fun getOrInitAppKeys(): Keys {
|
private suspend fun getOrInitAppKeys(): Keys {
|
||||||
val secret = secretStore.get(KEY_APP_KEYS)
|
val secret = secretStore.get(KEY_APP_KEYS)
|
||||||
|
|
||||||
// If app keys are already stored, use them
|
// If app keys are already stored, use them
|
||||||
if (secret != null) {
|
if (secret != null) return Keys.parse(secret)
|
||||||
return Keys.parse(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new app keys and save to the secret storage
|
// Generate new app keys and save to the secret storage
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
secretStore.set(KEY_APP_KEYS, keys.secretKey().toBech32())
|
secretStore.set(KEY_APP_KEYS, keys.secretKey().toBech32())
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createSigner(secret: String): AsyncNostrSigner {
|
private suspend fun createSigner(
|
||||||
|
secret: String,
|
||||||
|
password: String? = null
|
||||||
|
): Pair<AsyncNostrSigner, String?> {
|
||||||
return when {
|
return when {
|
||||||
secret.startsWith("nsec1") -> Keys.parse(secret)
|
secret.startsWith("nsec1") -> Keys.parse(secret) to null
|
||||||
|
|
||||||
|
secret.startsWith("ncryptsec1") -> {
|
||||||
|
if (password == null) throw IllegalArgumentException("Password is required")
|
||||||
|
val enc = EncryptedSecretKey.fromBech32(secret)
|
||||||
|
val secret = enc.decrypt(password)
|
||||||
|
val keys = Keys(secret)
|
||||||
|
|
||||||
|
keys to keys.secretKey().toBech32()
|
||||||
|
}
|
||||||
|
|
||||||
secret.startsWith("bunker://") -> {
|
secret.startsWith("bunker://") -> {
|
||||||
val appKeys = getOrInitAppKeys()
|
val appKeys = getOrInitAppKeys()
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
val bunker = NostrConnectUri.parse(secret)
|
||||||
val timeout = 50.seconds
|
val timeout = 50.seconds
|
||||||
NostrConnect(uri = bunker, appKeys, timeout, null)
|
NostrConnect(uri = bunker, appKeys, timeout, null) to null
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.startsWith("nip55://") -> {
|
secret.startsWith("nip55://") -> {
|
||||||
@@ -150,77 +154,50 @@ class AuthViewModel(
|
|||||||
val pubkey = PublicKey.parse(parts[1])
|
val pubkey = PublicKey.parse(parts[1])
|
||||||
|
|
||||||
handler.setPackageName(packageName)
|
handler.setPackageName(packageName)
|
||||||
ExternalSignerProxy(handler, pubkey)
|
ExternalSignerProxy(handler, pubkey) to null
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("Invalid secret format")
|
else -> throw IllegalArgumentException("Invalid secret format")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun verifyIdentity(secret: String): PublicKey? {
|
suspend fun importIdentity(secret: String, password: String? = null) {
|
||||||
try {
|
val (signer, decryptedSecret) = createSigner(secret, password)
|
||||||
val signer = createSigner(secret)
|
// Update signer
|
||||||
if (secret.startsWith("bunker://")) {
|
nostr.setSigner(signer)
|
||||||
showError("Please approve the connection.")
|
// Persist the secret in the secret storage
|
||||||
}
|
secretStore.set(KEY_USER_SIGNER, decryptedSecret ?: secret)
|
||||||
return signer.getPublicKeyAsync()
|
// Update local states
|
||||||
} catch (e: Exception) {
|
_state.update { it.copy(signerRequired = false) }
|
||||||
showError("Error: ${e.message}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun importIdentity(secret: String) {
|
|
||||||
_state.update { it.copy(isBusy = true) }
|
|
||||||
try {
|
|
||||||
val signer = createSigner(secret)
|
|
||||||
// Update signer
|
|
||||||
nostr.setSigner(signer)
|
|
||||||
// Persist the secret in the secret storage
|
|
||||||
secretStore.set(KEY_USER_SIGNER, secret)
|
|
||||||
// Update local states
|
|
||||||
_state.update { it.copy(signerRequired = false, isBusy = false) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
_state.update { it.copy(isBusy = false) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun connectExternalSigner() {
|
suspend fun connectExternalSigner() {
|
||||||
val handler = externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
val handler = externalSignerHandler ?: throw IllegalStateException("Signer not available")
|
||||||
_state.update { it.copy(isBusy = true) }
|
|
||||||
try {
|
|
||||||
val permissions = SignerPermissions.toJson(
|
|
||||||
listOf(
|
|
||||||
SignerPermissions.signEvent(0),
|
|
||||||
SignerPermissions.signEvent(3),
|
|
||||||
SignerPermissions.signEvent(10000),
|
|
||||||
SignerPermissions.signEvent(10050),
|
|
||||||
SignerPermissions.signEvent(10063),
|
|
||||||
SignerPermissions.signEvent(22242),
|
|
||||||
SignerPermissions.signEvent(30030),
|
|
||||||
SignerPermissions.signEvent(30315),
|
|
||||||
SignerPermissions.nip44Encrypt(),
|
|
||||||
SignerPermissions.nip44Decrypt(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = handler.getPublicKey(permissions) ?: throw Exception("Rejected")
|
val permissions = SignerPermissions.toJson(
|
||||||
val signer = ExternalSignerProxy(handler, result.pubkey)
|
listOf(
|
||||||
|
SignerPermissions.signEvent(0),
|
||||||
// Update signer
|
SignerPermissions.signEvent(3),
|
||||||
nostr.setSigner(signer)
|
SignerPermissions.signEvent(10000),
|
||||||
// Store the signer in the secret storage
|
SignerPermissions.signEvent(10050),
|
||||||
secretStore.set(
|
SignerPermissions.signEvent(10063),
|
||||||
KEY_USER_SIGNER,
|
SignerPermissions.signEvent(22242),
|
||||||
"nip55://${result.packageName}/${result.pubkey.toHex()}"
|
SignerPermissions.signEvent(30030),
|
||||||
|
SignerPermissions.signEvent(30315),
|
||||||
|
SignerPermissions.nip44Encrypt(),
|
||||||
|
SignerPermissions.nip44Decrypt(),
|
||||||
)
|
)
|
||||||
// Update local states
|
)
|
||||||
_state.update { it.copy(signerRequired = false, isBusy = false) }
|
|
||||||
} catch (e: Exception) {
|
val result = handler.getPublicKey(permissions) ?: throw Exception("Rejected")
|
||||||
_state.update { it.copy(isBusy = false) }
|
val signer = ExternalSignerProxy(handler, result.pubkey)
|
||||||
showError("Notice: ${e.message}")
|
|
||||||
}
|
// Update signer
|
||||||
|
nostr.setSigner(signer)
|
||||||
|
// Store the signer in the secret storage
|
||||||
|
secretStore.set(KEY_USER_SIGNER, "nip55://${result.packageName}/${result.pubkey.toHex()}")
|
||||||
|
// Update local states
|
||||||
|
_state.update { it.copy(signerRequired = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isExternalSignerAvailable(): Boolean {
|
fun isExternalSignerAvailable(): Boolean {
|
||||||
@@ -233,27 +210,16 @@ class AuthViewModel(
|
|||||||
picture: ByteArray?,
|
picture: ByteArray?,
|
||||||
contentType: String? = null
|
contentType: String? = null
|
||||||
) {
|
) {
|
||||||
_state.update { it.copy(isBusy = true) }
|
|
||||||
|
|
||||||
val keys = Keys.generate()
|
val keys = Keys.generate()
|
||||||
val secret = keys.secretKey().toBech32()
|
val secret = keys.secretKey().toBech32()
|
||||||
|
val avatarUrl = picture?.let {
|
||||||
try {
|
mediaRepository.blossomUpload(keys, it, contentType ?: "image/jpeg")
|
||||||
val avatarUrl = picture?.let {
|
|
||||||
mediaRepository.blossomUpload(nostr.signer.get(), it, contentType ?: "image/jpeg")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create identity
|
|
||||||
nostr.profiles.createIdentity(keys = keys, name = name, bio = bio, picture = avatarUrl)
|
|
||||||
|
|
||||||
// Persist the secret in the secret storage
|
|
||||||
secretStore.set(KEY_USER_SIGNER, secret)
|
|
||||||
|
|
||||||
// Update local states
|
|
||||||
_state.update { it.copy(isBusy = false, signerRequired = false) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError("Error: ${e.message}")
|
|
||||||
_state.update { it.copy(isBusy = false) }
|
|
||||||
}
|
}
|
||||||
|
// Create identity
|
||||||
|
nostr.profiles.createIdentity(keys = keys, name = name, bio = bio, picture = avatarUrl)
|
||||||
|
// Persist the secret in the secret storage
|
||||||
|
secretStore.set(KEY_USER_SIGNER, secret)
|
||||||
|
// Update local states
|
||||||
|
_state.update { it.copy(signerRequired = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user