SkateSpots/src/lib/mintReachability.ts
shakespeare.diy fc7564a6f8 Platform-aware wallet mints (Option C: native + browser scoping)
- Add @capacitor/core + src/lib/platform.ts (isNativePlatform/getPlatform, web-safe)
- mintReachability: on native (no CORS jail) short-circuit probe to 'ok' so ANY mint works, including mint.minibits.cash
- useCashu default mint: native prefers minibits (matches skatespots@minibits.cash brand identity) then coinos/21mint; browser uses CORS-friendly candidates
- MintPrivacyManager recommended list is platform-aware: native leads with Minibits (recommended), web leads with Coinos and flags Minibits as native-only
- CashuWallet: 'This mint can't be reached from your browser' banner for the browser CORS case (never shows on native), pointing users to a web-friendly mint or the native app
- Lightning invoice error mentions the native-app option

Same codebase now uses minibits directly when running as the native app, and a CORS-safe mint when running in a web browser.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 17:22:41 +07:00

82 lines
2.7 KiB
TypeScript

/**
* Cashu mint reachability check.
*
* A Cashu mint is only usable from a WEB wallet if it answers `GET /v1/info`
* with permissive CORS headers. Many popular mints (e.g. mint.minibits.cash)
* work great in mobile apps but do NOT set `Access-Control-Allow-Origin`, so the
* browser blocks them with a `NetworkError` — which otherwise only surfaces
* later as a cryptic "Failed to create invoice".
*
* In a NATIVE (Capacitor) shell there is no CORS restriction, so every mint is
* reachable and we short-circuit the probe to `ok`.
*/
import { isNativePlatform } from '@/lib/platform';
export type MintReachability = 'ok' | 'cors-blocked' | 'unreachable';
export interface MintProbeResult {
reachability: MintReachability;
/** Mint name from /v1/info when reachable. */
name?: string;
}
function normalizeMintUrl(url: string): string {
let u = url.trim().replace(/\/+$/, '');
if (!/^https?:\/\//.test(u)) u = `https://${u}`;
return u;
}
/**
* Probe a mint's `/v1/info` from the browser. Distinguishes a CORS block
* (fetch rejects with a TypeError and the request never yields a response)
* from a genuinely down mint. Best-effort — treats ambiguous failures as
* `unreachable` rather than falsely flagging CORS.
*/
export async function probeMintReachability(
mintUrl: string,
timeoutMs = 7000,
): Promise<MintProbeResult> {
// Native shells (Capacitor WebView) aren't subject to CORS — any mint works.
if (isNativePlatform()) {
return { reachability: 'ok' };
}
const base = normalizeMintUrl(mintUrl);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`${base}/v1/info`, {
method: 'GET',
headers: { Accept: 'application/json' },
signal: controller.signal,
mode: 'cors',
});
if (!res.ok) {
return { reachability: 'unreachable' };
}
let name: string | undefined;
try {
const info = await res.json();
if (info && typeof info.name === 'string') name = info.name;
} catch {
// info body wasn't JSON — still counts as reachable/CORS-ok
}
return { reachability: 'ok', name };
} catch (err) {
// A CORS failure throws a TypeError with a "Failed to fetch"-style message
// and no response object. An abort (timeout) is a genuine unreachable.
if (controller.signal.aborted) {
return { reachability: 'unreachable' };
}
const msg = err instanceof Error ? err.message : String(err);
if (/failed to fetch|networkerror|load failed|cors/i.test(msg)) {
return { reachability: 'cors-blocked' };
}
return { reachability: 'unreachable' };
} finally {
clearTimeout(timer);
}
}