// @ts-check /** * Allow HTTP / WS to staging API hosts via App Transport Security. * * Enabled when EXPO_PUBLIC_API_URL uses http:// (same rule as Android cleartext). * Collects hosts from both API and WS URLs (IP:port staging often differs only by scheme). */ const { withInfoPlist } = require('@expo/config-plugins'); /** * @param {string | undefined} raw * @returns {string | null} */ function insecureHttpHostFromUrl(raw) { if (!raw || !raw.startsWith('http://')) { return null; } try { return new URL(raw).hostname || null; } catch { return null; } } /** * @param {string | undefined} raw * @returns {string | null} */ function insecureWsHostFromUrl(raw) { if (!raw || !raw.startsWith('ws://')) { return null; } try { return new URL(raw).hostname || null; } catch { return null; } } /** * @param {string | undefined} apiUrl * @param {string | undefined} wsUrl * @returns {string[]} */ function collectInsecureHosts(apiUrl, wsUrl) { const hosts = new Set( [insecureHttpHostFromUrl(apiUrl), insecureWsHostFromUrl(wsUrl)].filter( (h) => typeof h === 'string' && h.length > 0, ), ); return [...hosts]; } /** * @param {string} host */ function isIpv4Literal(host) { return /^\d{1,3}(\.\d{1,3}){3}$/u.test(host); } /** * @param {import('expo/config').ExpoConfig} config * @param {{ enabled?: boolean; apiUrl?: string; wsUrl?: string }} props */ function withIosInsecureHttp(config, props = {}) { const enabled = props.enabled ?? false; const apiUrl = props.apiUrl ?? process.env.EXPO_PUBLIC_API_URL ?? ''; const wsUrl = props.wsUrl ?? process.env.EXPO_PUBLIC_WS_URL ?? ''; return withInfoPlist(config, (mod) => { if (!enabled) { return mod; } const hosts = collectInsecureHosts(apiUrl, wsUrl); if (hosts.length === 0) { console.warn( '[withIosInsecureHttp] enabled but no http/ws hosts found in apiUrl/wsUrl; skipping ATS exception.', ); return mod; } const existing = mod.modResults.NSAppTransportSecurity ?? {}; const existingDomains = existing.NSExceptionDomains ?? {}; /** @type {Record} */ const exceptionDomains = { ...existingDomains }; for (const host of hosts) { exceptionDomains[host] = { NSExceptionAllowsInsecureHTTPLoads: true, // IP literals have no subdomains; false avoids odd ATS behavior on some iOS versions. NSIncludesSubdomains: !isIpv4Literal(host), NSExceptionRequiresForwardSecrecy: false, }; } mod.modResults.NSAppTransportSecurity = { ...existing, /** * Staging often uses bare IP:port HTTP. Domain exceptions alone can fail on * newer iOS builds; allow cleartext while this plugin is enabled (http:// API only). */ NSAllowsArbitraryLoads: true, NSExceptionDomains: exceptionDomains, }; console.log( `[withIosInsecureHttp] ATS cleartext enabled for host(s): ${hosts.join(', ')}`, ); return mod; }); } module.exports = withIosInsecureHttp;