fix: implement NIP-46 nostrconnect:// Amber login properly
- Fix useLoginActions.ts: remove generateNostrConnectParams, generateNostrConnectURI, NostrConnectParams, NostrConnectStatus which don't exist in @nostrify/react@0.2.17/login - Rewrite LoginDialog.tsx Amber tab with proper NIP-46 client-initiated flow: - Generates ephemeral client keypair + random secret - Builds nostrconnect:// URI (no nostrsigner: scheme, no intents) - Opens Amber via window.open(_blank) - standard URI scheme Amber handles natively - Listens on relay pool for Amber's kind 24133 connect response - Decrypts with NIP-04, verifies secret to prevent spoofing - Uses NConnectSigner to call get_public_key - Creates NLogin bunker object and stores it persistently - Bunker tab: direct NLogin.fromBunker() paste flow - Extension tab: NIP-07 browser extension - No clipboard hacks, no nostrsigner: scheme, no Android intent issues Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
916f067238
commit
61795db997
2 changed files with 267 additions and 74 deletions
|
|
@ -7,9 +7,13 @@ import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useLoginActions, generateNostrConnectParams, generateNostrConnectURI, type NostrConnectParams, type NostrConnectStatus } from '@/hooks/useLoginActions';
|
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { NLogin } from '@nostrify/react/login';
|
||||||
|
import { useNostrLogin } from '@nostrify/react/login';
|
||||||
|
import { NConnectSigner, NSecSigner, NRelay1 } from '@nostrify/nostrify';
|
||||||
|
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
interface LoginDialogProps {
|
interface LoginDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -18,11 +22,40 @@ interface LoginDialogProps {
|
||||||
onSignup?: () => void;
|
onSignup?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RELAYS = [
|
// Relays used for NIP-46 nostrconnect:// handshake
|
||||||
|
const NOSTRCONNECT_RELAYS = [
|
||||||
'wss://relay.ditto.pub',
|
'wss://relay.ditto.pub',
|
||||||
'wss://relay.nostr.band',
|
'wss://relay.nostr.band',
|
||||||
|
'wss://nostr.mom',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Build a nostrconnect:// URI per NIP-46 spec */
|
||||||
|
function buildNostrConnectURI(opts: {
|
||||||
|
clientPubkey: string;
|
||||||
|
relays: string[];
|
||||||
|
secret: string;
|
||||||
|
name?: string;
|
||||||
|
url?: string;
|
||||||
|
perms?: string;
|
||||||
|
}): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const r of opts.relays) {
|
||||||
|
params.append('relay', r);
|
||||||
|
}
|
||||||
|
params.set('secret', opts.secret);
|
||||||
|
if (opts.name) params.set('name', opts.name);
|
||||||
|
if (opts.url) params.set('url', opts.url);
|
||||||
|
if (opts.perms) params.set('perms', opts.perms);
|
||||||
|
return `nostrconnect://${opts.clientPubkey}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate a random short secret string */
|
||||||
|
function randomSecret(): string {
|
||||||
|
const arr = new Uint8Array(12);
|
||||||
|
crypto.getRandomValues(arr);
|
||||||
|
return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onSignup }) => {
|
export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onSignup }) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [bunkerUri, setBunkerUri] = useState('');
|
const [bunkerUri, setBunkerUri] = useState('');
|
||||||
|
|
@ -32,72 +65,216 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
const [amberStep, setAmberStep] = useState<'idle' | 'connecting' | 'success'>('idle');
|
const [amberStep, setAmberStep] = useState<'idle' | 'connecting' | 'success'>('idle');
|
||||||
const [amberStatus, setAmberStatus] = useState('');
|
const [amberStatus, setAmberStatus] = useState('');
|
||||||
const [nostrConnectUri, setNostrConnectUri] = useState('');
|
const [nostrConnectUri, setNostrConnectUri] = useState('');
|
||||||
const [nostrConnectParams, setNostrConnectParams] = useState<NostrConnectParams | null>(null);
|
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
const relayRefs = useRef<NRelay1[]>([]);
|
||||||
|
|
||||||
const login = useLoginActions();
|
const login = useLoginActions();
|
||||||
|
const { addLogin, setLogin } = useNostrLogin();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
// Cleanup relay connections
|
||||||
|
const cleanupRelays = useCallback(() => {
|
||||||
|
for (const relay of relayRefs.current) {
|
||||||
|
try { relay.close(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
relayRefs.current = [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Reset state when dialog opens/closes
|
// Reset state when dialog opens/closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
abortRef.current = null;
|
abortRef.current = null;
|
||||||
|
cleanupRelays();
|
||||||
setAmberStep('idle');
|
setAmberStep('idle');
|
||||||
setAmberStatus('');
|
setAmberStatus('');
|
||||||
setNostrConnectUri('');
|
setNostrConnectUri('');
|
||||||
setNostrConnectParams(null);
|
|
||||||
setBunkerUri('');
|
setBunkerUri('');
|
||||||
setBunkerError('');
|
setBunkerError('');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen, cleanupRelays]);
|
||||||
|
|
||||||
// Generate a fresh nostrconnect:// URI and start waiting
|
/**
|
||||||
|
* Amber login via NIP-46 nostrconnect:// flow.
|
||||||
|
*
|
||||||
|
* 1. Generate a client keypair + random secret
|
||||||
|
* 2. Build nostrconnect:// URI
|
||||||
|
* 3. Open Amber (or allow manual copy)
|
||||||
|
* 4. Listen on relays for Amber's connect response (kind 24133)
|
||||||
|
* 5. Decrypt, verify secret, extract remote signer pubkey
|
||||||
|
* 6. Use NConnectSigner to call get_public_key
|
||||||
|
* 7. Store as NLogin bunker type
|
||||||
|
*/
|
||||||
const startAmberConnect = useCallback(async () => {
|
const startAmberConnect = useCallback(async () => {
|
||||||
|
// Cancel any previous attempt
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
|
cleanupRelays();
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
abortRef.current = controller;
|
abortRef.current = controller;
|
||||||
|
|
||||||
const params = generateNostrConnectParams({ relays: RELAYS });
|
// Step 1: Generate ephemeral client keypair
|
||||||
const uri = generateNostrConnectURI(params, {
|
const clientSk = generateSecretKey();
|
||||||
|
const clientNsec = nip19.nsecEncode(clientSk);
|
||||||
|
const clientPubkey = getPublicKey(clientSk);
|
||||||
|
|
||||||
|
// Step 2: Random secret to prevent spoofing
|
||||||
|
const secret = randomSecret();
|
||||||
|
|
||||||
|
// Step 3: Build the nostrconnect URI
|
||||||
|
const uri = buildNostrConnectURI({
|
||||||
|
clientPubkey,
|
||||||
|
relays: NOSTRCONNECT_RELAYS,
|
||||||
|
secret,
|
||||||
name: 'SkateSpots',
|
name: 'SkateSpots',
|
||||||
url: 'https://skate-spots.org',
|
url: 'https://skate-spots.org',
|
||||||
perms: 'sign_event,get_public_key,nip44_encrypt,nip44_decrypt',
|
perms: 'get_public_key,sign_event,nip44_encrypt,nip44_decrypt',
|
||||||
});
|
});
|
||||||
|
|
||||||
setNostrConnectParams(params);
|
|
||||||
setNostrConnectUri(uri);
|
setNostrConnectUri(uri);
|
||||||
setAmberStep('connecting');
|
setAmberStep('connecting');
|
||||||
setAmberStatus('Waiting for Amber to connect...');
|
setAmberStatus('Opening Amber...');
|
||||||
|
|
||||||
// Open Amber with the nostrconnect:// URI
|
// Step 4: Open Amber. window.open with '_blank' goes through the browser
|
||||||
// This works in browser, PWA, and Capacitor — it's a standard URI scheme
|
// intent path on Android, which makes Amber read URL params correctly.
|
||||||
// that Amber registers as an intent filter
|
// For nostrconnect:// Amber always handles this as a standard URI scheme.
|
||||||
window.location.href = uri;
|
try {
|
||||||
|
window.open(uri, '_blank');
|
||||||
|
} catch {
|
||||||
|
// Some environments block window.open; user can copy manually
|
||||||
|
}
|
||||||
|
|
||||||
|
setAmberStatus('Waiting for Amber to approve...');
|
||||||
|
|
||||||
|
// Step 5: Listen on relays for Amber's response
|
||||||
|
// Amber sends a kind 24133 event encrypted with NIP-04 to our client pubkey
|
||||||
|
const clientSigner = new NSecSigner(clientSk);
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const connectPromise = new Promise<{ signerPubkey: string; userPubkey: string }>(
|
||||||
|
async (resolve, reject) => {
|
||||||
|
// Listen on abort signal
|
||||||
|
controller.signal.addEventListener('abort', () => {
|
||||||
|
if (!resolved) reject(new DOMException('Aborted', 'AbortError'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to all relays in parallel and listen for responses
|
||||||
|
const relays = NOSTRCONNECT_RELAYS.map(url => new NRelay1(url));
|
||||||
|
relayRefs.current = relays;
|
||||||
|
|
||||||
|
const since = Math.floor(Date.now() / 1000) - 10;
|
||||||
|
|
||||||
|
for (const relay of relays) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Subscribe to kind 24133 events tagged to our client pubkey
|
||||||
|
for await (const msg of relay.req([{
|
||||||
|
kinds: [24133],
|
||||||
|
'#p': [clientPubkey],
|
||||||
|
since,
|
||||||
|
limit: 0,
|
||||||
|
}], { signal: controller.signal })) {
|
||||||
|
if (msg[0] !== 'EVENT') continue;
|
||||||
|
const event = msg[2];
|
||||||
|
|
||||||
|
if (resolved) break;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Decrypt with NIP-04 (Amber uses NIP-04 for the connect response)
|
||||||
|
const decrypted = await clientSigner.nip04.decrypt(
|
||||||
|
event.pubkey,
|
||||||
|
event.content,
|
||||||
|
);
|
||||||
|
const parsed = JSON.parse(decrypted) as {
|
||||||
|
id: string;
|
||||||
|
result: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify this is a connect response with our secret
|
||||||
|
if (parsed.result !== secret && parsed.result !== 'ack') continue;
|
||||||
|
if (parsed.error) {
|
||||||
|
reject(new Error(parsed.error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a valid connect response!
|
||||||
|
const signerPubkey = event.pubkey;
|
||||||
|
|
||||||
|
// Step 6: Use NConnectSigner to get the user's actual pubkey
|
||||||
|
setAmberStatus('Connected! Getting your pubkey...');
|
||||||
|
|
||||||
|
const relayForSigning = relay;
|
||||||
|
const signer = new NConnectSigner({
|
||||||
|
pubkey: signerPubkey,
|
||||||
|
signer: clientSigner,
|
||||||
|
relay: relayForSigning,
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userPubkey = await signer.getPublicKey();
|
||||||
|
resolved = true;
|
||||||
|
resolve({ signerPubkey, userPubkey });
|
||||||
|
} catch {
|
||||||
|
// Decryption failed or wrong format — not our event, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!resolved && (err as Error)?.name !== 'AbortError') {
|
||||||
|
// Relay connection failed, try others
|
||||||
|
console.warn('[Amber] Relay failed:', relay, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout after 3 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
reject(new Error('Timeout waiting for Amber'));
|
||||||
|
}
|
||||||
|
}, 3 * 60 * 1000);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await login.nostrconnect(
|
const { signerPubkey, userPubkey } = await connectPromise;
|
||||||
params,
|
resolved = true;
|
||||||
controller.signal,
|
|
||||||
(status: NostrConnectStatus) => {
|
// Step 7: Create NLogin bunker object and add it
|
||||||
if (status === 'pending') setAmberStatus('Waiting for Amber...');
|
// NLogin stores: type="bunker", pubkey=userPubkey, data={ bunkerPubkey, clientNsec, relays }
|
||||||
else if (status === 'connected') setAmberStatus('Connected! Getting your pubkey...');
|
const nlogin = new NLogin('bunker', userPubkey, {
|
||||||
else if (status === 'authenticated') setAmberStatus('Authenticated!');
|
bunkerPubkey: signerPubkey,
|
||||||
},
|
clientNsec,
|
||||||
);
|
relays: NOSTRCONNECT_RELAYS,
|
||||||
|
});
|
||||||
|
|
||||||
|
addLogin(nlogin);
|
||||||
|
setLogin(nlogin.id);
|
||||||
|
|
||||||
|
cleanupRelays();
|
||||||
setAmberStep('success');
|
setAmberStep('success');
|
||||||
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) {
|
||||||
|
cleanupRelays();
|
||||||
if ((e as Error)?.name === 'AbortError') return;
|
if ((e as Error)?.name === 'AbortError') return;
|
||||||
setAmberStep('idle');
|
setAmberStep('idle');
|
||||||
setAmberStatus('');
|
setAmberStatus('');
|
||||||
toast({ title: 'Connection failed', description: 'Amber did not connect. Try again.', variant: 'destructive' });
|
const msg = (e as Error)?.message ?? 'Unknown error';
|
||||||
|
toast({
|
||||||
|
title: 'Connection failed',
|
||||||
|
description: msg.includes('Timeout')
|
||||||
|
? 'Amber did not respond. Make sure you approved in Amber, then try again.'
|
||||||
|
: `Amber did not connect: ${msg}`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [login, onLogin, onClose, toast]);
|
}, [addLogin, setLogin, cleanupRelays, onLogin, onClose, toast]);
|
||||||
|
|
||||||
const handleBunkerLogin = async () => {
|
const handleBunkerLogin = async () => {
|
||||||
const uri = bunkerUri.trim();
|
const uri = bunkerUri.trim();
|
||||||
|
|
@ -112,8 +289,10 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
toast({ title: '🛹 Logged in!', description: 'Connected via bunker!' });
|
toast({ title: '🛹 Logged in!', description: 'Connected via bunker!' });
|
||||||
onLogin();
|
onLogin();
|
||||||
onClose();
|
onClose();
|
||||||
} catch {
|
} catch (e: unknown) {
|
||||||
setBunkerError('Connection failed. Check your bunker URI and try again.');
|
setBunkerError(
|
||||||
|
`Connection failed: ${(e as Error)?.message ?? 'Check your bunker URI and try again.'}`
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +310,17 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
|
|
||||||
const copyUri = () => {
|
const copyUri = () => {
|
||||||
navigator.clipboard.writeText(nostrConnectUri).catch(() => {});
|
navigator.clipboard.writeText(nostrConnectUri).catch(() => {});
|
||||||
toast({ title: 'Copied!', description: 'Paste in Amber → Connect apps → Add app' });
|
toast({ title: 'Copied!', description: 'Paste this in Amber → Scan/Paste → Paste connection string' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAmberWithUri = () => {
|
||||||
|
if (nostrConnectUri) {
|
||||||
|
try {
|
||||||
|
window.open(nostrConnectUri, '_blank');
|
||||||
|
} catch {
|
||||||
|
copyUri();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -170,7 +359,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
<div className='flex-1 border-t border-gray-800' />
|
<div className='flex-1 border-t border-gray-800' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login tabs — mobile: Amber + Bunker only */}
|
{/* Login tabs */}
|
||||||
<Tabs defaultValue={isAndroid ? 'amber' : 'bunker'} className="w-full">
|
<Tabs defaultValue={isAndroid ? 'amber' : 'bunker'} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3 bg-black/50 border border-[#ff0040]/30 rounded-lg mb-4 h-auto p-1">
|
<TabsList className="grid w-full grid-cols-3 bg-black/50 border border-[#ff0040]/30 rounded-lg mb-4 h-auto p-1">
|
||||||
<TabsTrigger value="amber" className="flex items-center gap-1 text-xs py-2 px-2">
|
<TabsTrigger value="amber" className="flex items-center gap-1 text-xs py-2 px-2">
|
||||||
|
|
@ -197,7 +386,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
<div>
|
<div>
|
||||||
<p className='text-sm text-white font-bold'>Login with Amber</p>
|
<p className='text-sm text-white font-bold'>Login with Amber</p>
|
||||||
<p className='text-xs text-gray-400 mt-1'>
|
<p className='text-xs text-gray-400 mt-1'>
|
||||||
Tap below — Amber opens, approve once, you're in.
|
Uses NIP-46 — no passwords, no clipboard hacks. Tap below, approve in Amber, done.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -205,11 +394,18 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
onClick={startAmberConnect}
|
onClick={startAmberConnect}
|
||||||
>
|
>
|
||||||
<Smartphone className="w-4 h-4 mr-2" />
|
<Smartphone className="w-4 h-4 mr-2" />
|
||||||
Open Amber
|
Connect with Amber
|
||||||
</Button>
|
</Button>
|
||||||
<p className='text-xs text-gray-500'>
|
<p className='text-xs text-gray-500'>
|
||||||
Need Amber?{' '}
|
Need Amber?{' '}
|
||||||
<a href="https://zapstore.dev/apps/naddr1qvzqqqr7pvpzqateqake4lc2fn77lflzq30jfpk8uhvtccalc66989er8cdmljceqqdkxmmd9enhyet9deshyaphvvejumn0wd68yumfvahx2usx8zmj2" target="_blank" rel="noopener noreferrer" className="text-[#ccff00] hover:underline">Get it on Zapstore</a>
|
<a
|
||||||
|
href="https://zapstore.dev/apps/naddr1qvzqqqr7pvpzqateqake4lc2fn77lflzq30jfpk8uhvtccalc66989er8cdmljceqqdkxmmd9enhyet9deshyaphvvejumn0wd68yumfvahx2usx8zmj2"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#ccff00] hover:underline"
|
||||||
|
>
|
||||||
|
Get it on Zapstore
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -222,29 +418,42 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
<p className='text-xs text-gray-400'>{amberStatus}</p>
|
<p className='text-xs text-gray-400'>{amberStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-xs text-gray-400 bg-black/40 rounded-lg p-3 space-y-1'>
|
<div className='text-xs text-gray-300 bg-black/40 rounded-lg p-3 space-y-1 border border-gray-800'>
|
||||||
<p className='font-bold text-white'>If Amber didn't open automatically:</p>
|
<p className='font-bold text-white mb-2'>In Amber:</p>
|
||||||
<p>1. Open <strong className='text-white'>Amber</strong></p>
|
<p>1. Open <strong className='text-white'>Amber</strong></p>
|
||||||
<p>2. Tap <strong className='text-white'>Connect apps → Paste link</strong></p>
|
<p>2. Tap <strong className='text-[#ccff00]'>Scan / Paste link</strong></p>
|
||||||
<p>3. Paste the connection string below</p>
|
<p>3. Paste the connection string</p>
|
||||||
|
<p>4. Tap <strong className='text-[#ccff00]'>Approve</strong></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{nostrConnectUri && (
|
<div className='flex gap-2'>
|
||||||
<Button
|
{nostrConnectUri && (
|
||||||
variant="outline"
|
<>
|
||||||
className='w-full rounded-full border-[#ccff00]/30 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs h-auto py-2'
|
<Button
|
||||||
onClick={copyUri}
|
variant="outline"
|
||||||
>
|
className='flex-1 rounded-full border-[#ccff00]/30 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs h-auto py-2'
|
||||||
<Copy className='w-3 h-3 mr-2' />
|
onClick={openAmberWithUri}
|
||||||
Copy connection string
|
>
|
||||||
</Button>
|
<Smartphone className='w-3 h-3 mr-1' />
|
||||||
)}
|
Open Amber
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className='flex-1 rounded-full border-gray-700 text-gray-300 hover:bg-gray-800 text-xs h-auto py-2'
|
||||||
|
onClick={copyUri}
|
||||||
|
>
|
||||||
|
<Copy className='w-3 h-3 mr-1' />
|
||||||
|
Copy URI
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className='flex-1 text-xs text-gray-500 hover:text-gray-300 h-auto py-2'
|
className='flex-1 text-xs text-gray-500 hover:text-gray-300 h-auto py-2'
|
||||||
onClick={() => { abortRef.current?.abort(); setAmberStep('idle'); setAmberStatus(''); }}
|
onClick={() => { abortRef.current?.abort(); cleanupRelays(); setAmberStep('idle'); setAmberStatus(''); }}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -274,15 +483,16 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
<div className='p-5 rounded-xl bg-black/50 border-2 border-blue-500/30 space-y-4'>
|
<div className='p-5 rounded-xl bg-black/50 border-2 border-blue-500/30 space-y-4'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<Cloud className='w-10 h-10 mx-auto mb-2 text-blue-400' />
|
<Cloud className='w-10 h-10 mx-auto mb-2 text-blue-400' />
|
||||||
<p className='text-sm text-white font-bold'>Login with Bunker (NIP-46)</p>
|
<p className='text-sm text-white font-bold'>Bunker Login (NIP-46)</p>
|
||||||
<p className='text-xs text-gray-400 mt-1'>
|
<p className='text-xs text-gray-400 mt-1'>
|
||||||
Paste your <code className='text-blue-300'>bunker://</code> connection string from Amber or nsec.app.
|
Paste your <code className='text-blue-300'>bunker://</code> URI from Amber or nsec.app.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-xs text-gray-400 bg-black/40 rounded-lg p-3 space-y-1'>
|
<div className='text-xs text-gray-300 bg-black/40 rounded-lg p-3 space-y-1 border border-gray-800'>
|
||||||
<p className='font-bold text-white'>From Amber:</p>
|
<p className='font-bold text-white mb-1'>How to get a bunker URI:</p>
|
||||||
<p>Settings → NIP-46 Bunker → Create connection → Copy URI</p>
|
<p><strong className='text-blue-300'>Amber:</strong> Settings → NIP-46 Bunker → Create → Copy</p>
|
||||||
|
<p><strong className='text-blue-300'>nsec.app:</strong> Connect app → Copy bunker URI</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
|
|
@ -290,7 +500,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
placeholder='bunker://...'
|
placeholder='bunker://...'
|
||||||
value={bunkerUri}
|
value={bunkerUri}
|
||||||
onChange={e => { setBunkerUri(e.target.value); setBunkerError(''); }}
|
onChange={e => { setBunkerUri(e.target.value); setBunkerError(''); }}
|
||||||
className='bg-black/60 border-blue-500/30 text-white placeholder:text-gray-600 text-xs'
|
className='bg-black/60 border-blue-500/30 text-white placeholder:text-gray-600 text-xs font-mono'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
autoCorrect='off'
|
autoCorrect='off'
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|
@ -308,7 +518,7 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className='text-center pt-1'>
|
<div className='text-center pt-1'>
|
||||||
<p className='text-xs text-gray-500 mb-2'>Or get a bunker from:</p>
|
<p className='text-xs text-gray-500 mb-2'>Or try:</p>
|
||||||
<div className='flex gap-2 justify-center flex-wrap'>
|
<div className='flex gap-2 justify-center flex-wrap'>
|
||||||
<a href="https://nsec.app" target="_blank" rel="noopener noreferrer" className="text-xs px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full hover:bg-blue-500/30">nsec.app →</a>
|
<a href="https://nsec.app" target="_blank" rel="noopener noreferrer" className="text-xs px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full hover:bg-blue-500/30">nsec.app →</a>
|
||||||
<a href="https://nostrum.social" target="_blank" rel="noopener noreferrer" className="text-xs px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full hover:bg-blue-500/30">Nostrum →</a>
|
<a href="https://nostrum.social" target="_blank" rel="noopener noreferrer" className="text-xs px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full hover:bg-blue-500/30">Nostrum →</a>
|
||||||
|
|
@ -322,8 +532,8 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
<div className='text-center p-5 rounded-xl bg-black/50 border-2 border-[#ff0040]/30 space-y-4'>
|
<div className='text-center p-5 rounded-xl bg-black/50 border-2 border-[#ff0040]/30 space-y-4'>
|
||||||
<Shield className='w-10 h-10 mx-auto text-[#ff0040]' />
|
<Shield className='w-10 h-10 mx-auto text-[#ff0040]' />
|
||||||
<div>
|
<div>
|
||||||
<p className='text-sm text-white font-bold'>Browser Extension</p>
|
<p className='text-sm text-white font-bold'>Browser Extension (NIP-07)</p>
|
||||||
<p className='text-xs text-gray-400 mt-1'>Best for desktop. Works with Alby, nos2x, and similar NIP-07 extensions.</p>
|
<p className='text-xs text-gray-400 mt-1'>Best for desktop. Works with Alby, nos2x, Flamingo, and other NIP-07 extensions.</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className='w-full rounded-full py-3 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white font-bold uppercase'
|
className='w-full rounded-full py-3 bg-[#ff0040] hover:bg-[#ff0040]/90 text-white font-bold uppercase'
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,11 @@ import { useNostr } from '@nostrify/react';
|
||||||
import {
|
import {
|
||||||
NLogin,
|
NLogin,
|
||||||
type NLoginType,
|
type NLoginType,
|
||||||
type NostrConnectParams,
|
|
||||||
type NostrConnectStatus,
|
|
||||||
useNostrLogin,
|
useNostrLogin,
|
||||||
generateNostrConnectParams,
|
|
||||||
generateNostrConnectURI,
|
|
||||||
} from '@nostrify/react/login';
|
} from '@nostrify/react/login';
|
||||||
|
|
||||||
// NOTE: This file should not be edited except for adding new login methods.
|
// NOTE: This file should not be edited except for adding new login methods.
|
||||||
|
|
||||||
export type { NostrConnectParams, NostrConnectStatus };
|
|
||||||
export { generateNostrConnectParams, generateNostrConnectURI };
|
|
||||||
|
|
||||||
export function useLoginActions() {
|
export function useLoginActions() {
|
||||||
const { nostr } = useNostr();
|
const { nostr } = useNostr();
|
||||||
const { logins, addLogin, setLogin, removeLogin } = useNostrLogin();
|
const { logins, addLogin, setLogin, removeLogin } = useNostrLogin();
|
||||||
|
|
@ -39,16 +32,6 @@ export function useLoginActions() {
|
||||||
const login = await NLogin.fromExtension();
|
const login = await NLogin.fromExtension();
|
||||||
addAndActivate(login);
|
addAndActivate(login);
|
||||||
},
|
},
|
||||||
// Login via nostrconnect:// (client-initiated NIP-46)
|
|
||||||
// Generates a URI, opens Amber, waits for Amber to connect back via relay.
|
|
||||||
async nostrconnect(
|
|
||||||
params: NostrConnectParams,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
onStatus?: (status: NostrConnectStatus) => void,
|
|
||||||
): Promise<void> {
|
|
||||||
const login = await NLogin.fromNostrConnect(params, nostr, { signal, onStatus });
|
|
||||||
addAndActivate(login);
|
|
||||||
},
|
|
||||||
// Log out the current user
|
// Log out the current user
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
const login = logins[0];
|
const login = logins[0];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue