← back to Homesonspec

apps/mobile/components/TrackedWebView.tsx

52 lines

/**
 * TrackedWebView — the ONLY sanctioned way to render a WebView in this app.
 *
 * WHY THIS EXISTS (TK-10387, DTD verdict A, 2026-09-03):
 * homesonspec.com serves Google Tag Manager, GA4 and the Meta pixel. The Browse
 * tab had been hardened against those, but the Saved tab opened a SECOND, raw
 * <WebView> to the same site with none of the protection — so trackers ran freely
 * there and the App Store "Data Not Collected" privacy label would have been FALSE.
 *
 * The leak happened because the hardening had to be REMEMBERED at every call site.
 * A per-call-site invariant enforced only by developer memory fails exactly once
 * per new WebView. So the fix is structural, not another copy-paste: this component
 * bakes the protection in, and `lib/webview-guard.test.mjs` fails the test suite if
 * any other file imports `react-native-webview` directly.
 *
 * Callers may still pass `injectedJavaScriptBeforeContentLoaded` for their own
 * scripts — it is APPENDED after the tracker block, never replacing it — and may
 * pass their own `onShouldStartLoadWithRequest`, which is AND-ed with the tracker
 * filter so a caller can further restrict but never loosen it.
 */
import { WebView, WebViewProps } from 'react-native-webview';
import { forwardRef } from 'react';
import { isTrackerUrl, TRACKER_HOSTS } from '../lib/tracker-policy';
import { buildTrackerBlock } from '../lib/tracker-block';

const TRACKER_BLOCK = buildTrackerBlock(TRACKER_HOSTS);

export type TrackedWebViewProps = WebViewProps;

const TrackedWebView = forwardRef<WebView, TrackedWebViewProps>(function TrackedWebView(
  { injectedJavaScriptBeforeContentLoaded, onShouldStartLoadWithRequest, ...rest },
  ref,
) {
  return (
    <WebView
      ref={ref}
      {...rest}
      // Tracker block always runs FIRST; a caller's script is appended, not swapped in.
      injectedJavaScriptBeforeContentLoaded={
        TRACKER_BLOCK + (injectedJavaScriptBeforeContentLoaded ?? '')
      }
      // Tracker filter is AND-ed with any caller filter: callers can restrict further,
      // never loosen.
      onShouldStartLoadWithRequest={(req) =>
        !isTrackerUrl(req.url) && (onShouldStartLoadWithRequest?.(req) ?? true)
      }
    />
  );
});

export default TrackedWebView;