Skip to content

Repository files navigation

@troco/managed-webview

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.

Installation

npm install @troco/managed-webview react-native-webview

This package contains no native code of its own. Your application still needs to install and configure react-native-webview.

Compatibility

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

Minimal usage

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.

Exact message readiness

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, paths, and external navigation

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

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.

Custom loading and recovery UI

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.

Lifecycle and callbacks

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:

  • onReady
  • onError
  • onStateChange
  • onNavigationDecision
  • onOpenExternal

The package also exports the prop, state, error, readiness, navigation, appearance, label, challenge, and renderer-context TypeScript types.

Native WebView props

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.

Expo SDK 57

npm install @troco/managed-webview
npx expo install react-native-webview

The repository's example/ app demonstrates load readiness, exact message readiness, restricted paths, external recovery, custom renderers, and retry. It requires network access.

Bare React Native

npm install @troco/managed-webview react-native-webview
npx pod-install

Follow the react-native-webview installation guidance for any platform-specific configuration required by your application.

Security boundary

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.

Development

nvm use
npm ci
npm run check

See CONTRIBUTING.md before opening a pull request.

License

MIT © 2026 Troco

About

Managed React Native WebView lifecycle and navigation policies

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages