diff --git a/mobile/app/(tabs)/import.tsx b/mobile/app/(tabs)/import.tsx index 0c6458e..beb99dc 100644 --- a/mobile/app/(tabs)/import.tsx +++ b/mobile/app/(tabs)/import.tsx @@ -91,14 +91,17 @@ export default function ImportScreen() { encoding: FileSystem.EncodingType.Base64, }); - // Wheel URL: prefer the configured instance; fall back to bincio.org. + // Fetch the bincio wheel here (React Native networking), not inside the + // WebView. WKWebView blocks HTTP requests via ATS; RN native networking + // allows local-network HTTP (NSAllowsLocalNetworking=true in Info.plist). const instanceUrl = await getInstanceUrl(dbCtx); - const wheelUrl = await resolveWheelUrl(instanceUrl); + setState({ status: 'loading', msg: 'Fetching Bincio engine…' }); + const wheelBase64 = await fetchWheelBase64(instanceUrl); const result = await extractFile( name, base64, - wheelUrl, + wheelBase64, (msg) => setState({ status: 'loading', msg }), ); @@ -146,11 +149,9 @@ export default function ImportScreen() { {state.status === 'loading' && ( {state.msg} - {state.msg.startsWith('Load') && ( - - First run downloads ~35 MB of the Python runtime. Subsequent runs are instant. - - )} + + First run downloads ~35 MB (Python runtime + packages). Subsequent runs are instant. + )} @@ -204,17 +205,31 @@ async function getInstanceUrl(db: ReturnType): Promise< return (row?.value ?? '').replace(/\/$/, ''); } -async function resolveWheelUrl(instanceUrl: string): Promise { +// In-memory cache so repeated imports in one session don't re-download the wheel. +let _cachedWheelBase64: string | null = null; + +async function fetchWheelBase64(instanceUrl: string): Promise { + if (_cachedWheelBase64) return _cachedWheelBase64; + const base = instanceUrl || 'https://bincio.org'; + + // Ask the instance for the canonical wheel URL (handles both dev and prod layouts). + let wheelUrl = `${base}/api/wheel/download`; try { - const resp = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); - if (resp.ok) { - const d = await resp.json() as { api_url?: string; url?: string }; + const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); + if (vr.ok) { + const d = await vr.json() as { api_url?: string; url?: string }; const path = d.api_url ?? d.url ?? '/api/wheel/download'; - return path.startsWith('http') ? path : `${base}${path}`; + wheelUrl = path.startsWith('http') ? path : `${base}${path}`; } } catch {} - return `${base}/api/wheel/download`; + + // Fetch via React Native networking (supports local HTTP; WKWebView would block it). + const resp = await fetch(wheelUrl); + if (!resp.ok) throw new Error(`Could not download Bincio engine (${resp.status}). Is the instance running?`); + const buf = await resp.arrayBuffer(); + _cachedWheelBase64 = Buffer.from(buf).toString('base64'); + return _cachedWheelBase64; } // ── Styles ─────────────────────────────────────────────────────────────────── diff --git a/mobile/extraction/PyodideWebView.tsx b/mobile/extraction/PyodideWebView.tsx index f430e06..222ef8b 100644 --- a/mobile/extraction/PyodideWebView.tsx +++ b/mobile/extraction/PyodideWebView.tsx @@ -87,10 +87,10 @@ var initError = null; })(); window._bincioExtract = async function(params) { - var reqId = params.reqId; - var filename = params.filename; - var base64 = params.base64; - var wheelUrl = params.wheelUrl; + var reqId = params.reqId; + var filename = params.filename; + var base64 = params.base64; + var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues) function post(m) { _post(Object.assign({}, m, { reqId: reqId })); } @@ -108,13 +108,14 @@ window._bincioExtract = async function(params) { } if (initError) throw new Error(initError); - // Install bincio wheel on first extraction (lazy: keeps startup fast) + // Install bincio wheel on first extraction. + // Wheel bytes arrive pre-fetched from the React Native side as base64, + // so the WebView never needs to make an HTTP request (avoids ATS blocks). if (!wheelReady) { post({ type: 'progress', msg: 'Loading Bincio…' }); - var resp = await fetch(wheelUrl); - if (!resp.ok) throw new Error('Failed to fetch Bincio wheel (' + resp.status + ')'); - var wheelBlob = new Blob([await resp.arrayBuffer()]); - var blobUrl = URL.createObjectURL(wheelBlob); + var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); }); + var wheelBlob = new Blob([wheelBytes]); + var blobUrl = URL.createObjectURL(wheelBlob); pyodide.globals.set('_blobUrl', blobUrl); await pyodide.runPythonAsync(_PY_INSTALL_WHEEL); URL.revokeObjectURL(blobUrl); @@ -158,7 +159,7 @@ export function PyodideWebView() { return ( void = () => {}, ): Promise { if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress')); @@ -67,7 +69,7 @@ export function extractFile( isExtracting = true; const reqId = String(++reqCounter); - const args = JSON.stringify({ reqId, filename, base64, wheelUrl }); + const args = JSON.stringify({ reqId, filename, base64, wheelBase64 }); return new Promise((resolve, reject) => { pending.set(reqId, {