From f010a616084e7dd47b4ed2418c97d31ad953bb31 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 8 Jul 2026 11:59:04 +0700 Subject: [PATCH] fix: use nostr-signer-capacitor-plugin for Amber NIP-55 login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All previous URL-scheme approaches (nostrsigner:, intent:) were fundamentally wrong for a Capacitor app. JavaScript in a WebView cannot reliably dispatch Android intents via URL navigation. The correct solution — used by Coracle and other Capacitor-based Nostr apps — is the nostr-signer-capacitor-plugin which communicates with Amber via native Android intents through the Capacitor bridge: NostrSignerPlugin.setPackageName({ packageName: 'com.greenart7c3.nostrsigner' }) NostrSignerPlugin.isExternalSignerInstalled() NostrSignerPlugin.getPublicKey() → { npub } This is synchronous from the JS side, handles the entire Android intent roundtrip natively, and returns the pubkey directly. No URL encoding, no callback URLs, no redirects, no malformed request errors. Co-authored-by: shakespeare.diy --- package-lock.json | 7 ++ package.json | 3 +- release-notes.txt | 9 +- src/components/auth/LoginDialog.tsx | 167 +++++++++++++--------------- 4 files changed, 92 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 249cbcb..ec21980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.462.0", "ngeohash": "^0.6.3", + "nostr-signer-capacitor-plugin": "^0.0.5", "nostr-tools": "^2.13.0", "npubcash-sdk": "^0.2.0", "qrcode": "^1.5.4", @@ -6554,6 +6555,12 @@ "node": ">=0.10.0" } }, + "node_modules/nostr-signer-capacitor-plugin": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/nostr-signer-capacitor-plugin/-/nostr-signer-capacitor-plugin-0.0.5.tgz", + "integrity": "sha512-/EvqWz71HZ5cWmzvfXWTm48AWZtbeZDbOg3vLwXyXPjnIp1DR7Wurww/Mo41ORNu1DNPlqH20l7kIXKO6vR5og==", + "license": "MIT" + }, "node_modules/nostr-tools": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.0.tgz", diff --git a/package.json b/package.json index 877ea2a..1667cb3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.0.31", + "version": "1.0.32", "type": "module", "homepage": "https://skate-spots.com", "scripts": { @@ -83,6 +83,7 @@ "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.462.0", "ngeohash": "^0.6.3", + "nostr-signer-capacitor-plugin": "^0.0.5", "nostr-tools": "^2.13.0", "npubcash-sdk": "^0.2.0", "qrcode": "^1.5.4", diff --git a/release-notes.txt b/release-notes.txt index 2f11c1d..7afb4f4 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,5 +1,6 @@ -v1.0.31 — Amber login redesigned for GrapheneOS +v1.0.32 — Amber login via native Capacitor plugin -- GrapheneOS Vanadium blocks intent: URLs from web pages (security hardening) -- New flow: tap Open Amber → approve in Amber → app tries clipboard auto-read → if denied, shows paste input for hex pubkey -- Works on all Android browsers including GrapheneOS Vanadium +- Replaced all URL-scheme hacks with nostr-signer-capacitor-plugin +- Plugin uses native Android intents directly — no nostrsigner: URL, no intent: URL, no encoding issues +- This is how Coracle and other proper Capacitor apps do it +- Requires: npm run android:build && cap sync android (plugin needs native sync) diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 5795153..4f43434 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -27,21 +27,51 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog const [nsecError, setNsecError] = useState(''); // Amber NIP-55 state - const [amberStep, setAmberStep] = useState<'idle' | 'waiting' | 'paste' | 'done'>('idle'); - const [pubkeyInput, setPubkeyInput] = useState(''); - const [pubkeyError, setPubkeyError] = useState(''); + const [amberStep, setAmberStep] = useState<'idle' | 'loading' | 'done' | 'error'>('idle'); + const [amberError, setAmberError] = useState(''); const login = useLoginActions(); const { toast } = useToast(); const isAndroid = /Android/i.test(navigator.userAgent); - // Step 1: open Amber asking it to copy pubkey to clipboard (no callback URL) - const handleAmberLogin = () => { - // nostrsigner: with no callbackUrl → Amber copies pubkey to clipboard - // This works on ALL Android browsers including GrapheneOS Vanadium - window.location.href = 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end'; - setAmberStep('waiting'); + const handleAmberLogin = async () => { + setAmberStep('loading'); + setAmberError(''); + try { + // Use the Capacitor plugin for native intent-based NIP-55 communication. + // This is the correct approach for Capacitor APKs — it uses Android intents + // directly, not URL schemes from JavaScript (which don't work reliably). + const { default: NostrSignerPlugin } = await import('nostr-signer-capacitor-plugin'); + + // Set Amber's package name + await NostrSignerPlugin.setPackageName({ packageName: 'com.greenart7c3.nostrsigner' }); + + // Check Amber is installed + const { installed } = await NostrSignerPlugin.isExternalSignerInstalled(); + if (!installed) { + throw new Error('Amber is not installed. Install it from Zapstore or F-Droid.'); + } + + // Request pubkey — opens Amber, user approves, returns npub + const { npub } = await NostrSignerPlugin.getPublicKey(); + + // Convert npub to hex pubkey + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(npub); + if (decoded.type !== 'npub') throw new Error('Invalid pubkey from Amber'); + const pubkey = decoded.data as string; + + login.amber(pubkey); + setAmberStep('done'); + toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); + setTimeout(() => { onLogin(); onClose(); }, 800); + } catch (e: unknown) { + const msg = (e as Error)?.message ?? 'Could not connect to Amber'; + console.error('[Amber] login failed:', e); + setAmberError(msg); + setAmberStep('error'); + } }; // Step 2: user comes back — try clipboard automatically, else show paste input @@ -91,8 +121,7 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog useEffect(() => { if (!isOpen) { setAmberStep('idle'); - setPubkeyInput(''); - setPubkeyError(''); + setAmberError(''); setBunkerUri(''); setBunkerError(''); setNsecInput(''); @@ -181,86 +210,46 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog {/* ── AMBER TAB ── */} +
- {/* Done */} - {amberStep === 'done' && ( -
- -

Logged in!

- -
- )} - - {/* Step 1 idle — show login button */} - {amberStep === 'idle' && ( -
-
- -
-
-

Login with Amber

-

Opens Amber → approve → come back here.

-
- -
- )} - - {/* Step 2 waiting — app just navigated to Amber */} - {amberStep === 'waiting' && ( -
- -
-

Approve in Amber

-

Tap "Get public key" in Amber, then come back here.

-
- - -
- )} - - {/* Step 3 paste — clipboard read failed or user chose manual */} - {amberStep === 'paste' && ( -
-
- -

Paste your pubkey

-

- In Amber: tap your profile → copy public key (hex). Paste it below. -

-
-
- { setPubkeyInput(e.target.value); setPubkeyError(''); }} - className='bg-black/60 border-[#ccff00]/30 text-white placeholder:text-gray-600 text-xs font-mono' - autoComplete='off' - autoCorrect='off' - spellCheck={false} - /> - {pubkeyError &&

{pubkeyError}

} -
- - -
- )} + {amberStep === 'done' && ( + <> + +

Logged in!

+ + + )} + {amberStep !== 'done' && ( + <> +
+ {amberStep === 'loading' + ? + : + } +
+
+

Login with Amber

+

+ {amberStep === 'loading' ? 'Opening Amber — approve the request...' : 'NIP-55 Android signer. One tap, done.'} +

+
+ {amberStep === 'error' && ( +

{amberError}

+ )} + + + )} +
{/* ── BUNKER TAB ── */}