From 0ad872d18656b879576a439d79c61b1668a8320f Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 07:20:43 +0700 Subject: [PATCH 1/6] remove toast message --- .../su/reya/coop/screens/ImportScreen.kt | 18 +++++++++++------- .../su/reya/coop/viewmodel/AuthViewModel.kt | 3 --- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt index aea7ee5..32f7259 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt @@ -242,13 +242,17 @@ fun ImportScreen() { Button( onClick = { scope.launch { - if (pubkey == null) { - authViewModel.verifyIdentity(secret).let { pubkey = it } - } else { - // Import the identity - authViewModel.importIdentity(secret) - // Navigate to the home screen - navigator.navigate(Screen.Home) + try { + if (pubkey == null) { + authViewModel.verifyIdentity(secret).let { pubkey = it } + } else { + // Import the identity + authViewModel.importIdentity(secret) + // Navigate to the home screen + navigator.navigate(Screen.Home) + } + } catch (e: Exception) { + snackbarHostState.showSnackbar("Error: ${e.message}") } } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt index 5d8b226..12dfa8c 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt @@ -160,9 +160,6 @@ class AuthViewModel( suspend fun verifyIdentity(secret: String): PublicKey? { try { val signer = createSigner(secret) - if (secret.startsWith("bunker://")) { - showError("Please approve the connection.") - } return signer.getPublicKeyAsync() } catch (e: Exception) { showError("Error: ${e.message}") -- 2.49.1 From 8750eaa225c47a6f558c4485472f6bf36578c833 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 07:27:50 +0700 Subject: [PATCH 2/6] fix crash --- .../src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt index d0283e3..ef18d2b 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -626,8 +626,8 @@ fun NewRequests(requests: List) { val secondRoom = requests.getOrNull(1) 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 { total == 1 -> { val message = firstRoom.lastMessage ?: "" -- 2.49.1 From d23b15df6f045569dfc0abd0235b43982d9527cb Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 09:15:25 +0700 Subject: [PATCH 3/6] refactor --- .../kotlin/su/reya/coop/screens/HomeScreen.kt | 2 +- .../su/reya/coop/screens/ImportScreen.kt | 108 +++++++++++------- .../su/reya/coop/screens/OnboardingScreen.kt | 3 + .../su/reya/coop/viewmodel/AuthViewModel.kt | 101 +++++++--------- 4 files changed, 107 insertions(+), 107 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt index ef18d2b..f2fbe89 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/HomeScreen.kt @@ -627,7 +627,7 @@ fun NewRequests(requests: List) { val firstRoomState by (firstRoom as Room).rememberUiState(nostrViewModel) val secondRoomState by (secondRoom ?: firstRoom).rememberUiState(nostrViewModel) - + val supportingText = when { total == 1 -> { val message = firstRoom.lastMessage ?: "" diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt index 32f7259..8aa2f50 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt @@ -48,23 +48,18 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back import coop.composeapp.generated.resources.ic_scanner -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import rust.nostr.sdk.Keys import rust.nostr.sdk.NostrConnectUri -import rust.nostr.sdk.PublicKey import su.reya.coop.LocalAuthViewModel import su.reya.coop.LocalNavigator -import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalScanResult import su.reya.coop.LocalSnackbarHostState import su.reya.coop.Screen -import su.reya.coop.shared.Avatar import su.reya.coop.shared.getExpressiveFontFamily @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -74,19 +69,13 @@ fun ImportScreen() { val navigator = LocalNavigator.current val qrScanResult = LocalScanResult.current val focusManager = LocalFocusManager.current - val nostrViewModel = LocalNostrViewModel.current val authViewModel = LocalAuthViewModel.current - val scope = rememberCoroutineScope() - val authState by authViewModel.state.collectAsStateWithLifecycle() - val isBusy = authState.isBusy var secret by remember { mutableStateOf("") } - var pubkey by remember { mutableStateOf(null) } - - val profile by remember(pubkey) { - pubkey?.let(nostrViewModel::getMetadata) ?: flowOf(null) - }.collectAsStateWithLifecycle(null) + var password by remember { mutableStateOf("") } + var requirePassword by remember { mutableStateOf(false) } + var loading by remember { mutableStateOf(false) } LaunchedEffect(qrScanResult.content) { qrScanResult.content?.let { result -> @@ -101,13 +90,19 @@ fun ImportScreen() { }.onSuccess { secret = result }.onFailure { e -> - snackbarHostState.showSnackbar("Invalid secret: ${e.message}") + e.message?.let { snackbarHostState.showSnackbar(it) } } // Clear the nav state qrScanResult.clear() } } + LaunchedEffect(secret) { + if (secret.startsWith("ncryptsec1")) { + requirePassword = true + } + } + Scaffold( containerColor = MaterialTheme.colorScheme.surfaceContainer, snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -160,21 +155,14 @@ fun ImportScreen() { .clip(MaterialShapes.Cookie9Sided.toShape()), contentAlignment = Alignment.Center ) { - Avatar( - picture = profile?.picture, - description = "Profile picture", - modifier = Modifier.fillMaxSize(), - shape = MaterialShapes.Cookie9Sided.toShape(), + Text( + text = "", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLargeEmphasized.copy( + fontFamily = getExpressiveFontFamily() + ), ) } - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = profile?.name ?: "", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLargeEmphasized.copy( - fontFamily = getExpressiveFontFamily() - ), - ) } Surface( modifier = Modifier @@ -186,7 +174,7 @@ fun ImportScreen() { Column( modifier = Modifier .fillMaxSize() - .padding(24.dp) + .padding(24.dp), ) { Column( modifier = Modifier @@ -203,9 +191,9 @@ fun ImportScreen() { BasicTextField( value = secret, onValueChange = { secret = it }, - enabled = !isBusy, + enabled = !loading, modifier = Modifier.fillMaxWidth(), - maxLines = 4, + singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, ), @@ -237,36 +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)) Button( onClick = { scope.launch { + loading = true try { - if (pubkey == null) { - authViewModel.verifyIdentity(secret).let { pubkey = it } - } else { - // Import the identity - authViewModel.importIdentity(secret) - // Navigate to the home screen - navigator.navigate(Screen.Home) - } + // Import the identity + authViewModel.importIdentity(secret, password) + // Navigate to the home screen + navigator.navigate(Screen.Home) } catch (e: Exception) { - snackbarHostState.showSnackbar("Error: ${e.message}") + snackbarHostState.showSnackbar(e.message ?: "Error") + loading = false } } - }, modifier = Modifier .fillMaxWidth() .height(ButtonDefaults.MediumContainerHeight), - enabled = secret.isNotBlank() && !isBusy, + enabled = secret.isNotBlank() && !loading, ) { - if (isBusy) { + if (loading) { LoadingIndicator() } else { Text( - text = if (pubkey == null) "Verify" else "Click again to Continue", + text = "Continue", style = MaterialTheme.typography.titleMediumEmphasized, ) } diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt index bbdf7d6..a042cd7 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/OnboardingScreen.kt @@ -160,7 +160,10 @@ fun OnboardingScreen() { scope.launch { if (authViewModel.isExternalSignerAvailable()) { try { + // Connect to the external signer + // TODO: show all available signers? authViewModel.connectExternalSigner() + // Navigate to the home screen navigator.navigate(Screen.Home) } catch (e: Exception) { e.message?.let { snackbarHostState.showSnackbar(it) } diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt index 12dfa8c..c92febd 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import rust.nostr.sdk.AsyncNostrSigner +import rust.nostr.sdk.EncryptedSecretKey import rust.nostr.sdk.Keys import rust.nostr.sdk.NostrConnect import rust.nostr.sdk.NostrConnectUri @@ -116,23 +117,25 @@ class AuthViewModel( private suspend fun getOrInitAppKeys(): Keys { val secret = secretStore.get(KEY_APP_KEYS) - // If app keys are already stored, use them - if (secret != null) { - return Keys.parse(secret) - } - + if (secret != null) return Keys.parse(secret) // Generate new app keys and save to the secret storage val keys = Keys.generate() secretStore.set(KEY_APP_KEYS, keys.secretKey().toBech32()) - return keys } - private suspend fun createSigner(secret: String): AsyncNostrSigner { + private suspend fun createSigner(secret: String, password: String? = null): AsyncNostrSigner { return when { secret.startsWith("nsec1") -> Keys.parse(secret) + secret.startsWith("ncryptsec1") -> { + if (password == null) throw IllegalArgumentException("Password is required") + val enc = EncryptedSecretKey.fromBech32(secret) + val secret = enc.decrypt(password) + Keys(secret) + } + secret.startsWith("bunker://") -> { val appKeys = getOrInitAppKeys() val bunker = NostrConnectUri.parse(secret) @@ -157,67 +160,41 @@ class AuthViewModel( } } - suspend fun verifyIdentity(secret: String): PublicKey? { - try { - val signer = createSigner(secret) - return signer.getPublicKeyAsync() - } catch (e: Exception) { - 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 importIdentity(secret: String, password: String? = null) { + val signer = createSigner(secret, password) + // Update signer + nostr.setSigner(signer) + // Persist the secret in the secret storage + secretStore.set(KEY_USER_SIGNER, secret) } suspend fun connectExternalSigner() { 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 signer = ExternalSignerProxy(handler, result.pubkey) - - // Update signer - nostr.setSigner(signer) - // Store the signer in the secret storage - secretStore.set( - KEY_USER_SIGNER, - "nip55://${result.packageName}/${result.pubkey.toHex()}" + 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(), ) - // Update local states - _state.update { it.copy(signerRequired = false, isBusy = false) } - } catch (e: Exception) { - _state.update { it.copy(isBusy = false) } - showError("Notice: ${e.message}") - } + ) + + val result = handler.getPublicKey(permissions) ?: throw Exception("Rejected") + val signer = ExternalSignerProxy(handler, result.pubkey) + + // 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 { -- 2.49.1 From a48c0c0e7cb4d5ca2fa043bdd37278b3e5a1a6ce Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 09:26:45 +0700 Subject: [PATCH 4/6] update --- .../su/reya/coop/screens/NewIdentityScreen.kt | 6 ---- .../su/reya/coop/shared/ProfileEditor.kt | 20 +++++++---- .../su/reya/coop/viewmodel/AuthViewModel.kt | 35 ++++++------------- 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt index 758d75d..80a3393 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/NewIdentityScreen.kt @@ -1,9 +1,7 @@ package su.reya.coop.screens import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import su.reya.coop.LocalAuthViewModel import su.reya.coop.LocalNavigator @@ -16,13 +14,9 @@ fun NewIdentityScreen() { val navigator = LocalNavigator.current val scope = rememberCoroutineScope() - val authState by authViewModel.state.collectAsStateWithLifecycle() - val isBusy = authState.isBusy - ProfileEditor( title = "Create a new identity", buttonLabel = "Continue", - isBusy = isBusy, onBack = { navigator.goBack() }, onConfirm = { name, bio, bytes, type -> scope.launch { diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt index 9aab16a..e975f85 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/shared/ProfileEditor.kt @@ -68,7 +68,6 @@ fun ProfileEditor( initialName: String = "", initialBio: String = "", initialPicture: Any? = null, // Accepts Uri (picked) or String (current URL) - isBusy: Boolean = false, onBack: () -> 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 bio by remember(initialBio) { mutableStateOf(initialBio) } var picture by remember(initialPicture) { mutableStateOf(initialPicture) } + var isBusy by remember { mutableStateOf(false) } val hasPicture = remember(picture) { when (picture) { @@ -267,14 +267,20 @@ fun ProfileEditor( .size(ButtonDefaults.MediumContainerHeight), onClick = { scope.launch { - val bytes = withContext(Dispatchers.IO) { - (picture as? Uri)?.let { - context.contentResolver.openInputStream(it)?.readBytes() + isBusy = true + try { + 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 = - (picture as? Uri)?.let { context.contentResolver.getType(it) } - onConfirm(name, bio, bytes, type) + isBusy = false } }, enabled = name.isNotBlank() && !isBusy diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt index c92febd..56fbbf2 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt @@ -21,7 +21,6 @@ import su.reya.coop.storage.SecretStorage import kotlin.time.Duration.Companion.seconds data class AuthState( - val isBusy: Boolean = false, val signerRequired: Boolean? = null, val isNotificationBannerDismissed: Boolean = false, ) @@ -88,8 +87,6 @@ class AuthViewModel( fun logout(onLogout: () -> Unit = {}) { viewModelScope.launch { try { - _state.update { it.copy(isBusy = true) } - // Reset the nostr signer and prune the database nostr.signer.switch(Keys.generate()) nostr.prune() @@ -99,11 +96,10 @@ class AuthViewModel( // Clear credentials from persistent storage secretStore.clear(KEY_USER_SIGNER) secretStore.clear(KEY_BANNER_DISMISSED) - // Call cleanup callback (e.g. to reset other ViewModels) onLogout() - - _state.update { it.copy(isBusy = false, signerRequired = true) } + // Reset local states + _state.update { it.copy(signerRequired = true) } } } } @@ -207,27 +203,16 @@ class AuthViewModel( picture: ByteArray?, contentType: String? = null ) { - _state.update { it.copy(isBusy = true) } - val keys = Keys.generate() val secret = keys.secretKey().toBech32() - - try { - 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) } + 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(signerRequired = false) } } } -- 2.49.1 From f49279fd100d3e82dbeae30005d352384863e696 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 10:04:23 +0700 Subject: [PATCH 5/6] fix --- .../su/reya/coop/viewmodel/AuthViewModel.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt index 56fbbf2..1c22e6c 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt @@ -69,7 +69,7 @@ class AuthViewModel( } runCatching { - val signer = createSigner(secret) + val (signer, _) = createSigner(secret) nostr.setSigner(signer) }.onSuccess { _state.update { it.copy(signerRequired = false) } @@ -121,22 +121,27 @@ class AuthViewModel( return keys } - private suspend fun createSigner(secret: String, password: String? = null): AsyncNostrSigner { + private suspend fun createSigner( + secret: String, + password: String? = null + ): Pair { 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) - Keys(secret) + val keys = Keys(secret) + + keys to keys.secretKey().toBech32() } secret.startsWith("bunker://") -> { val appKeys = getOrInitAppKeys() val bunker = NostrConnectUri.parse(secret) val timeout = 50.seconds - NostrConnect(uri = bunker, appKeys, timeout, null) + NostrConnect(uri = bunker, appKeys, timeout, null) to null } secret.startsWith("nip55://") -> { @@ -149,7 +154,7 @@ class AuthViewModel( val pubkey = PublicKey.parse(parts[1]) handler.setPackageName(packageName) - ExternalSignerProxy(handler, pubkey) + ExternalSignerProxy(handler, pubkey) to null } else -> throw IllegalArgumentException("Invalid secret format") @@ -157,11 +162,13 @@ class AuthViewModel( } suspend fun importIdentity(secret: String, password: String? = null) { - val signer = createSigner(secret, password) + val (signer, decryptedSecret) = createSigner(secret, password) // Update signer nostr.setSigner(signer) // Persist the secret in the secret storage - secretStore.set(KEY_USER_SIGNER, secret) + secretStore.set(KEY_USER_SIGNER, decryptedSecret ?: secret) + // Update local states + _state.update { it.copy(signerRequired = false) } } suspend fun connectExternalSigner() { @@ -206,7 +213,7 @@ class AuthViewModel( val keys = Keys.generate() val secret = keys.secretKey().toBech32() val avatarUrl = picture?.let { - mediaRepository.blossomUpload(nostr.signer.get(), it, contentType ?: "image/jpeg") + mediaRepository.blossomUpload(keys, it, contentType ?: "image/jpeg") } // Create identity nostr.profiles.createIdentity(keys = keys, name = name, bio = bio, picture = avatarUrl) -- 2.49.1 From 24d9e9673d0b21daaf0b8bfe6690303468a73d4e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 6 Jul 2026 14:28:12 +0700 Subject: [PATCH 6/6] . --- .../composeResources/drawable/ic_login.xml | 10 +++++++ .../su/reya/coop/screens/ImportScreen.kt | 29 +------------------ .../reya/coop/screens/UpdateProfileScreen.kt | 2 -- .../su/reya/coop/viewmodel/AuthViewModel.kt | 2 +- 4 files changed, 12 insertions(+), 31 deletions(-) create mode 100644 composeApp/src/androidMain/composeResources/drawable/ic_login.xml diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_login.xml b/composeApp/src/androidMain/composeResources/drawable/ic_login.xml new file mode 100644 index 0000000..820129b --- /dev/null +++ b/composeApp/src/androidMain/composeResources/drawable/ic_login.xml @@ -0,0 +1,10 @@ + + + diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt index 8aa2f50..967b712 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ImportScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator -import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -30,7 +29,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.toShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -40,13 +38,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_arrow_back @@ -60,7 +56,6 @@ import su.reya.coop.LocalNavigator import su.reya.coop.LocalScanResult import su.reya.coop.LocalSnackbarHostState import su.reya.coop.Screen -import su.reya.coop.shared.getExpressiveFontFamily @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -80,7 +75,7 @@ fun ImportScreen() { LaunchedEffect(qrScanResult.content) { qrScanResult.content?.let { result -> runCatching { - if (result.startsWith("nsec1")) { + if (result.startsWith("nsec1") || result.startsWith("ncryptsec1")) { Keys.parse(result) } else if (result.startsWith("bunker://")) { NostrConnectUri.parse(result) @@ -142,28 +137,6 @@ fun ImportScreen() { .padding(top = innerPadding.calculateTopPadding()) .imePadding(), ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(120.dp) - .clip(MaterialShapes.Cookie9Sided.toShape()), - contentAlignment = Alignment.Center - ) { - Text( - text = "", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLargeEmphasized.copy( - fontFamily = getExpressiveFontFamily() - ), - ) - } - } Surface( modifier = Modifier .fillMaxWidth() diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt index 074a9b7..efa531a 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/UpdateProfileScreen.kt @@ -15,7 +15,6 @@ fun UpdateProfileScreen() { val navigator = LocalNavigator.current val scope = rememberCoroutineScope() - val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false) val currentUser by nostrViewModel.currentUserProfile.collectAsStateWithLifecycle() val profile = currentUser?.metadata?.asRecord() @@ -25,7 +24,6 @@ fun UpdateProfileScreen() { initialName = profile?.displayName ?: profile?.name ?: "", initialBio = profile?.about ?: "", initialPicture = profile?.picture, - isBusy = isBusy, onBack = { navigator.goBack() }, onConfirm = { name, bio, bytes, type -> scope.launch { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt index 1c22e6c..e77db2e 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/viewmodel/AuthViewModel.kt @@ -59,7 +59,7 @@ class AuthViewModel( private fun login() { viewModelScope.launch { try { - val secret = withTimeoutOrNull(3.seconds) { + val secret = withTimeoutOrNull(5.seconds) { secretStore.get(KEY_USER_SIGNER) } -- 2.49.1