diff --git a/.claude/settings.json b/.claude/settings.json
index c0238b52b5..e7a86ad229 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,4 +1,8 @@
{
+ "env": {
+ "LANG": "C.UTF-8",
+ "LC_ALL": "C.UTF-8"
+ },
"hooks": {
"PreToolUse": [
{
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanOutcomeSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanOutcomeSheet.kt
index 1352a805b6..e6e06d6ea2 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanOutcomeSheet.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanOutcomeSheet.kt
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.qr_scanner_copy
+import com.vitorpamplona.amethyst.commons.resources.qr_scanner_kind_bunker
import com.vitorpamplona.amethyst.commons.resources.qr_scanner_kind_cashu
import com.vitorpamplona.amethyst.commons.resources.qr_scanner_kind_lightning
import com.vitorpamplona.amethyst.commons.resources.qr_scanner_kind_nostr_unsupported
@@ -137,7 +138,11 @@ private fun explain(payload: ScannedPayload): String =
}
is ScannedPayload.WalletConnect -> stringRes(Res.string.qr_scanner_kind_wallet)
- is ScannedPayload.Bunker, is ScannedPayload.NostrConnect -> stringRes(Res.string.qr_scanner_kind_signer)
+ // Split deliberately: Amethyst PUBLISHES bunker:// addresses (it is the signer) and has no
+ // screen that consumes one, so pointing the user at the signer screen -- as this used to --
+ // sends them somewhere that cannot accept it.
+ is ScannedPayload.Bunker -> stringRes(Res.string.qr_scanner_kind_bunker)
+ is ScannedPayload.NostrConnect -> stringRes(Res.string.qr_scanner_kind_signer)
is ScannedPayload.Lightning -> stringRes(Res.string.qr_scanner_kind_lightning)
is ScannedPayload.Cashu -> stringRes(Res.string.qr_scanner_kind_cashu)
is ScannedPayload.Web -> stringRes(Res.string.qr_scanner_kind_web)
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 309e8f338f..bef8fe833e 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,50 @@ class UriToRouteTest {
assertEquals(Route.Hashtag("foo"), uriToRoute("nostr:hashtag?id=foo", account))
}
+ // The QR scanner shows a "can't open this" sheet for anything uriToRoute returns null for, so
+ // what does and does not route here decides what that sheet ever has to explain.
+
+ @Test
+ fun rawWalletConnectUrisRouteWithoutTheDlnwcWrapper() {
+ // Not just the `dlnwc?value=` deep-link form: a wallet's QR code holds the bare URI, and
+ // the fallback at the end of uriToRoute is what catches it.
+ assertEquals(
+ Route.WalletAddNwc(NWC_URI),
+ uriToRoute(NWC_URI, account),
+ )
+ }
+
+ @Test
+ fun everyWalletConnectSchemeSpellingRoutes() {
+ assertEquals(
+ Route.WalletAddNwc(NWC_URI_NO_PLUS),
+ uriToRoute(NWC_URI_NO_PLUS, account),
+ )
+ }
+
+ @Test
+ fun walletConnectDeepLinksStillUnwrapTheValueParameter() {
+ assertEquals(
+ Route.WalletAddNwc(NWC_URI),
+ uriToRoute("dlnwc?value=$NWC_URI", account),
+ )
+ }
+
+ @Test
+ fun bunkerUrisDoNotRouteAnywhere() {
+ // Amethyst *publishes* bunker:// addresses (it is the remote signer); it has no screen that
+ // consumes one. The NIP-46 signer screen pairs nostrconnect:// offers only. Until that
+ // changes, a scanned bunker:// belongs in the scanner's explanation sheet, and the sheet
+ // must not tell the user to take it somewhere that cannot accept it.
+ assertNull(uriToRoute("bunker://$PUBKEY_HEX?relay=wss%3A%2F%2Frelay.example&secret=abc", account))
+ }
+
+ companion object {
+ private const val PUBKEY_HEX = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
+ private const val NWC_URI = "nostr+walletconnect://$PUBKEY_HEX?relay=wss%3A%2F%2Frelay.example&secret=$PUBKEY_HEX"
+ private const val NWC_URI_NO_PLUS = "nostrwalletconnect://$PUBKEY_HEX?relay=wss%3A%2F%2Frelay.example&secret=$PUBKEY_HEX"
+ }
+
@Test
fun nostrConnectOfferRoutesToTheSignerScreenCarryingTheUri() {
val offer = "nostrconnect://" + "b".repeat(64) + "?relay=wss%3A%2F%2Frelay.example.com&secret=abc123"
diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml
index 259a1a8869..4b80850cb4 100644
--- a/commonsUI/src/commonMain/composeResources/values/strings.xml
+++ b/commonsUI/src/commonMain/composeResources/values/strings.xml
@@ -218,7 +218,8 @@
This code carries a private key or a pairing secret, so its contents are not shown here. Use the screen that expects it.
This is a private key. Paste it on the login screen instead.
This is a wallet connection. Add it from Wallet settings.
- This is a remote signer. Add it from the NIP-46 signer screen.
+ This is a remote-signer address. Amethyst hands these out so other apps can ask it to sign — it cannot sign in with one.
+ This is an app asking to connect to your signer. Open it from the NIP-46 signer screen.
This is a Lightning invoice.
This is a Cashu token.
This is a web link.