diff --git a/package.json b/package.json index 5b31af7..5ebd754 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.0.33", + "version": "1.0.34", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/release-notes.txt b/release-notes.txt index b520d5a..c863ba5 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,5 +1,7 @@ -v1.0.33 — Amber login via Coracle's Capacitor plugin (correct fork + correct API) +v1.0.34 — Amber login: correct URL from source + clipboard/paste flow -- Switched to coracle-social/nostr-signer-capacitor-plugin (same fork Coracle uses in production) -- Fixed API calls to match Coracle fork: setPackageName(string) and getPublicKey(packageName) -- Requires: npm install && npx cap sync android after git pull +- Read Amber's actual IntentUtils.kt source to confirm exact URL format +- Correct: nostrsigner:?type=get_public_key&compressionType=none&returnType=signature +- No callbackUrl = Amber copies pubkey to clipboard (reliable on all Android) +- On return: auto-read clipboard (hex or npub), fallback to paste input +- Accepts both hex pubkey and npub1 format from Amber diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index dee5cfd..a785a9f 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -35,99 +35,89 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog const isAndroid = /Android/i.test(navigator.userAgent); - const AMBER_PACKAGE = 'com.greenart7c3.nostrsigner'; - - const handleAmberLogin = async () => { + // Step 1: open Amber with no callbackUrl → Amber copies pubkey to clipboard + // This is confirmed correct from Amber's IntentUtils.kt source: + // - Strips "nostrsigner:" prefix + // - URLDecodes the rest + // - Parses ?type=get_public_key&... query params + // - No callbackUrl → copies result to clipboard + const handleAmberLogin = () => { setAmberStep('loading'); - setAmberError(''); + // Exact format from Amber source: nostrsigner:?type=X&compressionType=none&returnType=signature + // No callbackUrl = Amber copies pubkey to clipboard + window.location.href = 'nostrsigner:?type=get_public_key&compressionType=none&returnType=signature'; + // After navigation, app will lose focus. When user comes back, visibilitychange fires. + setTimeout(() => setAmberStep('paste'), 500); + }; + + // Step 2: when app regains focus, try clipboard auto-read first + useEffect(() => { + if (amberStep !== 'paste') return; + const tryClipboard = async () => { + try { + const text = (await navigator.clipboard.readText()).trim(); + // Amber returns hex pubkey OR npub — handle both + if (/^[0-9a-f]{64}$/i.test(text)) { + completeAmberLogin(text.toLowerCase()); + return; + } + if (text.startsWith('npub1')) { + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(text); + if (decoded.type === 'npub') { + completeAmberLogin(decoded.data as string); + return; + } + } + } catch { /* clipboard denied — user will paste manually */ } + }; + tryClipboard(); + }, [amberStep]); + + const completeAmberLogin = (pubkey: string) => { try { - // Use the Capacitor plugin for native Android intent-based NIP-55. - // We use Coracle's fork API (github:coracle-social/nostr-signer-capacitor-plugin) - // which is the battle-tested version used in production by Coracle. - // - // API (Coracle fork — different from chebizarro's npm version): - // setPackageName(packageName: string): Promise - // getPublicKey(packageName?: string): Promise<{ npub: string; package: string }> - const { NostrSignerPlugin } = await import('nostr-signer-capacitor-plugin'); - - // Set package name first (Coracle fork takes string, not object) - await NostrSignerPlugin.setPackageName(AMBER_PACKAGE); - - // Check Amber is installed - const { installed } = await NostrSignerPlugin.isExternalSignerInstalled(AMBER_PACKAGE); - if (!installed) { - throw new Error('Amber is not installed. Get it from Zapstore or F-Droid.'); - } - - // Request pubkey — opens Amber natively, user approves, promise resolves - const { npub } = await NostrSignerPlugin.getPublicKey(AMBER_PACKAGE); - - // Convert npub → 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); + setAmberError((e as Error)?.message ?? 'Login failed'); setAmberStep('error'); } }; - // Step 2: user comes back — try clipboard automatically, else show paste input - useEffect(() => { - if (amberStep !== 'waiting') return; - const handleVisible = async () => { - if (document.visibilityState !== 'visible') return; - // Try reading clipboard automatically - try { - const text = await navigator.clipboard.readText(); - const clean = text.trim().toLowerCase(); - if (/^[0-9a-f]{64}$/.test(clean)) { - completeLogin(clean); - return; - } - } catch { /* clipboard permission denied — fall through to paste UI */ } - // Show paste input - setAmberStep('paste'); - }; - document.addEventListener('visibilitychange', handleVisible); - return () => document.removeEventListener('visibilitychange', handleVisible); - }, [amberStep]); + const [pubkeyInput, setPubkeyInput] = useState(''); + const [pubkeyError, setPubkeyError] = useState(''); - const completeLogin = (pubkey: string) => { - try { - login.amber(pubkey); - setAmberStep('done'); - toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); - setTimeout(() => { onLogin(); onClose(); }, 800); - } catch (e) { - console.error('[Amber] login.amber() failed:', e); - setAmberStep('idle'); - toast({ title: 'Login failed', description: 'Could not log in with Amber. Try again.', variant: 'destructive' }); - } - }; - - const handlePubkeySubmit = () => { - const clean = pubkeyInput.trim().toLowerCase(); - if (!/^[0-9a-f]{64}$/.test(clean)) { - setPubkeyError('Must be a 64-character hex pubkey'); + const handlePubkeySubmit = async () => { + const val = pubkeyInput.trim(); + // Accept hex pubkey or npub + if (/^[0-9a-f]{64}$/i.test(val)) { + completeAmberLogin(val.toLowerCase()); return; } - completeLogin(clean); + if (val.startsWith('npub1')) { + try { + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(val); + if (decoded.type === 'npub') { + completeAmberLogin(decoded.data as string); + return; + } + } catch { /* fall through */ } + } + setPubkeyError('Paste your hex pubkey (64 chars) or npub from Amber'); }; + + // Reset when dialog closes useEffect(() => { if (!isOpen) { setAmberStep('idle'); setAmberError(''); + setPubkeyInput(''); + setPubkeyError(''); setBunkerUri(''); setBunkerError(''); setNsecInput(''); @@ -216,46 +206,73 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog {/* ── AMBER TAB ── */} -
- {amberStep === 'done' && ( - <> - -

Logged in!

- - - )} + {/* Success */} + {amberStep === 'done' && ( +
+ +

Logged in!

+ +
+ )} + + {/* Idle / loading — open Amber button */} + {(amberStep === 'idle' || amberStep === 'loading') && ( +
+
+ {amberStep === 'loading' ? : } +
+
+

Login with Amber

+

Opens Amber → approve → come back here.

+
+ +
+ )} + + {/* Paste pubkey — after returning from Amber */} + {(amberStep === 'paste' || amberStep === 'error') && ( +
+
+ +

Paste your pubkey

+

+ In Amber: tap your profile → Copy public key. Then paste below. +

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

{amberError}

+ )} +
+ { 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' && ( - <> -
- {amberStep === 'loading' - ? - : - } -
-
-

Login with Amber

-

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

-
- {amberStep === 'error' && ( -

{amberError}

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