diff --git a/package-lock.json b/package-lock.json index 2ec57f0..69e443b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@bitcoinerlab/secp256k1": "^1.2.0", "@capacitor/camera": "^8.2.1", + "@capacitor/core": "^8.4.2", "@cashu/cashu-ts": "2.8.1", "@contextvm/sdk": "^0.12.0", "@fontsource-variable/inter": "^5.2.6", @@ -292,6 +293,12 @@ "integrity": "sha512-yq4TbVcv9Z91JLr7C5407s3rrXr/Dmk+Pr9FAVRn65s3Jg4iYabjwG4fVKUYlQL5TiUdBbZA3LiNAbUxJNcUkw==", "license": "MIT" }, + "node_modules/@capacitor/core": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.4.2.tgz", + "integrity": "sha512-fQPRb3JXRaU2pnufDUvqOjhsrYxDQeFRZyaj/sHydIUxD2NxOGHKMpmKUdI+U4OOmKuGZyvRpi7TSJU+7Bjbmg==", + "license": "MIT" + }, "node_modules/@cashu/cashu-ts": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.8.1.tgz", diff --git a/package.json b/package.json index 934b661..0acb9bf 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@bitcoinerlab/secp256k1": "^1.2.0", "@capacitor/camera": "^8.2.1", + "@capacitor/core": "^8.4.2", "@cashu/cashu-ts": "2.8.1", "@contextvm/sdk": "^0.12.0", "@fontsource-variable/inter": "^5.2.6", diff --git a/src/components/wallet/CashuWallet.tsx b/src/components/wallet/CashuWallet.tsx index 9fbac14..0562a02 100644 --- a/src/components/wallet/CashuWallet.tsx +++ b/src/components/wallet/CashuWallet.tsx @@ -98,26 +98,35 @@ export function CashuWallet() { mints, }); - // 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. + // Tracks the browser-only case where the user has mints but none are + // reachable (all CORS-blocked). Native shells never hit this. + const [noReachableMint, setNoReachableMint] = useState(false); + + // Pick an active mint when none is selected — prefer a 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) return; let cancelled = false; (async () => { + const { probeMintReachability } = await import('@/lib/mintReachability'); // 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); + setNoReachableMint(false); return; } } - // Fallback: nothing reachable — select the first so the UI still renders. - if (!cancelled) setActiveMintUrl(mints[0].url); + // Nothing reachable — select the first so the UI still renders, and flag + // the state so we can warn the user (browser CORS case). + if (!cancelled) { + setActiveMintUrl(mints[0].url); + setNoReachableMint(true); + } })(); return () => { @@ -190,6 +199,21 @@ export function CashuWallet() { + {/* No reachable mint (browser CORS) warning */} + {noReachableMint && ( + + + +

This mint can't be reached from your browser

+

+ Your saved mint doesn't allow web requests (no CORS). Add a web-friendly + mint below (e.g. mint.coinos.io) and set it active — or open + SkateSpots in the native app, where any mint (including Minibits) works. +

+
+
+ )} + {/* Balance Display */}
Total Balance
diff --git a/src/components/wallet/MintPrivacyManager.tsx b/src/components/wallet/MintPrivacyManager.tsx index 6d5cf6e..cadcb7b 100644 --- a/src/components/wallet/MintPrivacyManager.tsx +++ b/src/components/wallet/MintPrivacyManager.tsx @@ -25,6 +25,7 @@ import { } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useToast } from '@/hooks/useToast'; +import { isNativePlatform } from '@/lib/platform'; interface Mint { url: string; @@ -45,24 +46,33 @@ interface MintPrivacyManagerProps { // Recommended mints (no stablecoin mints). // Ordered by browser reachability — web wallets can only use mints that send -// CORS headers. Minibits works great in mobile apps but does NOT allow browser -// requests, so it's flagged and listed last. -const RECOMMENDED_MINTS = [ - { - url: 'https://mint.coinos.io', - name: 'Coinos', - description: 'Web-friendly mint (recommended for browsers)', - }, - { - url: 'https://legend.lnbits.com/cashu/api/v1/AptDNABNBXv8gpuywhx6NV', - name: 'LNbits Legend', - description: 'Community mint on LNbits', - }, - { - url: 'https://mint.minibits.cash/Bitcoin', - name: 'Minibits', - description: '⚠ Mobile-app only — not reachable from web browsers', - }, +// CORS headers. Minibits works great natively but does NOT allow browser +// requests, so on the web it's flagged and listed last. In a native shell there +// is no CORS restriction, so Minibits (the SkateSpots brand mint) leads. +const MINIBITS = { + url: 'https://mint.minibits.cash/Bitcoin', + name: 'Minibits', +}; +const COINOS = { + url: 'https://mint.coinos.io', + name: 'Coinos', +}; +const LNBITS = { + url: 'https://legend.lnbits.com/cashu/api/v1/AptDNABNBXv8gpuywhx6NV', + name: 'LNbits Legend', + description: 'Community mint on LNbits', +}; + +const RECOMMENDED_MINTS_WEB = [ + { ...COINOS, description: 'Web-friendly mint (recommended for browsers)' }, + LNBITS, + { ...MINIBITS, description: '⚠ Native app only — not reachable from web browsers' }, +]; + +const RECOMMENDED_MINTS_NATIVE = [ + { ...MINIBITS, description: 'SkateSpots mint — recommended' }, + { ...COINOS, description: 'Web-friendly mint' }, + LNBITS, ]; export function MintPrivacyManager({ @@ -75,6 +85,7 @@ export function MintPrivacyManager({ totalBalance, className, }: MintPrivacyManagerProps) { + const RECOMMENDED_MINTS = isNativePlatform() ? RECOMMENDED_MINTS_NATIVE : RECOMMENDED_MINTS_WEB; const [showPrivacyTips, setShowPrivacyTips] = useState(false); const [showAddMint, setShowAddMint] = useState(false); const [mintUrl, setMintUrl] = useState(''); diff --git a/src/hooks/wallet/useCashu.ts b/src/hooks/wallet/useCashu.ts index 2ac73b0..ea4fb1f 100644 --- a/src/hooks/wallet/useCashu.ts +++ b/src/hooks/wallet/useCashu.ts @@ -6,6 +6,7 @@ import { initializeCoco, ConsoleLogger, getEncodedToken, type Repositories } fro import { IndexedDbRepositories } from 'coco-cashu-indexeddb'; import type { Mint } from 'coco-cashu-core'; import { probeMintReachability } from '@/lib/mintReachability'; +import { isNativePlatform } from '@/lib/platform'; // Types from coco-cashu-core type CocoManager = Awaited>; @@ -193,12 +194,21 @@ export function useCashu(): UseCashuReturn { } if (!hasReachable) { - const CANDIDATE_MINTS = [ - 'https://mint.coinos.io', - 'https://21mint.me', - 'https://stablenut.cashu.network', - 'https://mint.lnvoltz.com', - ]; + // Native shells have no CORS jail, so prefer the SkateSpots brand mint + // (mint.minibits.cash — matches skatespots@minibits.cash). In a browser + // that mint is blocked, so fall back to CORS-friendly candidates. + const CANDIDATE_MINTS = isNativePlatform() + ? [ + 'https://mint.minibits.cash/Bitcoin', + 'https://mint.coinos.io', + 'https://21mint.me', + ] + : [ + '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()), ); diff --git a/src/hooks/wallet/useLightningOperations.ts b/src/hooks/wallet/useLightningOperations.ts index 6c5962d..aa8666c 100644 --- a/src/hooks/wallet/useLightningOperations.ts +++ b/src/hooks/wallet/useLightningOperations.ts @@ -100,7 +100,7 @@ export function useLightningOperations({ variant: 'destructive', title: looksUnreachable ? "Can't reach this mint" : 'Failed to create invoice', description: looksUnreachable - ? `${activeMintUrl?.replace(/^https?:\/\//, '')} can't be reached from your browser (it may not allow web requests). Switch to a different mint in wallet settings and try again.` + ? `${activeMintUrl?.replace(/^https?:\/\//, '')} can't be reached from your browser (no CORS). Switch to a web-friendly mint in wallet settings — or use the native app, where any mint works.` : raw, }); } diff --git a/src/lib/mintReachability.ts b/src/lib/mintReachability.ts index b984fd3..38156ca 100644 --- a/src/lib/mintReachability.ts +++ b/src/lib/mintReachability.ts @@ -1,15 +1,17 @@ /** - * Browser-side Cashu mint reachability check. + * Cashu mint reachability check. * - * A Cashu mint is only usable from a web wallet if it answers `GET /v1/info` + * 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". * - * This probe runs in the USER's browser (where CORS actually applies), so it's - * the only reliable place to detect the problem. + * 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 { @@ -34,6 +36,11 @@ export async function probeMintReachability( mintUrl: string, timeoutMs = 7000, ): Promise { + // 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); diff --git a/src/lib/platform.ts b/src/lib/platform.ts new file mode 100644 index 0000000..8429d5c --- /dev/null +++ b/src/lib/platform.ts @@ -0,0 +1,28 @@ +import { Capacitor } from '@capacitor/core'; + +/** + * Is the app running inside a native (Capacitor) shell — iOS / Android / desktop + * WebView — rather than a plain web browser? + * + * This matters for networking: a native WebView is NOT subject to browser CORS + * rules, so it can talk to any Cashu mint directly (including mint.minibits.cash, + * which sends no CORS headers). In a plain browser we must use a CORS-enabled + * mint. `Capacitor.isNativePlatform()` is web-safe and simply returns `false` + * when there's no native runtime. + */ +export function isNativePlatform(): boolean { + try { + return Capacitor.isNativePlatform(); + } catch { + return false; + } +} + +/** 'ios' | 'android' | 'web' */ +export function getPlatform(): string { + try { + return Capacitor.getPlatform(); + } catch { + return 'web'; + } +}