fix: Amber login using confirmed correct URL from Amber source code

Read Amber's actual IntentUtils.kt to confirm exact URL parsing:
- Strips "nostrsigner:" prefix
- URLDecodes the remainder
- Parses ?key=value&key=value query params
- "type" param controls operation
- No callbackUrl → copies result to clipboard

Correct URL: nostrsigner:?type=get_public_key&compressionType=none&returnType=signature

Flow:
1. Tap "Open Amber" → window.location.href = nostrsigner: URL
2. Amber opens, user approves, copies hex pubkey to clipboard
3. App regains focus → tries navigator.clipboard.readText() auto
4. If clipboard denied → shows paste input (accepts hex or npub1)
5. Login complete

Removed broken Capacitor plugin dependency (couldn't install git dep).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-08 12:22:58 +07:00
parent 9e6d6f1c39
commit 132b6cae7c
3 changed files with 134 additions and 115 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "skate-spots", "name": "skate-spots",
"private": true, "private": true,
"version": "1.0.33", "version": "1.0.34",
"type": "module", "type": "module",
"homepage": "https://skate-spots.com", "homepage": "https://skate-spots.com",
"scripts": { "scripts": {

View file

@ -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) - Read Amber's actual IntentUtils.kt source to confirm exact URL format
- Fixed API calls to match Coracle fork: setPackageName(string) and getPublicKey(packageName) - Correct: nostrsigner:?type=get_public_key&compressionType=none&returnType=signature
- Requires: npm install && npx cap sync android after git pull - 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

View file

@ -35,99 +35,89 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
const isAndroid = /Android/i.test(navigator.userAgent); const isAndroid = /Android/i.test(navigator.userAgent);
const AMBER_PACKAGE = 'com.greenart7c3.nostrsigner'; // Step 1: open Amber with no callbackUrl → Amber copies pubkey to clipboard
// This is confirmed correct from Amber's IntentUtils.kt source:
const handleAmberLogin = async () => { // - Strips "nostrsigner:" prefix
// - URLDecodes the rest
// - Parses ?type=get_public_key&... query params
// - No callbackUrl → copies result to clipboard
const handleAmberLogin = () => {
setAmberStep('loading'); 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 { 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<void>
// 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); login.amber(pubkey);
setAmberStep('done'); setAmberStep('done');
toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' });
setTimeout(() => { onLogin(); onClose(); }, 800); setTimeout(() => { onLogin(); onClose(); }, 800);
} catch (e: unknown) { } catch (e: unknown) {
const msg = (e as Error)?.message ?? 'Could not connect to Amber'; setAmberError((e as Error)?.message ?? 'Login failed');
console.error('[Amber] login failed:', e);
setAmberError(msg);
setAmberStep('error'); setAmberStep('error');
} }
}; };
// Step 2: user comes back — try clipboard automatically, else show paste input const [pubkeyInput, setPubkeyInput] = useState('');
useEffect(() => { const [pubkeyError, setPubkeyError] = useState('');
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 completeLogin = (pubkey: string) => { const handlePubkeySubmit = async () => {
try { const val = pubkeyInput.trim();
login.amber(pubkey); // Accept hex pubkey or npub
setAmberStep('done'); if (/^[0-9a-f]{64}$/i.test(val)) {
toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); completeAmberLogin(val.toLowerCase());
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');
return; 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 // Reset when dialog closes
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setAmberStep('idle'); setAmberStep('idle');
setAmberError(''); setAmberError('');
setPubkeyInput('');
setPubkeyError('');
setBunkerUri(''); setBunkerUri('');
setBunkerError(''); setBunkerError('');
setNsecInput(''); setNsecInput('');
@ -216,46 +206,73 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
{/* ── AMBER TAB ── */} {/* ── AMBER TAB ── */}
<TabsContent value='amber' className='space-y-3'> <TabsContent value='amber' className='space-y-3'>
<div className='text-center p-5 rounded-xl bg-black/50 border-2 border-[#ccff00]/30 space-y-4'>
{amberStep === 'done' && ( {/* Success */}
<> {amberStep === 'done' && (
<Check className='w-10 h-10 mx-auto text-green-500' /> <div className='text-center p-5 rounded-xl bg-black/50 border-2 border-green-500/30 space-y-3'>
<p className='text-sm text-green-400 font-bold'>Logged in!</p> <Check className='w-10 h-10 mx-auto text-green-500' />
<Loader2 className="w-4 h-4 mx-auto animate-spin text-green-500" /> <p className='text-sm text-green-400 font-bold'>Logged in!</p>
</> <Loader2 className="w-4 h-4 mx-auto animate-spin text-green-500" />
)} </div>
)}
{/* Idle / loading — open Amber button */}
{(amberStep === 'idle' || amberStep === 'loading') && (
<div className='text-center p-5 rounded-xl bg-black/50 border-2 border-[#ccff00]/30 space-y-4'>
<div className="w-14 h-14 rounded-full bg-[#ccff00]/10 border-2 border-[#ccff00]/40 flex items-center justify-center mx-auto">
{amberStep === 'loading' ? <Loader2 className='w-7 h-7 text-[#ccff00] animate-spin' /> : <Smartphone className='w-7 h-7 text-[#ccff00]' />}
</div>
<div>
<p className='text-sm text-white font-bold'>Login with Amber</p>
<p className='text-xs text-gray-400 mt-1'>Opens Amber → approve → come back here.</p>
</div>
<Button
className='w-full rounded-full py-3 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase'
onClick={handleAmberLogin}
disabled={amberStep === 'loading'}
>
<Smartphone className="w-4 h-4 mr-2" />Open Amber
</Button>
</div>
)}
{/* Paste pubkey — after returning from Amber */}
{(amberStep === 'paste' || amberStep === 'error') && (
<div className='p-5 rounded-xl bg-black/50 border-2 border-[#ccff00]/30 space-y-4'>
<div className='text-center space-y-1'>
<Smartphone className='w-8 h-8 mx-auto text-[#ccff00]' />
<p className='text-sm text-white font-bold'>Paste your pubkey</p>
<p className='text-xs text-gray-400'>
In Amber: tap your profile → <strong className='text-white'>Copy public key</strong>. Then paste below.
</p>
</div>
{amberStep === 'error' && amberError && (
<p className='text-xs text-red-400 bg-red-500/10 rounded-lg p-2 text-center'>{amberError}</p>
)}
<div className='space-y-2'>
<Input
placeholder='npub1... or hex pubkey'
value={pubkeyInput}
onChange={e => { 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 && <p className="text-xs text-red-400">{pubkeyError}</p>}
</div>
<Button
className='w-full rounded-full py-3 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase'
disabled={!pubkeyInput.trim()}
onClick={handlePubkeySubmit}
>
<Check className='w-4 h-4 mr-2' />Connect
</Button>
<Button variant='ghost' className='w-full text-xs text-gray-600 h-auto py-1'
onClick={() => { setAmberStep('idle'); setPubkeyInput(''); setPubkeyError(''); setAmberError(''); }}>
← Back
</Button>
</div>
)}
{amberStep !== 'done' && (
<>
<div className="w-14 h-14 rounded-full bg-[#ccff00]/10 border-2 border-[#ccff00]/40 flex items-center justify-center mx-auto">
{amberStep === 'loading'
? <Loader2 className='w-7 h-7 text-[#ccff00] animate-spin' />
: <Smartphone className='w-7 h-7 text-[#ccff00]' />
}
</div>
<div>
<p className='text-sm text-white font-bold'>Login with Amber</p>
<p className='text-xs text-gray-400 mt-1'>
{amberStep === 'loading' ? 'Opening Amber — approve the request...' : 'NIP-55 Android signer. One tap, done.'}
</p>
</div>
{amberStep === 'error' && (
<p className='text-xs text-red-400 bg-red-500/10 rounded-lg p-2'>{amberError}</p>
)}
<Button
className='w-full rounded-full py-3 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold uppercase'
onClick={handleAmberLogin}
disabled={amberStep === 'loading'}
>
{amberStep === 'loading'
? <><Loader2 className="w-4 h-4 mr-2 animate-spin" />Waiting...</>
: <><Smartphone className="w-4 h-4 mr-2" />Login with Amber</>
}
</Button>
</>
)}
</div>
</TabsContent> </TabsContent>
{/* ── BUNKER TAB ── */} {/* ── BUNKER TAB ── */}