fix(mints): native app fetched /api/mints from the local shell -> HTML/JSON error
On the native app the shell is served from https://localhost, so the relative /api/mints/known hit the local asset server and returned index.html, surfacing as 'Unexpected token < ... not valid JSON' on /mints. - Native now fetches https://skate-spots.org/api/mints/known via CapacitorHttp (bundled core plugin, no WebView CORS), with a plain-fetch fallback. - Web/dev unchanged (same-origin relative path via the Netlify redirect / Vite proxy). - HTML or unparseable bodies now become a friendly 'unavailable' message instead of a raw JSON parse error.
This commit is contained in:
parent
4e4c50ab71
commit
6a442c394f
2 changed files with 88 additions and 18 deletions
|
|
@ -248,6 +248,15 @@ session with **no other memory of this project**. So:
|
|||
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
|
||||
https://mintradar.org/api/mints/:splat 200`), dev via `server.proxy` in
|
||||
`vite.config.ts`. Data is unofficial/rate-limited; adding a mint is native-only.
|
||||
⚠️ **Relative `/api/*` paths don't work in the native app** (fixed 2026-09-27):
|
||||
the shell is served from `https://localhost`, so `/api/mints/known` hits the
|
||||
LOCAL asset server and returns `index.html` → `Unexpected token '<' … not valid
|
||||
JSON`. `useMintDirectory` now uses `https://skate-spots.org/api/mints/known`
|
||||
via **CapacitorHttp** on native (core plugin, no CORS) with a plain-fetch
|
||||
fallback, and turns an HTML body into a friendly message. Same pattern already
|
||||
used by `sasquatchBrain.ts` / `useSasquatchAiStatus` (`Capacitor.isNativePlatform()
|
||||
? 'https://skate-spots.org/api/…' : '/api/…'`) — follow it for any new `/api`
|
||||
call, and never let a raw JSON parse error reach the UI.
|
||||
- **Lazy routes**: every route uses `lazyWithRetry` (`src/lib/lazyWithRetry.ts`) —
|
||||
retries, then one full reload per session, so a chunk-load failure self-heals
|
||||
instead of showing the error boundary.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
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).
|
||||
*
|
||||
* Fetched **same-origin** at `/api/mints/*` — MintRadar has no CORS, so prod
|
||||
* proxies it via a Netlify redirect (see `public/_redirects`) and dev via the
|
||||
* Vite proxy in `vite.config.ts`. The data is unofficial, rate-limited and may
|
||||
* change without notice — treat it as a hint, not gospel.
|
||||
* 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;
|
||||
|
|
@ -23,24 +33,75 @@ export interface MintDirectoryEntry {
|
|||
lastOnlineAt?: string;
|
||||
}
|
||||
|
||||
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<MintDirectoryEntry[]> {
|
||||
// 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<MintDirectoryEntry[]>({
|
||||
queryKey: ['mint-directory'],
|
||||
staleTime: 10 * 60 * 1000,
|
||||
retry: 1,
|
||||
queryFn: async (c) => {
|
||||
const res = await fetch('/api/mints/known', {
|
||||
signal: AbortSignal.any([c.signal, AbortSignal.timeout(15000)]),
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`The mint directory is unavailable right now (HTTP ${res.status}).`);
|
||||
}
|
||||
const data = (await res.json()) as unknown;
|
||||
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,
|
||||
);
|
||||
},
|
||||
queryFn: (c) => loadDirectory(AbortSignal.any([c.signal, AbortSignal.timeout(20000)])),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue