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:
parent
9e6d6f1c39
commit
132b6cae7c
3 changed files with 134 additions and 115 deletions
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ── */}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue