feat(desktop): add nostrconnect:// login flow for QR-based signer pairing
Adds client-initiated NIP-46 (nostrconnect://) flow so users can pair with a remote signer (e.g. Amber) by scanning a QR code instead of manually copying a bunker:// URI. Changes: - Fix RemoteSignerManager to decrypt NIP-44 content before parsing - Add CoroutineScope to NostrSignerRemote for async event callbacks - Add loginWithNostrConnect() to AccountManager with 120s timeout - Add QrCodeCanvas composable using ZXing for QR generation - Add "Connect" tab to LoginCard showing QR + copyable URI + waiting state - Wire nostrconnect flow through LoginScreen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-1
@@ -42,6 +42,11 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NostrSignerRemote(
|
||||
val signer: NostrSignerInternal,
|
||||
@@ -51,6 +56,8 @@ class NostrSignerRemote(
|
||||
val permissions: String? = null,
|
||||
val secret: String? = null,
|
||||
) : NostrSigner(signer.pubKey) {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
private val manager =
|
||||
RemoteSignerManager(
|
||||
signer = signer,
|
||||
@@ -69,7 +76,13 @@ class NostrSignerRemote(
|
||||
),
|
||||
) { event ->
|
||||
if (event is NostrConnectEvent) {
|
||||
manager.newResponse(event)
|
||||
scope.launch {
|
||||
try {
|
||||
manager.newResponse(event)
|
||||
} catch (_: Exception) {
|
||||
// Ignore events we can't decrypt or parse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +92,7 @@ class NostrSignerRemote(
|
||||
|
||||
fun closeSubscription() {
|
||||
subscription.close()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
override fun isWriteable(): Boolean = true
|
||||
|
||||
+3
-2
@@ -41,8 +41,9 @@ class RemoteSignerManager(
|
||||
) {
|
||||
private val awaitingRequests = LargeCache<String, Continuation<BunkerResponse>>()
|
||||
|
||||
fun newResponse(responseEvent: NostrConnectEvent) {
|
||||
val bunkerResponse = OptimizedJsonMapper.fromJsonTo<BunkerResponse>(responseEvent.content)
|
||||
suspend fun newResponse(responseEvent: NostrConnectEvent) {
|
||||
val decryptedJson = signer.decrypt(responseEvent.content, remoteKey)
|
||||
val bunkerResponse = OptimizedJsonMapper.fromJsonTo<BunkerResponse>(decryptedJson)
|
||||
awaitingRequests.get(bunkerResponse.id)?.resume(bunkerResponse)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user