← 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;