- 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>
82 lines
2.7 KiB
TypeScript
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);
|
|
}
|
|
}
|