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/* →
|
at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* →
|
||||||
https://mintradar.org/api/mints/:splat 200`), dev via `server.proxy` in
|
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.
|
`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`) —
|
- **Lazy routes**: every route uses `lazyWithRetry` (`src/lib/lazyWithRetry.ts`) —
|
||||||
retries, then one full reload per session, so a chunk-load failure self-heals
|
retries, then one full reload per session, so a chunk-load failure self-heals
|
||||||
instead of showing the error boundary.
|
instead of showing the error boundary.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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).
|
* A mint from the MintRadar community directory (https://mintradar.org).
|
||||||
*
|
*
|
||||||
* Fetched **same-origin** at `/api/mints/*` — MintRadar has no CORS, so prod
|
* MintRadar sends no CORS headers, so on the WEB we fetch it **same-origin** at
|
||||||
* proxies it via a Netlify redirect (see `public/_redirects`) and dev via the
|
* `/api/mints/*` — Netlify proxies it in prod (see `public/_redirects`) and the
|
||||||
* Vite proxy in `vite.config.ts`. The data is unofficial, rate-limited and may
|
* Vite proxy does in dev.
|
||||||
* change without notice — treat it as a hint, not gospel.
|
*
|
||||||
|
* 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 {
|
export interface MintDirectoryEntry {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -23,24 +33,75 @@ export interface MintDirectoryEntry {
|
||||||
lastOnlineAt?: string;
|
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() {
|
export function useMintDirectory() {
|
||||||
return useQuery<MintDirectoryEntry[]>({
|
return useQuery<MintDirectoryEntry[]>({
|
||||||
queryKey: ['mint-directory'],
|
queryKey: ['mint-directory'],
|
||||||
staleTime: 10 * 60 * 1000,
|
staleTime: 10 * 60 * 1000,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
queryFn: async (c) => {
|
queryFn: (c) => loadDirectory(AbortSignal.any([c.signal, AbortSignal.timeout(20000)])),
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue