diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 30fa7e80c0..bfb3a0f6d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectURI import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -224,6 +225,12 @@ fun uriToRoute( return connectedAppRoute(uri) } + // A scanned/opened `nostrconnect://` offer is an app asking to connect to our signer: open the + // NIP-46 signer screen and let it run the pairing (it enables the signer as part of connecting). + if (uri.startsWith(NostrConnectURI.NOSTRCONNECT_SCHEME)) { + return Route.Nip46Signer(connectUri = uri) + } + relayGroupInviteRoute(uri)?.let { return it } concordInviteRoute(uri)?.let { return it } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 5ab0cc7d03..48e98fb6e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -424,7 +424,7 @@ fun BuildNavigation( composableFromEndArgs(capWidth = false) { NostrAppScreen(it.coordinate, accountViewModel, nav) } composableFromEnd { ConnectedAppsScreen(accountViewModel, nav) } composableFromEndArgs { ConnectedAppDetailScreen(it.coordinate, accountViewModel, nav) } - composableFromEnd { Nip46SignerScreen(accountViewModel, nav) } + composableFromEndArgs { Nip46SignerScreen(accountViewModel, nav, it.connectUri) } composableFromEnd { RelayAuthSettingsScreen(accountViewModel, nav) } composableFromEndArgs { SoftwareAppDetailScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } composableFromEnd { CalendarsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 952668dd60..4b4f2e3347 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -107,7 +107,10 @@ sealed class Route { @Serializable object ConnectedApps : Route() - @Serializable object Nip46Signer : Route() + @Serializable data class Nip46Signer( + /** When set (from a scanned/opened `nostrconnect://` offer), the screen connects that app on open. */ + val connectUri: String? = null, + ) : Route() @Serializable object RelayAuthSettings : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index fcebc84a5d..473bbb9a09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -75,7 +75,7 @@ fun buildSettingsCatalog( symEntry(R.string.profile_badges_title, MaterialSymbols.MilitaryTech, R.string.profile_badges_search_keywords, Route.ProfileBadges), symEntry(R.string.payment_targets, MaterialSymbols.Payment, R.string.payment_targets_search_keywords, Route.EditPaymentTargets), symEntry(R.string.napplet_permissions_title, MaterialSymbols.Apps, R.string.napplet_connected_apps_search_keywords, Route.ConnectedApps), - symEntry(R.string.nip46_signer_title, MaterialSymbols.Key, R.string.nip46_signer_search_keywords, Route.Nip46Signer), + symEntry(R.string.nip46_signer_title, MaterialSymbols.Key, R.string.nip46_signer_search_keywords, Route.Nip46Signer()), symEntry(R.string.relay_auth_settings_title, MaterialSymbols.Lock, R.string.relay_auth_search_keywords, Route.RelayAuthSettings), symEntry(R.string.security_filters, MaterialSymbols.Security, R.string.security_filters_search_keywords, Route.SecurityFilters), symEntry(R.string.call_settings, MaterialSymbols.Phone, R.string.call_settings_search_keywords, Route.CallSettings), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt index b57613e86c..293ad1d25e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt @@ -101,6 +101,7 @@ private val LiveGreen = Color(0xFF3DDC84) fun Nip46SignerScreen( accountViewModel: AccountViewModel, nav: INav, + connectUri: String? = null, ) { val account = accountViewModel.account val signer = account.nip46Signer @@ -138,6 +139,11 @@ fun Nip46SignerScreen( } } + // Opened from a scanned/shared nostrconnect:// offer — pair that app on open (this also enables the signer). + LaunchedEffect(connectUri) { + if (!connectUri.isNullOrBlank()) onConnect(connectUri) + } + if (scanning) { SimpleQrCodeScanner { contents -> scanning = false @@ -162,47 +168,52 @@ fun Nip46SignerScreen( return@Column } - if (!enabled) { - DisabledHero(onEnable = { signer.setEnabled(true) }) - return@Column - } - - LiveStatusCard( - relayCount = relays.size, - connectedCount = connectedCount, - onToggleOff = { signer.setEnabled(false) }, - ) - - if (relays.isEmpty()) { - WarningCard(stringResource(R.string.nip46_signer_status_no_relays)) - } - - bunkerUri?.let { uri -> - QrHeroCard( - uri = uri, - onCopy = { - clipboard.setText(AnnotatedString(uri)) - Toast.makeText(context, R.string.nip46_signer_copied, Toast.LENGTH_SHORT).show() - }, - onRegenerate = { confirmRotate = true }, + if (enabled) { + LiveStatusCard( + relayCount = relays.size, + connectedCount = connectedCount, + onToggleOff = { signer.setEnabled(false) }, ) + + if (relays.isEmpty()) { + WarningCard(stringResource(R.string.nip46_signer_status_no_relays)) + } + + bunkerUri?.let { uri -> + QrHeroCard( + uri = uri, + onCopy = { + clipboard.setText(AnnotatedString(uri)) + Toast.makeText(context, R.string.nip46_signer_copied, Toast.LENGTH_SHORT).show() + }, + onRegenerate = { confirmRotate = true }, + ) + } + } else { + DisabledHero(onEnable = { signer.setEnabled(true) }) } + // The Connect section stays reachable even while off: scanning an app's nostrconnect:// + // code pairs it and enables the signer as part of connecting (no separate "enable" step). ConnectSection( onScan = { scanning = true }, onPaste = { onConnect(it) }, ) - ConnectedAppsRow( - count = connectedCount, - onClick = { nav.nav(Route.ConnectedApps) }, - ) + if (enabled || connectedCount > 0) { + ConnectedAppsRow( + count = connectedCount, + onClick = { nav.nav(Route.ConnectedApps) }, + ) + } - Text( - stringResource(R.string.nip46_signer_background_hint), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + if (enabled) { + Text( + stringResource(R.string.nip46_signer_background_hint), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 4ab827baf0..7ef6f6d57b 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -923,8 +923,8 @@ Paste a nostrconnect:// link Connect Manage connected apps - App connected - Connected %1$s + App connected. Amethyst now signs for it in the background. + Connected %1$s. Amethyst now signs for it in the background. Not a valid nostrconnect:// link That link carries no relay to connect on Could not connect: %1$s diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt index 798f847409..309e8f338f 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt @@ -58,6 +58,12 @@ class UriToRouteTest { assertEquals(Route.Hashtag("foo"), uriToRoute("nostr:hashtag?id=foo", account)) } + @Test + fun nostrConnectOfferRoutesToTheSignerScreenCarryingTheUri() { + val offer = "nostrconnect://" + "b".repeat(64) + "?relay=wss%3A%2F%2Frelay.example.com&secret=abc123" + assertEquals(Route.Nip46Signer(connectUri = offer), uriToRoute(offer, account)) + } + @Test fun fragmentHashtagOrNullExtractsTheTag() { assertEquals("NostrMultiplayerGames", fragmentHashtagOrNull("#NostrMultiplayerGames"))