import { useCallback } from 'react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { SKATE_NIP05_DOMAIN, buildSkateNip05, normalizeSkateNip05Name, skateApiBase, } from '@/lib/skateNip05'; export interface SkateNip05Availability { available: boolean; reason: string | null; } export type SkateNip05ClaimResult = | { ok: true; nip05: string } | { ok: false; error: string }; /** UTF-8 safe base64 (the NIP-98 event travels as base64 JSON in a header). */ function b64(value: string): string { const bytes = new TextEncoder().encode(value); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary); } /** * Claim a free `name@skate-spots.org` handle. * * The claim is proven with a NIP-98 (kind 27235) event signed by the account * that will own the name, so only the owner can claim a name for themselves. * Nothing here blocks the app: callers treat a failure as "no handle yet". */ export function useSkateNip05() { const { user } = useCurrentUser(); const checkAvailability = useCallback( async (name: string, signal?: AbortSignal): Promise => { const clean = normalizeSkateNip05Name(name); if (!clean) return { available: false, reason: null }; try { const res = await fetch( `${skateApiBase()}/api/nip05/check?name=${encodeURIComponent(clean)}`, { signal }, ); if (!res.ok) return { available: false, reason: 'Could not check right now.' }; const data = (await res.json()) as { available?: boolean; reason?: string | null }; return { available: !!data.available, reason: data.reason ?? null }; } catch { return { available: false, reason: 'Could not check right now.' }; } }, [], ); const claim = useCallback( async (name: string): Promise => { const clean = normalizeSkateNip05Name(name); if (!user) return { ok: false, error: 'Log in first.' }; if (!clean) return { ok: false, error: 'Enter a name.' }; try { const auth = await user.signer.signEvent({ kind: 27235, content: '', created_at: Math.floor(Date.now() / 1000), tags: [ ['u', `https://${SKATE_NIP05_DOMAIN}/api/nip05/claim`], ['method', 'POST'], ['action', 'claim'], ['name', clean], ], }); const res = await fetch(`${skateApiBase()}/api/nip05/claim`, { method: 'POST', headers: { 'x-nip05-auth': b64(JSON.stringify(auth)) }, }); const data = (await res.json().catch(() => ({}))) as { ok?: boolean; nip05?: string; error?: string }; if (res.ok && data.ok) return { ok: true, nip05: data.nip05 ?? buildSkateNip05(clean) }; return { ok: false, error: data.error || 'Could not claim that name.' }; } catch { return { ok: false, error: 'Could not reach the name service.' }; } }, [user], ); /** Hand a name back (owner, or a platform admin). Rarely used from the UI. */ const release = useCallback( async (name: string): Promise<{ ok: true } | { ok: false; error: string }> => { const clean = normalizeSkateNip05Name(name); if (!user) return { ok: false, error: 'Log in first.' }; try { const auth = await user.signer.signEvent({ kind: 27235, content: '', created_at: Math.floor(Date.now() / 1000), tags: [ ['u', `https://${SKATE_NIP05_DOMAIN}/api/nip05/release`], ['method', 'POST'], ['action', 'release'], ['name', clean], ], }); const res = await fetch(`${skateApiBase()}/api/nip05/release`, { method: 'POST', headers: { 'x-nip05-auth': b64(JSON.stringify(auth)) }, }); const data = (await res.json().catch(() => ({}))) as { ok?: boolean; error?: string }; if (res.ok && data.ok) return { ok: true }; return { ok: false, error: data.error || 'Could not release that name.' }; } catch { return { ok: false, error: 'Could not reach the name service.' }; } }, [user], ); return { domain: SKATE_NIP05_DOMAIN, checkAvailability, claim, release, canClaim: !!user }; }