A managed React Native WebView with explicit navigation, readiness, timeout, challenge, and recovery policies.
ManagedWebView wraps
react-native-webview
with a small lifecycle and navigation policy. It keeps embedded content
non-interactive until it is ready, rejects unsafe navigation, unmounts failed
documents, and presents an explicit recovery action instead of opening another
application automatically.
npm install @troco/managed-webview react-native-webviewThis package contains no native code of its own. Your application still needs
to install and configure react-native-webview.
| Dependency | Supported version |
|---|---|
| React | 19.x |
| React Native | 0.86.x and newer 0.x releases |
| react-native-webview | 13.16.x |
| Expo | SDK 57 |
| Platforms | iOS and Android |
import { ManagedWebView } from "@troco/managed-webview";
export function HelpScreen() {
return <ManagedWebView uri="https://example.com/help" />;
}By default, the initial HTTPS origin is the only allowed origin, every path on that origin is allowed, readiness follows the main-document load event, and the timeout is 10 seconds.
Use message readiness when the website has its own initialization step. The message comparison is exact and case-sensitive.
<ManagedWebView
uri="https://example.com/app"
readiness={{ type: "message", message: "app:ready:v1" }}
injectedJavaScript={`
window.ReactNativeWebView.postMessage("app:ready:v1");
true;
`}
/>Only a message from the active allowed document can mark the component ready. Native load completion alone is not sufficient in this mode.
Origins are matched exactly. Subdomains and non-default ports are not included
implicitly. Path prefixes are segment-aware, so /account allows
/account/profile but not /account-malicious. Use allowedPaths when a
document must match exactly without allowing descendants.
import { Linking } from "react-native";
import { ManagedWebView } from "@troco/managed-webview";
<ManagedWebView
uri="https://app.example.com/account"
allowedOrigins={["https://app.example.com", "https://www.example.com"]}
allowedPathPrefixes={["/account"]}
allowedPaths={["/privacy"]}
onNavigationDecision={(decision) => {
console.log(decision.action);
}}
onOpenExternal={(url) => Linking.openURL(url)}
/>;Allowed navigation returns to loading state until the destination satisfies the configured readiness contract. A consumer can also normalize every allowed destination before it loads, for example to preserve an embedded page contract across internal links:
<ManagedWebView
uri="https://app.example.com/account?embedded=1&locale=en"
allowedPathPrefixes={["/account"]}
normalizeAllowedUrl={(value) => {
const url = new URL(value);
url.searchParams.set("embedded", "1");
url.searchParams.set("locale", "en");
return url.href;
}}
/>The normalized result is validated against the same origin and path policy. Unsafe or external results never load inside the WebView.
A valid HTTPS destination outside the allowed policy enters recovery. The destination opens only after the user chooses Open in browser. Malformed URLs, credentials, HTTP URLs, and unsupported schemes are blocked. For a blocked unsafe URL, the browser action uses the last allowed document instead of the rejected destination.
An embedded page can request the same explicit recovery flow with one narrow JSON message contract:
<ManagedWebView
uri="https://example.com/app"
externalNavigationMessageType="app:external:v1"
/>window.ReactNativeWebView.postMessage(
JSON.stringify({
type: "app:external:v1",
url: "https://docs.example.org/guide",
}),
);Other JSON messages are ignored.
Challenge detection is disabled by default because status and text markers can produce false positives. Enable the built-in status and marker lists with:
<ManagedWebView uri="https://example.com/app" challengeDetection />Or replace either list:
<ManagedWebView
uri="https://example.com/app"
challengeDetection={{
statuses: [403, 429, 503],
markers: ["verify you are human", "/challenge/"],
}}
/>An empty custom array disables that detection source. Document detection reads the title and at most the first 2,000 visible-text characters on the device. It posts only a private detection signal; it does not send page content to the application or a remote service.
The default interface uses React Native primitives, neutral light and dark palettes, accessible labels, and 44-point action targets. Every label and color token can be replaced. Full renderers can also be supplied:
import { Pressable, Text, View } from "react-native";
import { ManagedWebView } from "@troco/managed-webview";
<ManagedWebView
uri="https://example.com/app"
labels={{ loading: "Opening secure content..." }}
renderLoading={({ labels }) => <Text>{labels.loading}</Text>}
renderError={({ state, labels, retry, openExternal }) => (
<View>
<Text accessibilityRole="header">{labels.errorTitle}</Text>
<Text>{state.error.kind}</Text>
{retry ? (
<Pressable accessibilityRole="button" onPress={retry}>
<Text>{labels.retry}</Text>
</Pressable>
) : null}
{openExternal ? (
<Pressable
accessibilityRole="button"
onPress={() => {
void openExternal();
}}
>
<Text>{labels.openExternal}</Text>
</Pressable>
) : null}
</View>
)}
/>;Custom loading UI must block interaction while the document is not ready. Custom recovery UI should expose a heading and preserve a minimum 44-point touch target for actions.
The WebView is unmounted after timeout, main-document network or HTTP failure, unsafe navigation, external navigation, process termination, or detected challenge. Runtime failures expose retry and a validated external target. Configuration failures expose neither action because the props must change.
The public callbacks are:
onReadyonErroronStateChangeonNavigationDecisiononOpenExternal
The package also exports the prop, state, error, readiness, navigation, appearance, label, challenge, and renderer-context TypeScript types.
Pass non-owned native options through webViewProps:
<ManagedWebView
uri="https://example.com/app"
webViewProps={{
javaScriptEnabled: true,
allowsInlineMediaPlayback: true,
}}
/>Source, navigation, message, failure, process, injection, interaction, and
accessibility handlers remain package-owned and cannot be overridden through
webViewProps.
npm install @troco/managed-webview
npx expo install react-native-webviewThe repository's example/ app demonstrates load readiness, exact message
readiness, restricted paths, external recovery, custom renderers, and retry.
It requires network access.
npm install @troco/managed-webview react-native-webview
npx pod-installFollow the react-native-webview installation guidance for any
platform-specific configuration required by your application.
This package enforces explicit navigation and lifecycle policies. It cannot make arbitrary third-party content fully secure. Consumers remain responsible for the content they embed, authentication design, transport security, native platform configuration, and any permissions or data exposed to the page.
Do not put secrets in URLs, injected scripts, page messages, issue reports, or logs. See SECURITY.md for private vulnerability reporting.
nvm use
npm ci
npm run checkSee CONTRIBUTING.md before opening a pull request.
MIT © 2026 Troco