{"id":51016042,"url":"https://github.com/l2hyunwoo/nitro-webview","last_synced_at":"2026-06-21T10:31:27.373Z","repository":{"id":361570439,"uuid":"1254806181","full_name":"l2hyunwoo/nitro-webview","owner":"l2hyunwoo","description":"Nitro + WebView = 🚀","archived":false,"fork":false,"pushed_at":"2026-06-01T16:09:37.000Z","size":1936,"stargazers_count":23,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T05:33:10.446Z","etag":null,"topics":["nitro-modules","react-native","webview"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/l2hyunwoo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T02:57:33.000Z","updated_at":"2026-06-08T12:06:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/l2hyunwoo/nitro-webview","commit_stats":null,"previous_names":["l2hyunwoo/nitro-webview"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/l2hyunwoo/nitro-webview","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l2hyunwoo%2Fnitro-webview","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l2hyunwoo%2Fnitro-webview/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l2hyunwoo%2Fnitro-webview/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l2hyunwoo%2Fnitro-webview/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/l2hyunwoo","download_url":"https://codeload.github.com/l2hyunwoo/nitro-webview/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/l2hyunwoo%2Fnitro-webview/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34607126,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nitro-modules","react-native","webview"],"created_at":"2026-06-21T10:31:23.552Z","updated_at":"2026-06-21T10:31:27.368Z","avatar_url":"https://github.com/l2hyunwoo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nitro-webview\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd align=\"center\"\u003e\u003cb\u003eiOS\u003c/b\u003e\u003c/td\u003e\n    \u003ctd align=\"center\"\u003e\u003cb\u003eAndroid\u003c/b\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cvideo src=\"https://github.com/user-attachments/assets/4ae45afd-b595-4efd-8e44-25c1d03434a8\" width=\"360\" autoplay loop muted playsinline /\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cvideo src=\"https://github.com/user-attachments/assets/9431b353-24a1-41f5-9aee-1fea0520e13d\" width=\"360\" autoplay loop muted playsinline /\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nA React Native WebView built on [Nitro Modules][nitro] — pure Swift / Kotlin native sides, JSI-direct prop and event dispatch, no bridge round-trips.\n\n## Introduction\n\n`nitro-webview` is a drop-in WebView component for React Native that replaces the legacy bridge with [Nitro Modules][nitro]'s JSI-direct dispatch. It targets two audiences:\n\n- **Experienced RN + Nitro developers** who want a WebView that participates in the Nitro view contract — `getHostComponent`, hybrid refs, `callback(...)` event handlers, `Promise\u003cT\u003e` method results — without paying for JSON serialization or thread-hops on every prop update or event.\n- **Teams evaluating WebView libraries** (\"comparison shoppers\") who already use `react-native-webview` and want to know what they keep, what changes, and what improves before they switch.\n\n### What you keep coming from `react-native-webview`\n\n- Same conceptual props (`source`, `userAgent`, `injectedJavaScript`, `onLoadStart` / `onLoadEnd`, `onMessage`, `onError`, `onShouldStartLoadWithRequest`, `onFileDownload`).\n- Same `window.ReactNativeWebView.postMessage(...)` page-side contract.\n- Same `originWhitelist`-style default (`['http://*', 'https://*']`) exposed as `DEFAULT_ORIGIN_WHITELIST`.\n- Same `WebViewNavigationType` string union (`'click' | 'formsubmit' | 'backforward' | 'reload' | 'formresubmit' | 'other'`) so existing call-sites compile unchanged.\n\n### What changes\n\n- Event props must be wrapped in `callback(...)` from `react-native-nitro-modules` so Nitro can dispatch them on the right thread.\n- `onShouldStartLoadWithRequest` returns `Promise\u003cboolean\u003e` directly — no `lockIdentifier` round-trip. `async` callbacks are awaited transparently.\n- Imperative methods (`goBack`, `evaluateJavaScript`, `getCookies`, `setCookie`, `clearCookies`, …) live on the **hybrid ref** captured via the `hybridRef` prop, not on a React `ref`.\n- Native packages: `io.github.l2hyunwoo.nitrowebview` (Android) / `NitroWebView` Swift module (iOS). MIT-licensed, npm-published as `nitro-webview` (unscoped).\n\n### Why Nitro\n\nNitro Modules pipes props, methods, and event callbacks through JSI so a load event or a cookie read does not round-trip through `NativeEventEmitter` or the bridge's serialization queue. For a WebView — which is event-heavy (navigation, messages, errors, downloads) — that is the main practical win.\n\n## Quick Start\n\n### 1. Install\n\n```sh\nyarn add nitro-webview react-native-nitro-modules\ncd ios \u0026\u0026 pod install\n```\n\n`react-native-nitro-modules` is a peer dependency — install it explicitly so your dependency graph stays deterministic.\n\n### 2. Render a WebView\n\n```tsx\nimport { NitroWebView, callback } from 'nitro-webview'\n\nexport default function Screen() {\n  return (\n    \u003cNitroWebView\n      style={{ flex: 1 }}\n      source={{ uri: 'https://example.com' }}\n      onLoadEnd={callback(() =\u003e console.log('loaded'))}\n    /\u003e\n  )\n}\n```\n\nEvery event prop must be wrapped in `callback(...)` so Nitro can dispatch it on the right thread. Passing a raw function will throw at render time.\n\n### 3. Call imperative methods\n\n```tsx\nimport { useRef } from 'react'\nimport { NitroWebView, callback, type NitroWebViewType } from 'nitro-webview'\n\nexport default function Screen() {\n  const ref = useRef\u003cNitroWebViewType | null\u003e(null)\n\n  return (\n    \u003c\u003e\n      \u003cNitroWebView\n        style={{ flex: 1 }}\n        source={{ uri: 'https://example.com' }}\n        hybridRef={callback((r) =\u003e {\n          ref.current = r\n        })}\n      /\u003e\n      \u003cButton title=\"reload\" onPress={() =\u003e ref.current?.reload()} /\u003e\n    \u003c/\u003e\n  )\n}\n```\n\n### 4. Configure platform setup\n\niOS and Android both need a small amount of host-app configuration for file upload and download to work — see [Platform setup](#platform-setup) below.\n\n## API Reference\n\n### `NitroWebView` component\n\nThe exported React component. Backed by `getHostComponent\u003cNitroWebViewProps, NitroWebViewMethods\u003e('NitroWebView', () =\u003e NitroWebViewConfig)`.\n\n#### Props\n\n| Prop | Type | Notes |\n| --- | --- | --- |\n| `source` | `WebViewSource` | `{ uri, headers? }` or `{ html, baseUrl? }`. Drives navigation. Required. |\n| `defaultHeaders` | `Record\u003cstring, string\u003e` | Global HTTP headers attached to every main-frame navigation request. Per-request `source.headers` win on key conflict. |\n| `userAgent` | `string` | Overrides the platform default UA for every request (main-frame + sub-resource). `undefined` / empty restores the WebKit / Chromium default. |\n| `injectedJavaScript` | `string` | Fire-and-forget script run on every page load. |\n| `onLoadStart` | `(event: WebViewLoadEvent) =\u003e void` | Fired when the WebView begins loading content. |\n| `onLoadEnd` | `(event: WebViewLoadEvent) =\u003e void` | Fired when the WebView finishes loading content. |\n| `onNavigationStateChange` | `(state: WebViewNavigationState) =\u003e void` | URL / title / `canGoBack` / `canGoForward` / `loading`. |\n| `onMessage` | `(event: WebViewMessageEvent) =\u003e void` | Fires when the page calls `window.ReactNativeWebView.postMessage(...)`. |\n| `onError` | `(event: NitroWebViewErrorEvent) =\u003e void` | Navigation failure (network, SSL). |\n| `onFileDownload` | `(event: FileDownloadEvent) =\u003e void` | Native intercepts a download and surfaces `{ url, mimeType?, fileName?, contentLength?, userAgent? }`. Storage is the JS layer's responsibility. |\n| `onShouldStartLoadWithRequest` | `(event: ShouldStartLoadRequest) =\u003e boolean \\| Promise\u003cboolean\u003e` | Allow/block each navigation before it starts. Returning `false` (or a `Promise` resolving to `false`) cancels silently. |\n\n#### Methods (via `hybridRef`)\n\nThe hybrid ref captured by `hybridRef={callback((r) =\u003e ref.current = r)}` exposes:\n\n| Method | Return | Notes |\n| --- | --- | --- |\n| `goBack()` | `void` | Navigate back in history. |\n| `goForward()` | `void` | Navigate forward in history. |\n| `reload()` | `void` | Reload the current page. |\n| `stopLoading()` | `void` | Stop the current load. |\n| `evaluateJavaScript(code)` | `Promise\u003cstring\u003e` | Result is the serialized string evaluation. iOS uses `String(describing:)`; Android uses the JSON-encoded `ValueCallback\u003cString\u003e` result. Undefined/nil surfaces as `''`. |\n| `getCookies(url)` | `Promise\u003cCookie[]\u003e` | iOS returns the full attribute set. Android `CookieManager` only exposes `name` and `value` on read — other fields are left `undefined`. |\n| `setCookie(url, cookie)` | `Promise\u003cvoid\u003e` | `Cookie = { name, value, domain?, path?, expires?, secure?, httpOnly? }`. `expires` is milliseconds since epoch (`Date.now()`-compatible). |\n| `clearCookies()` | `Promise\u003cvoid\u003e` | Bulk clear via `WKWebsiteDataStore` (iOS) / `CookieManager.removeAllCookies` (Android). The promise resolves only after the platform reports completion. |\n\n### Types\n\n#### `WebViewSource`\n\n```ts\ntype WebViewSource = UriSource | HtmlSource\n\ninterface UriSource {\n  uri: string\n  headers?: Record\u003cstring, string\u003e\n}\n\ninterface HtmlSource {\n  html: string\n  baseUrl?: string\n}\n```\n\n`UriSource.headers` are per-request HTTP headers attached only to the main-frame navigation a `source` change triggers. Redirects, sub-frames, and sub-resource requests do not re-apply them.\n\n#### `ShouldStartLoadRequest`\n\n```ts\ninterface ShouldStartLoadRequest {\n  url: string\n  navigationType: WebViewNavigationType\n  mainDocumentURL?: string   // iOS only\n  isTopFrame?: boolean       // iOS only\n  hasTargetFrame?: boolean   // iOS only — false for target=_blank\n}\n\ntype WebViewNavigationType =\n  | 'click' | 'formsubmit' | 'backforward'\n  | 'reload' | 'formresubmit' | 'other'\n```\n\nAndroid leaves the three optional fields `undefined` because `WebViewClient.shouldOverrideUrlLoading` does not expose them, and always reports `navigationType: 'other'`.\n\nThe JS callback may be `async` — the bridge transparently awaits any returned thenable before applying the decision.\n\n#### `WebViewNavigationState` \u0026 `WebViewLoadEvent`\n\n```ts\ninterface WebViewNavigationState {\n  url: string\n  title: string\n  loading: boolean\n  canGoBack: boolean\n  canGoForward: boolean\n}\n\ninterface WebViewLoadEvent {\n  nativeEvent: WebViewNavigationState\n}\n```\n\n#### `WebViewMessageEvent`\n\n```ts\ninterface WebViewMessageNativeEvent {\n  data: string  // literal string from window.ReactNativeWebView.postMessage(...)\n  url: string\n}\n\ninterface WebViewMessageEvent {\n  nativeEvent: WebViewMessageNativeEvent\n}\n```\n\n#### `NitroWebViewErrorEvent`\n\n```ts\ninterface NitroWebViewErrorNativeEvent {\n  code: number         // NSError.code (iOS) / WebResourceError.getErrorCode() (Android)\n  description: string  // localizedDescription (iOS) / getDescription().toString() (Android)\n  url: string          // empty string when neither delegate nor error provided one\n  domain: string       // NSError.domain (iOS) / stable string mirror (Android)\n}\n\ninterface NitroWebViewErrorEvent {\n  nativeEvent: NitroWebViewErrorNativeEvent\n}\n\ntype WebViewErrorEvent = NitroWebViewErrorEvent  // alias\n```\n\n#### `Cookie`\n\n```ts\ninterface Cookie {\n  name: string\n  value: string\n  domain?: string        // platform-derived from url when omitted\n  path?: string          // defaults to '/'\n  expires?: number       // ms since Unix epoch; omit for a session cookie\n  secure?: boolean       // restrict to HTTPS\n  httpOnly?: boolean     // hide from document.cookie\n}\n```\n\n#### `FileDownload` \u0026 `FileDownloadEvent`\n\n```ts\ninterface FileDownload {\n  url: string             // always http/https — blob: URLs are out of scope\n  mimeType?: string\n  fileName?: string       // iOS: URLResponse.suggestedFilename\n                          // Android: DownloadUtils.guessFileName (Content-Disposition)\n  contentLength?: number  // -1 or absent when the platform did not supply a length\n  userAgent?: string      // typically absent on iOS\n}\n\ninterface FileDownloadEvent {\n  nativeEvent: FileDownload\n}\n```\n\n### Origin whitelist helpers\n\nPure-TS helpers for building allowlist-style policies on top of `onShouldStartLoadWithRequest`. They do **not** depend on React Native or Nitro at runtime, so they can be unit-tested in isolation.\n\n```ts\nimport {\n  DEFAULT_ORIGIN_WHITELIST,\n  createOriginWhitelistGuard,\n  originMatches,\n  wrapWithOriginWhitelist,\n} from 'nitro-webview'\nimport type {\n  OnShouldStartLoadWithRequest,\n  OriginWhitelistGuard,\n} from 'nitro-webview'\n```\n\n| Export | Signature | Notes |\n| --- | --- | --- |\n| `DEFAULT_ORIGIN_WHITELIST` | `readonly ['http://*', 'https://*']` | Frozen. Mirrors `react-native-webview`'s documented default. |\n| `originMatches(url, patterns)` | `(string, readonly string[]) =\u003e boolean` | Returns `true` iff the **origin** (`scheme://host[:port]`) of `url` matches one of the glob `patterns`. `*` is the only wildcard. Case-insensitive on scheme + host. Empty pattern list returns `false`. Unparseable URL returns `false`. |\n| `createOriginWhitelistGuard(patterns?, inner?)` | `(readonly string[], OnShouldStartLoadWithRequest?) =\u003e OriginWhitelistGuard` | Builds a guard that rejects non-matching origins immediately and delegates matching ones to `inner` (or allows them when `inner` is absent). |\n| `wrapWithOriginWhitelist(handler, patterns?)` | `(OnShouldStartLoadWithRequest, readonly string[]?) =\u003e OnShouldStartLoadWithRequest` | Fast-path wrapper: when `patterns === DEFAULT_ORIGIN_WHITELIST` (by reference), the returned guard short-circuits `true` and `handler` is never invoked. Otherwise delegates straight to `handler(event)`. |\n\n```ts\nimport { wrapWithOriginWhitelist, DEFAULT_ORIGIN_WHITELIST } from 'nitro-webview'\n\nconst handler = wrapWithOriginWhitelist(\n  (event) =\u003e !event.url.startsWith('https://example.org/'),\n  DEFAULT_ORIGIN_WHITELIST,\n)\n```\n\n### Source helpers\n\n```ts\nimport {\n  isHtmlSource,\n  isUriSource,\n  normalizeHtmlSource,\n  sourceToCommand,\n} from 'nitro-webview'\n```\n\n| Export | Signature | Notes |\n| --- | --- | --- |\n| `isUriSource(source)` | `(WebViewSource) =\u003e source is UriSource` | Structural narrowing on a non-empty `uri` string. |\n| `isHtmlSource(source)` | `(WebViewSource) =\u003e source is HtmlSource` | Structural narrowing on a string `html` field. |\n| `normalizeHtmlSource(source)` | `(WebViewSource) =\u003e LoadHtmlCommand \\| null` | Returns a `loadHtml` native command, or `null` when `source` is not an `HtmlSource`. |\n| `sourceToCommand(source)` | `(WebViewSource) =\u003e NativeViewCommand` | Maps the `source` prop to the native view command (`loadUrl` or `loadHtml`). Throws `TypeError` on malformed input. |\n\n### Event dispatchers\n\nLower-level builders used by `NitroWebView` internally. Exported for advanced consumers building custom event pipelines (e.g. for tests or mocks).\n\n| Export | Signature |\n| --- | --- |\n| `createLoadStartDispatcher(onLoadStart?)` | `(OnLoadStart \\| undefined) =\u003e LoadStartDispatcher` |\n| `createLoadDispatcher(onLoad?)` | `(OnLoad \\| undefined) =\u003e LoadDispatcher` |\n| `createLoadEndDispatcher(onLoadEnd?)` | `(OnLoadEnd \\| undefined) =\u003e LoadEndDispatcher` |\n\nEach dispatcher dedupes by `navigationId` so duplicate native fires never reach JS.\n\n### Bridge script\n\nThe injected `window.ReactNativeWebView.postMessage(...)` shim is built in pure TS so it can be unit-tested and shared across platforms.\n\n```ts\nimport {\n  ANDROID_NATIVE_BRIDGE_NAME,\n  BRIDGE_NAME,\n  buildBridgeScript,\n  evaluateBridgeScript,\n} from 'nitro-webview'\n```\n\n| Export | Notes |\n| --- | --- |\n| `BRIDGE_NAME` | `'ReactNativeWebView'`. Public identifier installed on `window`. |\n| `ANDROID_NATIVE_BRIDGE_NAME` | `'ReactNativeWebViewNative'`. Internal Android `JavascriptInterface` name. |\n| `buildBridgeScript(platform)` | Returns the literal JavaScript source string for the injected bridge. Idempotent — never overwrites a page-defined `postMessage`. |\n| `evaluateBridgeScript(platform, sandbox)` | Evaluates the script against an in-memory `sandbox` (used for tests). |\n\n### `callback` re-export\n\n```ts\nimport { callback } from 'nitro-webview'\n```\n\nRe-exported verbatim from `react-native-nitro-modules`. Every event prop (`onLoadStart`, `onLoadEnd`, `onMessage`, `onError`, `onShouldStartLoadWithRequest`, `onFileDownload`, `hybridRef`) must pass through this wrapper.\n\n## Platform setup\n\n### iOS file upload setup\n\nThe system file picker on iOS reads from the camera, the photo library, and (for video capture) the microphone. iOS crashes the app the first time the picker accesses one of these subsystems without an explanatory string. Add all three usage descriptions to your app's `Info.plist` even if your web content only triggers one of them — iOS may surface the unified picker:\n\n```xml\n\u003ckey\u003eNSCameraUsageDescription\u003c/key\u003e\n\u003cstring\u003eThis app uses the camera to let you upload photos and videos from web pages.\u003c/string\u003e\n\u003ckey\u003eNSPhotoLibraryUsageDescription\u003c/key\u003e\n\u003cstring\u003eThis app needs photo library access to let you upload images from web pages.\u003c/string\u003e\n\u003ckey\u003eNSMicrophoneUsageDescription\u003c/key\u003e\n\u003cstring\u003eThis app uses the microphone to record audio when you upload a video from a web page.\u003c/string\u003e\n```\n\nThe strings are shown verbatim in the iOS permission prompt — rewrite them in your app's voice and supported locales.\n\n### Android file upload setup\n\nThe library ships its own FileProvider declaration with authority `${applicationId}.nitrowebview.fileprovider`. The consuming app must still declare the media permissions in its `AndroidManifest.xml` for the file chooser to surface photos / videos / camera capture:\n\n```xml\n\u003cuses-permission android:name=\"android.permission.CAMERA\" /\u003e\n\u003cuses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" /\u003e\n\u003cuses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" /\u003e\n```\n\nThe library also pulls `org.mozilla.components:support-utils` for its Content-Disposition–aware `DownloadUtils.guessFileName` — the consuming app must expose Mozilla's Maven repository in its `android/build.gradle`:\n\n```groovy\nallprojects {\n  repositories {\n    maven { url \"https://maven.mozilla.org/maven2\" }\n  }\n}\n```\n\n## License\n\nMIT.\n\n[nitro]: https://github.com/mrousavy/nitro\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fl2hyunwoo%2Fnitro-webview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fl2hyunwoo%2Fnitro-webview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fl2hyunwoo%2Fnitro-webview/lists"}