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>
This commit is contained in:
parent
b830bf965d
commit
fc7564a6f8
8 changed files with 123 additions and 35 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* No reachable mint (browser CORS) warning */}
|
||||
{noReachableMint && (
|
||||
<Alert className="bg-amber-950/30 border-amber-500/50">
|
||||
<AlertCircle className="h-4 w-4 text-amber-400" />
|
||||
<AlertDescription className="text-gray-300 space-y-1">
|
||||
<p className="font-bold text-amber-300">This mint can't be reached from your browser</p>
|
||||
<p className="text-sm">
|
||||
Your saved mint doesn't allow web requests (no CORS). Add a web-friendly
|
||||
mint below (e.g. <span className="font-mono">mint.coinos.io</span>) and set it active — or open
|
||||
SkateSpots in the native app, where any mint (including Minibits) works.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Balance Display */}
|
||||
<div className="text-center py-6 space-y-2">
|
||||
<div className="text-sm text-gray-400 uppercase tracking-wide">Total Balance</div>
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof initializeCoco>>;
|
||||
|
|
@ -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()),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
|
|
|
|||
28
src/lib/platform.ts
Normal file
28
src/lib/platform.ts
Normal file
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue