From b830bf965d27bb68a2c9c47dc1c503825939c343 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Fri, 24 Jul 2026 17:11:31 +0700 Subject: [PATCH] Auto-heal wallets stuck on a CORS-blocked mint - useCashu now probes existing mints on every init (not just when empty); if none are browser-reachable it adds a reachable candidate mint. This fixes existing users whose only saved mint is the legacy CORS-blocked mint.minibits.cash default. - CashuWallet active-mint selection now prefers the first browser-reachable mint instead of blindly mints[0], so users aren't stuck with a mint that fails every invoice. Co-authored-by: shakespeare.diy --- src/components/wallet/CashuWallet.tsx | 28 +++++++++++++++++--- src/hooks/wallet/useCashu.ts | 38 ++++++++++++++++++++------- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/components/wallet/CashuWallet.tsx b/src/components/wallet/CashuWallet.tsx index 1eafe49..9fbac14 100644 --- a/src/components/wallet/CashuWallet.tsx +++ b/src/components/wallet/CashuWallet.tsx @@ -98,11 +98,31 @@ export function CashuWallet() { mints, }); - // Set first mint as active if none selected + // Pick an active mint when none is selected — prefer a browser-REACHABLE mint + // so a user whose first saved mint is CORS-blocked (e.g. legacy minibits) + // doesn't get stuck with a mint that fails every invoice. useEffect(() => { - if (mints.length > 0 && !activeMintUrl) { - setActiveMintUrl(mints[0].url); - } + if (mints.length === 0 || activeMintUrl) return; + let cancelled = false; + + (async () => { + // Probe mints in order; select the first reachable one. + for (const m of mints) { + const { probeMintReachability } = await import('@/lib/mintReachability'); + const probe = await probeMintReachability(m.url); + if (cancelled) return; + if (probe.reachability === 'ok') { + setActiveMintUrl(m.url); + return; + } + } + // Fallback: nothing reachable — select the first so the UI still renders. + if (!cancelled) setActiveMintUrl(mints[0].url); + })(); + + return () => { + cancelled = true; + }; }, [mints, activeMintUrl, setActiveMintUrl]); // Show wallet loading state diff --git a/src/hooks/wallet/useCashu.ts b/src/hooks/wallet/useCashu.ts index 1141ac7..2ac73b0 100644 --- a/src/hooks/wallet/useCashu.ts +++ b/src/hooks/wallet/useCashu.ts @@ -167,35 +167,55 @@ export function useCashu(): UseCashuReturn { console.warn('[Cashu] Failed to enable some coco services:', err); } - // Add a default mint if the user has none yet. + // Ensure the user has at least one BROWSER-REACHABLE mint. // // A mint MUST send permissive CORS headers or the browser blocks every // request with a NetworkError ("Failed to fetch mint"). We can't know from - // build time which mint is reachable in THIS browser, so we probe an - // ordered list of web-friendly candidates and add the first that works. - // (mint.minibits.cash is intentionally excluded — it's mobile-app only.) + // build time which mint is reachable in THIS browser, so we probe. + // + // This runs on every init so it also AUTO-HEALS an existing user whose + // only saved mint is CORS-blocked (e.g. legacy mint.minibits.cash default): + // if none of their mints are reachable, we add a working one and — since + // the UI picks the first mint — keep the broken one but ensure a good one + // exists. (mint.minibits.cash is excluded from candidates — mobile only.) try { const existingMints = await coco.mint.getAllTrustedMints(); - if (existingMints.length === 0) { + + // Does the user already have at least one reachable mint? + let hasReachable = false; + for (const m of existingMints) { + const probe = await probeMintReachability(m.mintUrl); + if (probe.reachability === 'ok') { + hasReachable = true; + break; + } + console.warn('[Cashu] Existing mint not usable in browser:', m.mintUrl, probe.reachability); + } + + if (!hasReachable) { const CANDIDATE_MINTS = [ 'https://mint.coinos.io', 'https://21mint.me', 'https://stablenut.cashu.network', 'https://mint.lnvoltz.com', ]; + const alreadyHave = new Set( + existingMints.map((m: Mint) => m.mintUrl.toLowerCase()), + ); for (const url of CANDIDATE_MINTS) { + if (alreadyHave.has(url.toLowerCase())) continue; const probe = await probeMintReachability(url); if (probe.reachability === 'ok') { - console.log('[Cashu] Adding default mint:', url); + console.log('[Cashu] Adding reachable mint:', url); await coco.mint.addMint(url, { trusted: true }); - console.log('[Cashu] Default mint added successfully'); + console.log('[Cashu] Reachable mint added successfully'); break; } - console.warn('[Cashu] Skipping unreachable default candidate:', url, probe.reachability); + console.warn('[Cashu] Skipping unreachable candidate:', url, probe.reachability); } } } catch (err) { - console.warn('[Cashu] Failed to add default mint:', err); + console.warn('[Cashu] Failed to ensure a reachable mint:', err); } setIsInitialized(true);