import { useQuery } from '@tanstack/react-query'; import { CapacitorHttp } from '@capacitor/core'; import { isNativePlatform } from '@/lib/platform'; /** * A mint from the MintRadar community directory (https://mintradar.org). * * MintRadar sends no CORS headers, so on the WEB we fetch it **same-origin** at * `/api/mints/*` — Netlify proxies it in prod (see `public/_redirects`) and the * Vite proxy does in dev. * * On the NATIVE app the shell is served from `https://localhost`, so a relative * `/api/...` path hits the **local asset server** and gets `index.html` back — * which blew up as `Unexpected token '<' … is not valid JSON` on the Mints page. * So native goes to the real site with Capacitor's native HTTP, which also * bypasses WebView CORS (MintRadar has none). * * The data is unofficial, rate-limited and may change without notice — treat it * as a hint, not gospel. */ export interface MintDirectoryEntry { url: string; name?: string; iconUrl?: string; descriptionLong?: string; online?: boolean; degraded?: boolean; archived?: boolean; latencyMs?: number; version?: string; nutCount?: number; tosUrl?: string; lastOnlineAt?: string; // ── Community signal MintRadar also returns (previously unused) ──────────── /** Average community rating, 0–5. */ reviewAvgRating?: number; /** How many community ratings back the average. */ reviewCount?: number; /** Rating after MintRadar's own weighting. */ reviewWeightedRating?: number; /** 0–100 composite reliability score. */ reliabilityScore?: number; uptimePct24h?: number; uptimePct7d?: number; /** Liveness audit volume — tokens actually minted/melted through it. */ auditNMints?: number; auditNMelts?: number; auditNErrors?: number; auditRecentErrors?: number; auditRecentTotal?: number; auditCheckedAt?: string; serverLocation?: string; units?: string[]; mintMethods?: Array<{ unit?: string; method?: string; min_amount?: number; max_amount?: number }>; meltMethods?: Array<{ unit?: string; method?: string; min_amount?: number; max_amount?: number }>; /** The mint's own Nostr announcement key, when it publishes one. */ pubkey?: string; nostrAnnouncedAt?: string; discoveredAt?: string; lastError?: string | null; } const SITE_ORIGIN = 'https://skate-spots.org'; const DIRECTORY_PATH = '/api/mints/known'; const UNAVAILABLE = 'The mint directory is unavailable right now.'; /** Parse a JSON body, turning an HTML page into a friendly error. */ function parseDirectory(text: string): unknown { const trimmed = text.trim(); if (trimmed.startsWith('<')) { // An HTML page (SPA fallback or an upstream error) — never surface a raw // "Unexpected token '<'" to the user. throw new Error(UNAVAILABLE); } try { return JSON.parse(trimmed); } catch { throw new Error('Unexpected mint directory response.'); } } function normalize(data: unknown): MintDirectoryEntry[] { if (!Array.isArray(data)) throw new Error('Unexpected mint directory response.'); return (data as MintDirectoryEntry[]).filter( (m) => m && typeof m.url === 'string' && m.url.length > 0, ); } async function loadDirectory(signal: AbortSignal): Promise { // Web / dev: relative URL → same-origin proxy. if (!isNativePlatform()) { const res = await fetch(DIRECTORY_PATH, { signal, headers: { accept: 'application/json' }, }); if (!res.ok) throw new Error(`The mint directory is unavailable right now (HTTP ${res.status}).`); return normalize(parseDirectory(await res.text())); } const absolute = `${SITE_ORIGIN}${DIRECTORY_PATH}`; // Native: Capacitor's HTTP plugin (no CORS restrictions). try { const res = await CapacitorHttp.request({ url: absolute, method: 'GET', headers: { accept: 'application/json' }, connectTimeout: 15000, readTimeout: 15000, }); if (res.status >= 200 && res.status < 300) { // CapacitorHttp parses JSON for us; a non-JSON body arrives as a string. const raw: unknown = typeof res.data === 'string' ? parseDirectory(res.data) : res.data; return normalize(raw); } console.warn('[mints] native HTTP returned', res.status); } catch (e) { console.warn('[mints] native HTTP failed:', e); } // Fallback: a plain fetch to the absolute URL (works on CORS-free WebViews). const res = await fetch(absolute, { signal, headers: { accept: 'application/json' } }); if (!res.ok) throw new Error(`The mint directory is unavailable right now (HTTP ${res.status}).`); return normalize(parseDirectory(await res.text())); } export function useMintDirectory() { return useQuery({ queryKey: ['mint-directory'], staleTime: 10 * 60 * 1000, retry: 1, queryFn: (c) => loadDirectory(AbortSignal.any([c.signal, AbortSignal.timeout(20000)])), }); }