fix: pre-fetch bincio wheel via RN networking to avoid ATS blocking HTTP in WKWebView

WKWebView blocks HTTP requests (ATS) even when NSAllowsLocalNetworking is set for
the app's own networking — so fetch(http://192.168.x.x/api/wheel/download) inside
the WebView always fails with 'Load failed' on iOS.

- extractActivity.ts: rename wheelUrl param to wheelBase64; WebView now receives
  the wheel as pre-fetched base64 bytes rather than a URL to fetch itself
- PyodideWebView.tsx: decode wheelBase64 → Uint8Array → Blob → blob URL for
  micropip.install; fix baseUrl '' → 'https://localhost' (null origin blocks fetch
  on iOS)
- import.tsx: add fetchWheelBase64() that resolves the wheel URL via
  /api/wheel/version then fetches with native networking (HTTP works); caches
  result in memory so repeated imports in one session don't re-download
This commit is contained in:
Davide Scaini
2026-04-24 23:13:24 +02:00
parent 966528a0bf
commit 84e5cead08
3 changed files with 44 additions and 26 deletions
+29 -14
View File
@@ -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' && (
<View style={styles.statusBox}>
<Text style={styles.statusMsg}>{state.msg}</Text>
{state.msg.startsWith('Load') && (
<Text style={styles.statusHint}>
First run downloads ~35 MB of the Python runtime. Subsequent runs are instant.
</Text>
)}
<Text style={styles.statusHint}>
First run downloads ~35 MB (Python runtime + packages). Subsequent runs are instant.
</Text>
</View>
)}
@@ -204,17 +205,31 @@ async function getInstanceUrl(db: ReturnType<typeof useSQLiteContext>): Promise<
return (row?.value ?? '').replace(/\/$/, '');
}
async function resolveWheelUrl(instanceUrl: string): Promise<string> {
// 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<string> {
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 ───────────────────────────────────────────────────────────────────