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.