A Fabric-native WebView for controlled web flows: authentication, checkout, OAuth redirects, and 3-D Secure challenges.
Instead of a general purpose browser view, this library gives you a WebView that denies by default. Every top-level navigation is validated natively against an explicit origin allowlist, custom scheme redirects are intercepted and surfaced as events instead of being loaded, and the web-to-native bridge is a single string-only method.
- New Architecture only. A real Fabric component (
WKWebViewon iOS,android.webkit.WebViewon Android), not a wrapper aroundreact-native-webview. - Exact origin matching. Scheme, host, and effective port. No wildcards, no substring matching, no "starts with".
- Native enforcement. Server redirects never reach JS, so the policy runs in native code on both platforms.
- Fail closed. Invalid configuration entries are dropped, unknown URLs are blocked, and unsupported features raise errors instead of silently degrading.
See SECURITY.md for the threat model and what this library does not protect against, and docs/FABRIC_ARCHITECTURE.md for a code-level tour of the component.
- React Native 0.85+ with the New Architecture enabled (Fabric). There is no Paper fallback.
- iOS 15.1+, Android minSdk 24.
npm install react-native-secure-webview
cd ios && pod installNo manual native setup is required. The Fabric component is registered through Codegen on both platforms.
The package ships untranspiled ESM. If your tests import components that use it, allow it through Jest's transform in your jest.config.js:
transformIgnorePatterns: [
'node_modules/(?!(?:react-native|@react-native|react-native-secure-webview)/)',
],import { useRef } from 'react';
import {
SecureWebView,
type SecureWebViewRef,
} from 'react-native-secure-webview';
function Checkout() {
const webView = useRef<SecureWebViewRef>(null);
return (
<SecureWebView
ref={webView}
style={{ flex: 1 }}
source={{ uri: 'https://checkout.example.com/pay' }}
allowedOrigins={[
'https://checkout.example.com',
'https://3ds.acs-provider.com',
]}
allowedSchemes={['myapp']}
onDeepLink={({ url }) => finishCheckout(url)}
onMessage={({ data }) => console.log('page said:', data)}
onSecurityViolation={({ type, url }) => report(type, url)}
onError={({ code, message }) => showRetry(code, message)}
/>
);
}| Prop | Type | Description |
|---|---|---|
source |
{ uri: string } |
The http(s) URL to load. Its origin must be covered by allowedOrigins, or nothing loads. |
allowedOrigins |
string[] |
Exact origins allowed for top-level navigation, e.g. "https://checkout.example.com" or "https://api.example.com:8443". No paths, no wildcards. Invalid entries are dropped with a dev warning. |
allowedSchemes |
string[]? |
Custom schemes (e.g. "myapp") that are intercepted and emitted as onDeepLink events. http, https, javascript, data, file, intent, and other browser-internal schemes are never allowed here. |
session |
'persistent' | 'ephemeral' |
Cookie and storage behavior. Default 'persistent'. See sessions. |
onNavigation |
(e) => void |
Navigation state changed: { url, loading, canGoBack, canGoForward }. |
onDeepLink |
(e) => void |
An allow-listed custom scheme navigation was intercepted: { url, scheme }. The URL is not loaded. |
onMessage |
(e) => void |
The page called window.ReactNativeSecureWebView.postMessage(string): { data }. Strings only, main frame only, allow-listed origins only. |
onError |
(e) => void |
A load failed or the component degraded: { code, message, url? }. Codes are platform-prefixed (ios_-1009, android_-2), renderer-crash codes (android_render_process_gone, ios_content_process_terminated — recover with reload()), or library-level (ephemeral_not_supported, message_bridge_not_supported). |
onSecurityViolation |
(e) => void |
A navigation was blocked: { type, url } where type is origin_not_allowed, scheme_not_allowed, or invalid_url. |
reload(), goBack(), goForward(), stopLoading(). Calls after unmount are no-ops with a dev warning.
Every top-level navigation, including server redirects, is decided natively:
| Navigation | Result |
|---|---|
| http(s) URL with allow-listed origin | Loaded |
| http(s) URL with any other origin | Blocked, onSecurityViolation (origin_not_allowed) |
| Allow-listed custom scheme | Blocked, onDeepLink |
| Unknown scheme | Blocked, onSecurityViolation (scheme_not_allowed) |
| Unparseable or hostile URL | Blocked, onSecurityViolation (invalid_url) |
Origins match exactly after canonicalization (lowercase scheme and host, explicit effective port). https://checkout.example.com matches https://checkout.example.com:443/anything but not https://sub.checkout.example.com, http://checkout.example.com, or https://checkout.example.com:8443.
- iOS:
'persistent'uses the defaultWKWebsiteDataStore.'ephemeral'uses a non-persistent store, so cookies and storage exist only for the lifetime of the web view. - Android: cookies live in the process-wide
CookieManagerand cannot be isolated per WebView. Rather than fake isolation,session="ephemeral"fails closed on Android: nothing loads andonErrorfires withephemeral_not_supported.
Pages loaded from an allow-listed origin can call:
window.ReactNativeSecureWebView.postMessage('a string');That is the entire bridge. No RN-to-web messaging, no JS injection, no typed RPC.
Messages are accepted only from the main frame and only from allow-listed origins, enforced natively on both platforms: iOS validates the sender frame and origin in the WKScriptMessageHandler; Android injects the bridge object exclusively into allow-listed main-frame origins via WebViewCompat.addWebMessageListener origin rules (and re-validates each message). On the rare Android system WebView too old for WEB_MESSAGE_LISTENER, the bridge is not installed at all — fail closed — and onError fires once with message_bridge_not_supported.
example/ contains a demo app with one preset per behavior (allowed browsing, blocked origin, deep links and messaging, ephemeral session, load errors). The deep-link and messaging preset uses a local demo page:
cd example
yarn demo-page # serves demo-page/ on http://localhost:8087
yarn ios # or: yarn androidJS injection, local files and HTML strings, downloads, popups and window.open, media, camera, and geolocation permissions, RN-to-web messaging, cookie APIs, and OAuth or payment SDK integration. Feature requests are welcome as issues, but each is weighed against the security posture above.
See the contributing guide and code of conduct. To report a vulnerability, see SECURITY.md.
MIT