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 ── */}