@rodrigocoliveira/mobile-app-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodrigo Casagrande
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @rodrigocoliveira/mobile-app-bridge
2
+
3
+ Build features once in your web app, and let the Expo WebView shell that loads it do the native parts.
4
+
5
+ This package is a small, promise-based bridge between a web page and the Expo app that renders it in a `react-native-webview`. The page calls `await bridge.call('camera.requestPermission')`, and the app runs the native handler you registered and answers.
6
+
7
+ It is a cleaned-up version of the bridge that ran in production in the Agendart app. It uses the same `postMessage` / `injectJavaScript` / `CustomEvent` mechanics, and adds request/response correlation, strict host matching and a fix for Android's injection race.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @rodrigocoliveira/mobile-app-bridge
13
+ ```
14
+
15
+ The web side has no dependencies. The Expo app also needs:
16
+
17
+ ```bash
18
+ npx expo install react-native-webview expo-constants
19
+ ```
20
+
21
+ ## Web usage (`/web`)
22
+
23
+ ```ts
24
+ import { bridge, BridgeError } from '@rodrigocoliveira/mobile-app-bridge/web'
25
+
26
+ bridge.isApp // true inside the app (window.ReactNativeWebView exists)
27
+ bridge.info // { platform: 'ios' | 'android', appVersion, buildNumber }, or null if the badge is missing
28
+
29
+ const { granted } = await bridge.call<{ granted: boolean }>('camera.requestPermission')
30
+ await bridge.call('billing.purchase', { productId: 'pro' }, { timeout: 120_000 }) // default 30s; 0 disables
31
+
32
+ const off = bridge.on('push.opened', (data) => { /* ... */ })
33
+ off()
34
+ ```
35
+
36
+ - **SSR-safe:** importing the module never touches `window`.
37
+ - **Events are fire-and-forget.** `bridge.on` only receives events that arrive while it is subscribed.
38
+ - **Pull, don't push.** Anything that must not be lost is a method the page calls when it is ready. For example, the notification that opened the app:
39
+
40
+ ```ts
41
+ // app: store it at startup, expose it as a handler
42
+ handlers={{ 'push.getInitial': async () => initialNotification }}
43
+ // page: ask on boot (and again after any reload)
44
+ const notification = await bridge.call('push.getInitial')
45
+ ```
46
+
47
+ `call()` rejects with a `BridgeError` whose `code` is one of the following:
48
+
49
+ | Code | When |
50
+ |---|---|
51
+ | `NOT_IN_APP` | Called outside the app. It rejects immediately and nothing is sent. |
52
+ | `TIMEOUT` | No response within `timeout` |
53
+ | `UNKNOWN_METHOD` | This app build has no handler for the method |
54
+ | `HANDLER_ERROR` | The handler threw without a `code`, or returned something that is not JSON-serializable |
55
+ | `INVALID_PARAMS` | `params` cannot be JSON-serialized. It rejects immediately and nothing is sent. |
56
+ | *anything else* | The `code` your handler threw, for example `throw Object.assign(new Error('...'), { code: 'CANCELLED' })` |
57
+
58
+ ## Native usage (`/native`)
59
+
60
+ ```tsx
61
+ import { BridgeWebView, useBridgeEmitter } from '@rodrigocoliveira/mobile-app-bridge/native'
62
+
63
+ export default function Main() {
64
+ const emitter = useBridgeEmitter() // optional; only needed to send events to the page
65
+
66
+ return (
67
+ <BridgeWebView
68
+ source={{ uri: 'https://app.brand.com' }}
69
+ emitter={emitter}
70
+ trustedHosts={['app.brand.com', '*.brand.com']} // load in the app AND may call handlers
71
+ inAppHosts={['*.amazonaws.com', 'accounts.google.com']} // load in the app, NO bridge access
72
+ handlers={{
73
+ 'camera.requestPermission': async () => ({ granted: true }),
74
+ 'haptics.impact': async ({ style }) => { /* ... */ },
75
+ }}
76
+ />
77
+ )
78
+ }
79
+
80
+ // anywhere: emitter.emit('app.stateChange', { state }) // sent now; dropped if no page is there
81
+ ```
82
+
83
+ `BridgeWebView` accepts every `WebView` prop. It never silently overrides a prop you pass:
84
+
85
+ | Prop | Behavior |
86
+ |---|---|
87
+ | `onMessage` | Bridge messages are consumed. **Every other message is forwarded to your `onMessage`.** |
88
+ | `onShouldStartLoadWithRequest` | Yours runs first. Return `true`/`false` to decide, or `undefined` to use the default policy. |
89
+ | `injectedJavaScriptBeforeContentLoaded` | The bridge badge first, then your script |
90
+ | `injectedJavaScript` | The badge again (idempotent), then your script |
91
+ | `injectedJavaScriptForMainFrameOnly` | Always `true`. It cannot be overridden. |
92
+ | `ref` | Forwarded to the underlying `WebView` (`reload`, `goBack`, …) |
93
+
94
+ ## Security and navigation
95
+
96
+ - **Hosts are matched exactly on the hostname.** `*.brand.com` matches subdomains, but not `brand.com` itself and not `evilbrand.com`. URL tricks such as `https://app.brand.com@evil.io` or `https://evil.io\@app.brand.com` resolve to the real host.
97
+ - **Only `trustedHosts` can call handlers.** Messages from any other origin are dropped (with a warning in `__DEV__`).
98
+ - **Only `trustedHosts` pages receive responses and events.** Every native-to-page delivery re-checks `location.hostname` in-page, so an `inAppHosts` or third-party page never sees push payloads.
99
+ - **Default navigation policy:**
100
+ 1. An http(s) URL on a `trustedHosts` or `inAppHosts` host loads in the app.
101
+ 2. Any http(s) **iframe** loads in place. This keeps reCAPTCHA, maps, GTM and payment widgets working.
102
+ 3. `about:`, `blob:`, `file:`, `javascript:` and `data:` in the top frame are blocked.
103
+ 4. Everything else opens outside the app (`Linking.openURL`), including other hosts, `tel:`, `mailto:` and `whatsapp:`.
104
+ - **Scripts, `fetch` and images are never affected.** Google Analytics and similar tools need no allowlisting.
105
+ - **Iframes.**
106
+ - On iOS, iframes do not get `window.ReactNativeWebView` at all.
107
+ - On Android they do, but their messages carry the iframe's own origin and are dropped unless that origin is trusted.
108
+ - **So never embed pages from a trusted host that you do not control.**
109
+ - **What `inAppHosts` pages can see.** They see `bridge.isApp === true`, because `window.ReactNativeWebView` exists in every page the WebView loads, but they cannot call handlers or receive events.
110
+
111
+ The results on both platforms are in [`docs/e2e-results.md`](docs/e2e-results.md).
112
+
113
+ ## Migrating from a legacy bridge
114
+
115
+ Messages without the bridge marker still reach your own `onMessage`, so you can migrate one event at a time:
116
+
117
+ ```tsx
118
+ <BridgeWebView
119
+ handlers={{ 'camera.requestPermission': requestCameraPermission }} // migrated
120
+ onMessage={(e) => { const { event_name } = JSON.parse(e.nativeEvent.data); /* legacy events */ }}
121
+ trustedHosts={['app.agendart.com.br']}
122
+ />
123
+ ```
124
+
125
+ ## Running the example
126
+
127
+ ```bash
128
+ bun install
129
+ bun run example:web # builds the package, serves example/web on :5055
130
+ ```
131
+
132
+ Then, in another terminal:
133
+
134
+ ```bash
135
+ cd example/app && npm install && npx expo start --ios # or --android (run `adb reverse tcp:5055 tcp:5055` first)
136
+ ```
137
+
138
+ The full recipe for running the scenarios on the iOS simulator and Android emulator (including how Claude Code drives them) is in [`docs/device-testing.md`](docs/device-testing.md). In Claude Code, `/device-e2e` runs it.
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,37 @@
1
+ // src/shared/protocol.ts
2
+ var PROTOCOL_MARKER = 1;
3
+ var BRIDGE_EVENT = "mobile-app-bridge";
4
+ var BADGE_KEY = "__MOBILE_APP_BRIDGE__";
5
+ var DEFAULT_TIMEOUT = 3e4;
6
+ var ErrorCode = {
7
+ NOT_IN_APP: "NOT_IN_APP",
8
+ TIMEOUT: "TIMEOUT",
9
+ UNKNOWN_METHOD: "UNKNOWN_METHOD",
10
+ HANDLER_ERROR: "HANDLER_ERROR",
11
+ INVALID_PARAMS: "INVALID_PARAMS"
12
+ };
13
+ function isBridgeMessage(value) {
14
+ if (typeof value !== "object" || value === null) return false;
15
+ const message = value;
16
+ if (message.bridge !== PROTOCOL_MARKER) return false;
17
+ switch (message.kind) {
18
+ case "req":
19
+ return typeof message.id === "string" && typeof message.method === "string";
20
+ case "res":
21
+ if (typeof message.id !== "string" || typeof message.ok !== "boolean") return false;
22
+ return message.ok || typeof message.error === "object" && message.error !== null;
23
+ case "evt":
24
+ return typeof message.name === "string";
25
+ default:
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export {
31
+ BRIDGE_EVENT,
32
+ BADGE_KEY,
33
+ DEFAULT_TIMEOUT,
34
+ ErrorCode,
35
+ isBridgeMessage
36
+ };
37
+ //# sourceMappingURL=chunk-SEQSUJLW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/protocol.ts"],"sourcesContent":["export const PROTOCOL_MARKER = 1 as const\nexport const BRIDGE_EVENT = 'mobile-app-bridge'\nexport const BADGE_KEY = '__MOBILE_APP_BRIDGE__'\nexport const DEFAULT_TIMEOUT = 30_000\n\nexport const ErrorCode = {\n NOT_IN_APP: 'NOT_IN_APP',\n TIMEOUT: 'TIMEOUT',\n UNKNOWN_METHOD: 'UNKNOWN_METHOD',\n HANDLER_ERROR: 'HANDLER_ERROR',\n INVALID_PARAMS: 'INVALID_PARAMS',\n} as const\n\nexport type Platform = 'ios' | 'android'\n\nexport interface AppInfo {\n platform: Platform\n appVersion: string\n buildNumber: string\n}\n\nexport interface RequestMessage {\n bridge: typeof PROTOCOL_MARKER\n kind: 'req'\n id: string\n method: string\n params?: unknown\n}\n\nexport interface ErrorPayload {\n code: string\n message: string\n}\n\nexport type ResponseMessage =\n | { bridge: typeof PROTOCOL_MARKER; kind: 'res'; id: string; ok: true; result?: unknown }\n | { bridge: typeof PROTOCOL_MARKER; kind: 'res'; id: string; ok: false; error: ErrorPayload }\n\nexport interface EventMessage {\n bridge: typeof PROTOCOL_MARKER\n kind: 'evt'\n name: string\n data?: unknown\n}\n\nexport type BridgeMessage = RequestMessage | ResponseMessage | EventMessage\n\nexport function isBridgeMessage(value: unknown): value is BridgeMessage {\n if (typeof value !== 'object' || value === null) return false\n const message = value as Record<string, unknown>\n if (message.bridge !== PROTOCOL_MARKER) return false\n\n switch (message.kind) {\n case 'req':\n return typeof message.id === 'string' && typeof message.method === 'string'\n case 'res':\n if (typeof message.id !== 'string' || typeof message.ok !== 'boolean') return false\n return message.ok || (typeof message.error === 'object' && message.error !== null)\n case 'evt':\n return typeof message.name === 'string'\n default:\n return false\n }\n}\n"],"mappings":";AAAO,IAAM,kBAAkB;AACxB,IAAM,eAAe;AACrB,IAAM,YAAY;AAClB,IAAM,kBAAkB;AAExB,IAAM,YAAY;AAAA,EACvB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,gBAAgB;AAClB;AAoCO,SAAS,gBAAgB,OAAwC;AACtE,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,UAAU;AAChB,MAAI,QAAQ,WAAW,gBAAiB,QAAO;AAE/C,UAAQ,QAAQ,MAAM;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,QAAQ,OAAO,YAAY,OAAO,QAAQ,WAAW;AAAA,IACrE,KAAK;AACH,UAAI,OAAO,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,UAAW,QAAO;AAC9E,aAAO,QAAQ,MAAO,OAAO,QAAQ,UAAU,YAAY,QAAQ,UAAU;AAAA,IAC/E,KAAK;AACH,aAAO,OAAO,QAAQ,SAAS;AAAA,IACjC;AACE,aAAO;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/native/index.ts
31
+ var native_exports = {};
32
+ __export(native_exports, {
33
+ BridgeWebView: () => BridgeWebView,
34
+ useBridgeEmitter: () => useBridgeEmitter
35
+ });
36
+ module.exports = __toCommonJS(native_exports);
37
+
38
+ // src/native/BridgeWebView.tsx
39
+ var import_react = require("react");
40
+ var import_react_native = require("react-native");
41
+ var import_expo_constants = __toESM(require("expo-constants"), 1);
42
+ var import_react_native_webview = require("react-native-webview");
43
+
44
+ // src/native/app-info.ts
45
+ function resolveAppInfo(platformOS, expoConfig, override = {}) {
46
+ const platform = platformOS === "android" ? "android" : "ios";
47
+ const buildNumber = platform === "ios" ? expoConfig?.ios?.buildNumber : expoConfig?.android?.versionCode?.toString();
48
+ return {
49
+ platform,
50
+ appVersion: expoConfig?.version ?? "0.0.0",
51
+ buildNumber: buildNumber ?? "0",
52
+ ...override
53
+ };
54
+ }
55
+
56
+ // src/native/hosts.ts
57
+ var URL_PATTERN = /^([a-z][a-z0-9+.-]*):(?:\/\/(?:[^@/\\?#]*@)?(\[[^\]]*\]|[^:/\\?#]*))?/i;
58
+ function schemeOf(url) {
59
+ const match = URL_PATTERN.exec(url.trim());
60
+ return match ? match[1].toLowerCase() : null;
61
+ }
62
+ function hostnameOf(url) {
63
+ const host = URL_PATTERN.exec(url.trim())?.[2];
64
+ if (!host) return null;
65
+ return host.toLowerCase().replace(/\.$/, "");
66
+ }
67
+ function isTrustedHostname(hostname, trustedHosts) {
68
+ const host = hostname.toLowerCase().replace(/\.$/, "");
69
+ return trustedHosts.some((entry) => {
70
+ const pattern = entry.toLowerCase();
71
+ if (pattern.startsWith("*.")) return host.endsWith(`.${pattern.slice(2)}`);
72
+ return host === pattern;
73
+ });
74
+ }
75
+ function isTrustedUrl(url, trustedHosts) {
76
+ const hostname = hostnameOf(url);
77
+ return hostname !== null && isTrustedHostname(hostname, trustedHosts);
78
+ }
79
+
80
+ // src/native/navigation.ts
81
+ var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["about", "blob", "file", "javascript", "data"]);
82
+ function decideNavigation(request, hosts) {
83
+ const scheme = schemeOf(request.url);
84
+ const isHttp = scheme === "http" || scheme === "https";
85
+ const isSubframe = request.isTopFrame === false;
86
+ if (isHttp && (isTrustedUrl(request.url, hosts.trustedHosts) || isTrustedUrl(request.url, hosts.inAppHosts))) {
87
+ return "load";
88
+ }
89
+ if (isSubframe && (isHttp || scheme === "about")) return "load";
90
+ if (scheme === null || BLOCKED_SCHEMES.has(scheme)) return "block";
91
+ return "open-external";
92
+ }
93
+ function composeNavigationHandler(consumer, getHosts, openExternal) {
94
+ return (request) => {
95
+ const custom = consumer?.(request);
96
+ if (typeof custom === "boolean") return custom;
97
+ const decision = decideNavigation(request, getHosts());
98
+ if (decision === "open-external") openExternal(request.url);
99
+ return decision === "load";
100
+ };
101
+ }
102
+
103
+ // src/shared/protocol.ts
104
+ var PROTOCOL_MARKER = 1;
105
+ var BRIDGE_EVENT = "mobile-app-bridge";
106
+ var BADGE_KEY = "__MOBILE_APP_BRIDGE__";
107
+ var ErrorCode = {
108
+ NOT_IN_APP: "NOT_IN_APP",
109
+ TIMEOUT: "TIMEOUT",
110
+ UNKNOWN_METHOD: "UNKNOWN_METHOD",
111
+ HANDLER_ERROR: "HANDLER_ERROR",
112
+ INVALID_PARAMS: "INVALID_PARAMS"
113
+ };
114
+ function isBridgeMessage(value) {
115
+ if (typeof value !== "object" || value === null) return false;
116
+ const message = value;
117
+ if (message.bridge !== PROTOCOL_MARKER) return false;
118
+ switch (message.kind) {
119
+ case "req":
120
+ return typeof message.id === "string" && typeof message.method === "string";
121
+ case "res":
122
+ if (typeof message.id !== "string" || typeof message.ok !== "boolean") return false;
123
+ return message.ok || typeof message.error === "object" && message.error !== null;
124
+ case "evt":
125
+ return typeof message.name === "string";
126
+ default:
127
+ return false;
128
+ }
129
+ }
130
+
131
+ // src/native/router.ts
132
+ function routeMessage(raw, sourceUrl, handlers, trustedHosts) {
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(raw);
136
+ } catch {
137
+ return { type: "forward" };
138
+ }
139
+ if (!isBridgeMessage(parsed)) return { type: "forward" };
140
+ if (parsed.kind !== "req") return { type: "drop", reason: "unexpected-kind" };
141
+ if (!isTrustedUrl(sourceUrl, trustedHosts)) return { type: "drop", reason: "untrusted-origin" };
142
+ return { type: "response", response: runHandler(parsed, handlers) };
143
+ }
144
+ async function runHandler(request, handlers) {
145
+ const handler = Object.prototype.hasOwnProperty.call(handlers, request.method) ? handlers[request.method] : void 0;
146
+ if (!handler) {
147
+ return failure(request.id, { code: ErrorCode.UNKNOWN_METHOD, message: `No handler registered for "${request.method}"` });
148
+ }
149
+ let result;
150
+ try {
151
+ result = await handler(request.params);
152
+ } catch (error) {
153
+ return failure(request.id, toErrorPayload(error));
154
+ }
155
+ try {
156
+ JSON.stringify(result);
157
+ } catch {
158
+ return failure(request.id, {
159
+ code: ErrorCode.HANDLER_ERROR,
160
+ message: `Result of "${request.method}" is not JSON-serializable`
161
+ });
162
+ }
163
+ return { bridge: 1, kind: "res", id: request.id, ok: true, result };
164
+ }
165
+ function failure(id, error) {
166
+ return { bridge: 1, kind: "res", id, ok: false, error };
167
+ }
168
+ function toErrorPayload(error) {
169
+ if (typeof error === "object" && error !== null) {
170
+ const { code, message } = error;
171
+ return {
172
+ code: typeof code === "string" ? code : ErrorCode.HANDLER_ERROR,
173
+ message: typeof message === "string" ? message : String(error)
174
+ };
175
+ }
176
+ return { code: ErrorCode.HANDLER_ERROR, message: String(error) };
177
+ }
178
+
179
+ // src/native/scripts.ts
180
+ function serializeForScript(value) {
181
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
182
+ }
183
+ function buildBadgeScript(info) {
184
+ return `(function () { window.${BADGE_KEY} = ${serializeForScript(info)}; })();`;
185
+ }
186
+ function buildDispatchScript(message, trustedHosts) {
187
+ return [
188
+ "(function () {",
189
+ " var host = String(location.hostname || '').toLowerCase().replace(/\\.$/, '');",
190
+ ` var trusted = ${serializeForScript(trustedHosts.map((entry) => entry.toLowerCase()))};`,
191
+ " var allowed = trusted.some(function (entry) {",
192
+ " if (entry.indexOf('*.') === 0) return host.slice(-(entry.length - 1)) === entry.slice(1);",
193
+ " return host === entry;",
194
+ " });",
195
+ " if (!allowed) return;",
196
+ ` window.dispatchEvent(new CustomEvent(${JSON.stringify(BRIDGE_EVENT)}, { detail: ${serializeForScript(message)} }));`,
197
+ "})();",
198
+ "true;"
199
+ ].join("\n");
200
+ }
201
+ function composeInjectedScript(info, consumerScript) {
202
+ return [buildBadgeScript(info), consumerScript ?? "", "true;"].join("\n;\n");
203
+ }
204
+
205
+ // src/native/BridgeWebView.tsx
206
+ var import_jsx_runtime = require("react/jsx-runtime");
207
+ function assignRef(ref, value) {
208
+ if (typeof ref === "function") ref(value);
209
+ else if (ref) ref.current = value;
210
+ }
211
+ var BridgeWebView = (0, import_react.forwardRef)(function BridgeWebView2(props, ref) {
212
+ const {
213
+ trustedHosts,
214
+ inAppHosts,
215
+ handlers,
216
+ emitter,
217
+ appInfo,
218
+ onMessage,
219
+ onShouldStartLoadWithRequest,
220
+ injectedJavaScriptBeforeContentLoaded,
221
+ injectedJavaScript,
222
+ ...webViewProps
223
+ } = props;
224
+ const webViewRef = (0, import_react.useRef)(null);
225
+ const handlersRef = (0, import_react.useRef)(handlers ?? {});
226
+ const trustedHostsRef = (0, import_react.useRef)(trustedHosts);
227
+ const inAppHostsRef = (0, import_react.useRef)(inAppHosts ?? []);
228
+ handlersRef.current = handlers ?? {};
229
+ trustedHostsRef.current = trustedHosts;
230
+ inAppHostsRef.current = inAppHosts ?? [];
231
+ const setRef = (0, import_react.useCallback)(
232
+ (instance) => {
233
+ webViewRef.current = instance;
234
+ assignRef(ref, instance);
235
+ },
236
+ [ref]
237
+ );
238
+ const send = (0, import_react.useCallback)((message) => {
239
+ webViewRef.current?.injectJavaScript(buildDispatchScript(message, trustedHostsRef.current));
240
+ }, []);
241
+ (0, import_react.useEffect)(() => emitter?.connect(send), [emitter, send]);
242
+ const appInfoKey = JSON.stringify(appInfo ?? {});
243
+ const info = (0, import_react.useMemo)(() => resolveAppInfo(import_react_native.Platform.OS, import_expo_constants.default.expoConfig, appInfo), [appInfoKey]);
244
+ const injectedBeforeLoad = (0, import_react.useMemo)(
245
+ () => composeInjectedScript(info, injectedJavaScriptBeforeContentLoaded),
246
+ [info, injectedJavaScriptBeforeContentLoaded]
247
+ );
248
+ const injectedAfterLoad = (0, import_react.useMemo)(() => composeInjectedScript(info, injectedJavaScript), [info, injectedJavaScript]);
249
+ const handleMessage = (0, import_react.useCallback)(
250
+ (event) => {
251
+ const { data, url } = event.nativeEvent;
252
+ const route = routeMessage(data, url, handlersRef.current, trustedHostsRef.current);
253
+ if (route.type === "forward") {
254
+ onMessage?.(event);
255
+ return;
256
+ }
257
+ if (route.type === "drop") {
258
+ if (__DEV__) console.warn(`[mobile-app-bridge] dropped message (${route.reason}) from ${url}`);
259
+ return;
260
+ }
261
+ void route.response.then(send);
262
+ },
263
+ [onMessage, send]
264
+ );
265
+ const handleShouldStart = (0, import_react.useMemo)(
266
+ () => composeNavigationHandler(
267
+ onShouldStartLoadWithRequest,
268
+ () => ({ trustedHosts: trustedHostsRef.current, inAppHosts: inAppHostsRef.current }),
269
+ (url) => {
270
+ import_react_native.Linking.openURL(url).catch(() => {
271
+ });
272
+ }
273
+ ),
274
+ [onShouldStartLoadWithRequest]
275
+ );
276
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
277
+ import_react_native_webview.WebView,
278
+ {
279
+ ...webViewProps,
280
+ ref: setRef,
281
+ onMessage: handleMessage,
282
+ onShouldStartLoadWithRequest: handleShouldStart,
283
+ injectedJavaScriptBeforeContentLoaded: injectedBeforeLoad,
284
+ injectedJavaScript: injectedAfterLoad,
285
+ injectedJavaScriptForMainFrameOnly: true,
286
+ injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true
287
+ }
288
+ );
289
+ });
290
+
291
+ // src/native/useBridgeEmitter.ts
292
+ var import_react2 = require("react");
293
+
294
+ // src/native/emitter.ts
295
+ function createEmitterCore() {
296
+ let send = null;
297
+ return {
298
+ emit(name, data) {
299
+ if (!send) return false;
300
+ const message = { bridge: 1, kind: "evt", name };
301
+ if (data !== void 0) message.data = data;
302
+ send(message);
303
+ return true;
304
+ },
305
+ connect(nextSend) {
306
+ send = nextSend;
307
+ return () => {
308
+ if (send === nextSend) send = null;
309
+ };
310
+ }
311
+ };
312
+ }
313
+
314
+ // src/native/useBridgeEmitter.ts
315
+ function useBridgeEmitter() {
316
+ const ref = (0, import_react2.useRef)(null);
317
+ if (ref.current === null) ref.current = createEmitterCore();
318
+ return ref.current;
319
+ }
320
+ // Annotate the CommonJS export names for ESM import in node:
321
+ 0 && (module.exports = {
322
+ BridgeWebView,
323
+ useBridgeEmitter
324
+ });
325
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/native/index.ts","../../src/native/BridgeWebView.tsx","../../src/native/app-info.ts","../../src/native/hosts.ts","../../src/native/navigation.ts","../../src/shared/protocol.ts","../../src/native/router.ts","../../src/native/scripts.ts","../../src/native/useBridgeEmitter.ts","../../src/native/emitter.ts"],"sourcesContent":["export { BridgeWebView } from './BridgeWebView'\nexport type { BridgeWebViewProps } from './BridgeWebView'\nexport { useBridgeEmitter } from './useBridgeEmitter'\nexport type { BridgeEmitter } from './emitter'\nexport type { Handler, Handlers } from './router'\nexport type { AppInfo } from '../shared/protocol'\n","import { forwardRef, useCallback, useEffect, useMemo, useRef, type ForwardedRef } from 'react'\nimport { Linking, Platform } from 'react-native'\nimport Constants from 'expo-constants'\nimport { WebView, type WebViewMessageEvent, type WebViewProps } from 'react-native-webview'\nimport type { AppInfo, EventMessage, ResponseMessage } from '../shared/protocol'\nimport { resolveAppInfo } from './app-info'\nimport type { BridgeEmitter } from './emitter'\nimport { composeNavigationHandler } from './navigation'\nimport { routeMessage, type Handlers } from './router'\nimport { buildDispatchScript, composeInjectedScript } from './scripts'\n\ntype ShouldStartLoadRequest = Parameters<NonNullable<WebViewProps['onShouldStartLoadWithRequest']>>[0]\n\nexport interface BridgeWebViewProps\n extends Omit<\n WebViewProps,\n | 'onShouldStartLoadWithRequest'\n | 'injectedJavaScriptForMainFrameOnly'\n | 'injectedJavaScriptBeforeContentLoadedForMainFrameOnly'\n > {\n /** Hostnames allowed to call handlers and to load inside the WebView. `*.x.com` matches subdomains. */\n trustedHosts: readonly string[]\n /** Hostnames that load inside the WebView WITHOUT bridge access (file previews, OAuth, 3-D Secure). */\n inAppHosts?: readonly string[]\n /** Methods the page can call with `bridge.call(method, params)`. */\n handlers?: Handlers\n /** From `useBridgeEmitter()`, to send events to the page. */\n emitter?: BridgeEmitter\n /** Overrides for the badge values read from expo-constants. */\n appInfo?: Partial<AppInfo>\n /** Runs before the default policy. Return true/false to decide, or undefined to fall back to it. */\n onShouldStartLoadWithRequest?: (request: ShouldStartLoadRequest) => boolean | undefined\n}\n\nfunction assignRef<T>(ref: ForwardedRef<T>, value: T | null) {\n if (typeof ref === 'function') ref(value)\n else if (ref) ref.current = value\n}\n\nexport const BridgeWebView = forwardRef<WebView, BridgeWebViewProps>(function BridgeWebView(props, ref) {\n const {\n trustedHosts,\n inAppHosts,\n handlers,\n emitter,\n appInfo,\n onMessage,\n onShouldStartLoadWithRequest,\n injectedJavaScriptBeforeContentLoaded,\n injectedJavaScript,\n ...webViewProps\n } = props\n\n const webViewRef = useRef<WebView | null>(null)\n const handlersRef = useRef<Handlers>(handlers ?? {})\n const trustedHostsRef = useRef(trustedHosts)\n const inAppHostsRef = useRef<readonly string[]>(inAppHosts ?? [])\n handlersRef.current = handlers ?? {}\n trustedHostsRef.current = trustedHosts\n inAppHostsRef.current = inAppHosts ?? []\n\n const setRef = useCallback(\n (instance: WebView | null) => {\n webViewRef.current = instance\n assignRef(ref, instance)\n },\n [ref],\n )\n\n // Every native-to-page delivery re-checks the current page's host in-page (see buildDispatchScript).\n const send = useCallback((message: ResponseMessage | EventMessage) => {\n webViewRef.current?.injectJavaScript(buildDispatchScript(message, trustedHostsRef.current))\n }, [])\n\n useEffect(() => emitter?.connect(send), [emitter, send])\n\n const appInfoKey = JSON.stringify(appInfo ?? {})\n // appInfoKey stands in for appInfo so a new object literal each render does not rebuild the scripts.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const info = useMemo(() => resolveAppInfo(Platform.OS, Constants.expoConfig, appInfo), [appInfoKey])\n const injectedBeforeLoad = useMemo(\n () => composeInjectedScript(info, injectedJavaScriptBeforeContentLoaded),\n [info, injectedJavaScriptBeforeContentLoaded],\n )\n // The badge runs again after load: on Android the before-load injection can lose a race with document creation.\n const injectedAfterLoad = useMemo(() => composeInjectedScript(info, injectedJavaScript), [info, injectedJavaScript])\n\n const handleMessage = useCallback(\n (event: WebViewMessageEvent) => {\n const { data, url } = event.nativeEvent\n const route = routeMessage(data, url, handlersRef.current, trustedHostsRef.current)\n if (route.type === 'forward') {\n onMessage?.(event)\n return\n }\n if (route.type === 'drop') {\n if (__DEV__) console.warn(`[mobile-app-bridge] dropped message (${route.reason}) from ${url}`)\n return\n }\n void route.response.then(send)\n },\n [onMessage, send],\n )\n\n const handleShouldStart = useMemo(\n () =>\n composeNavigationHandler<ShouldStartLoadRequest>(\n onShouldStartLoadWithRequest,\n () => ({ trustedHosts: trustedHostsRef.current, inAppHosts: inAppHostsRef.current }),\n (url) => {\n Linking.openURL(url).catch(() => {})\n },\n ),\n [onShouldStartLoadWithRequest],\n )\n\n return (\n <WebView\n {...webViewProps}\n ref={setRef}\n onMessage={handleMessage}\n onShouldStartLoadWithRequest={handleShouldStart}\n injectedJavaScriptBeforeContentLoaded={injectedBeforeLoad}\n injectedJavaScript={injectedAfterLoad}\n injectedJavaScriptForMainFrameOnly\n injectedJavaScriptBeforeContentLoadedForMainFrameOnly\n />\n )\n})\n","import type { AppInfo, Platform } from '../shared/protocol'\n\nexport interface ExpoConfigLike {\n version?: string\n ios?: { buildNumber?: string }\n android?: { versionCode?: number }\n}\n\nexport function resolveAppInfo(\n platformOS: string,\n expoConfig: ExpoConfigLike | null | undefined,\n override: Partial<AppInfo> = {},\n): AppInfo {\n const platform: Platform = platformOS === 'android' ? 'android' : 'ios'\n const buildNumber =\n platform === 'ios' ? expoConfig?.ios?.buildNumber : expoConfig?.android?.versionCode?.toString()\n\n return {\n platform,\n appVersion: expoConfig?.version ?? '0.0.0',\n buildNumber: buildNumber ?? '0',\n ...override,\n }\n}\n","// Regex-based on purpose: React Native's URL polyfill has historically not implemented `hostname`.\n// Backslashes are excluded from userinfo/host because browsers treat `\\` as `/` in http(s) URLs.\nconst URL_PATTERN = /^([a-z][a-z0-9+.-]*):(?:\\/\\/(?:[^@/\\\\?#]*@)?(\\[[^\\]]*\\]|[^:/\\\\?#]*))?/i\n\nexport function schemeOf(url: string): string | null {\n const match = URL_PATTERN.exec(url.trim())\n return match ? match[1]!.toLowerCase() : null\n}\n\nexport function hostnameOf(url: string): string | null {\n const host = URL_PATTERN.exec(url.trim())?.[2]\n if (!host) return null\n return host.toLowerCase().replace(/\\.$/, '')\n}\n\nexport function isTrustedHostname(hostname: string, trustedHosts: readonly string[]): boolean {\n const host = hostname.toLowerCase().replace(/\\.$/, '')\n return trustedHosts.some((entry) => {\n const pattern = entry.toLowerCase()\n if (pattern.startsWith('*.')) return host.endsWith(`.${pattern.slice(2)}`)\n return host === pattern\n })\n}\n\nexport function isTrustedUrl(url: string, trustedHosts: readonly string[]): boolean {\n const hostname = hostnameOf(url)\n return hostname !== null && isTrustedHostname(hostname, trustedHosts)\n}\n","import { isTrustedUrl, schemeOf } from './hosts'\n\nexport type NavigationDecision = 'load' | 'block' | 'open-external'\n\nexport interface NavigationRequestLike {\n url: string\n /** iOS only. Android only asks about top-frame navigations, so `undefined` means top frame. */\n isTopFrame?: boolean\n}\n\nexport interface HostLists {\n /** Load in the WebView and may call handlers. */\n trustedHosts: readonly string[]\n /** Load in the WebView without bridge access (file previews, OAuth, 3-D Secure). */\n inAppHosts: readonly string[]\n}\n\nconst BLOCKED_SCHEMES = new Set(['about', 'blob', 'file', 'javascript', 'data'])\n\nexport function decideNavigation(request: NavigationRequestLike, hosts: HostLists): NavigationDecision {\n const scheme = schemeOf(request.url)\n const isHttp = scheme === 'http' || scheme === 'https'\n const isSubframe = request.isTopFrame === false\n\n if (isHttp && (isTrustedUrl(request.url, hosts.trustedHosts) || isTrustedUrl(request.url, hosts.inAppHosts))) {\n return 'load'\n }\n // Iframes (reCAPTCHA, maps, GTM, payment widgets) must load in place; they never get the badge.\n if (isSubframe && (isHttp || scheme === 'about')) return 'load'\n if (scheme === null || BLOCKED_SCHEMES.has(scheme)) return 'block'\n return 'open-external'\n}\n\nexport function composeNavigationHandler<R extends NavigationRequestLike>(\n consumer: ((request: R) => boolean | undefined) | undefined,\n getHosts: () => HostLists,\n openExternal: (url: string) => void,\n): (request: R) => boolean {\n return (request) => {\n const custom = consumer?.(request)\n if (typeof custom === 'boolean') return custom\n\n const decision = decideNavigation(request, getHosts())\n if (decision === 'open-external') openExternal(request.url)\n return decision === 'load'\n }\n}\n","export const PROTOCOL_MARKER = 1 as const\nexport const BRIDGE_EVENT = 'mobile-app-bridge'\nexport const BADGE_KEY = '__MOBILE_APP_BRIDGE__'\nexport const DEFAULT_TIMEOUT = 30_000\n\nexport const ErrorCode = {\n NOT_IN_APP: 'NOT_IN_APP',\n TIMEOUT: 'TIMEOUT',\n UNKNOWN_METHOD: 'UNKNOWN_METHOD',\n HANDLER_ERROR: 'HANDLER_ERROR',\n INVALID_PARAMS: 'INVALID_PARAMS',\n} as const\n\nexport type Platform = 'ios' | 'android'\n\nexport interface AppInfo {\n platform: Platform\n appVersion: string\n buildNumber: string\n}\n\nexport interface RequestMessage {\n bridge: typeof PROTOCOL_MARKER\n kind: 'req'\n id: string\n method: string\n params?: unknown\n}\n\nexport interface ErrorPayload {\n code: string\n message: string\n}\n\nexport type ResponseMessage =\n | { bridge: typeof PROTOCOL_MARKER; kind: 'res'; id: string; ok: true; result?: unknown }\n | { bridge: typeof PROTOCOL_MARKER; kind: 'res'; id: string; ok: false; error: ErrorPayload }\n\nexport interface EventMessage {\n bridge: typeof PROTOCOL_MARKER\n kind: 'evt'\n name: string\n data?: unknown\n}\n\nexport type BridgeMessage = RequestMessage | ResponseMessage | EventMessage\n\nexport function isBridgeMessage(value: unknown): value is BridgeMessage {\n if (typeof value !== 'object' || value === null) return false\n const message = value as Record<string, unknown>\n if (message.bridge !== PROTOCOL_MARKER) return false\n\n switch (message.kind) {\n case 'req':\n return typeof message.id === 'string' && typeof message.method === 'string'\n case 'res':\n if (typeof message.id !== 'string' || typeof message.ok !== 'boolean') return false\n return message.ok || (typeof message.error === 'object' && message.error !== null)\n case 'evt':\n return typeof message.name === 'string'\n default:\n return false\n }\n}\n","import {\n ErrorCode,\n isBridgeMessage,\n type ErrorPayload,\n type RequestMessage,\n type ResponseMessage,\n} from '../shared/protocol'\nimport { isTrustedUrl } from './hosts'\n\n// `any` is intentional: handlers type their own params.\nexport type Handler = (params: any) => unknown\nexport type Handlers = Record<string, Handler>\n\nexport type RouteResult =\n | { type: 'forward' }\n | { type: 'drop'; reason: 'untrusted-origin' | 'unexpected-kind' }\n | { type: 'response'; response: Promise<ResponseMessage> }\n\nexport function routeMessage(\n raw: string,\n sourceUrl: string,\n handlers: Handlers,\n trustedHosts: readonly string[],\n): RouteResult {\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { type: 'forward' }\n }\n\n if (!isBridgeMessage(parsed)) return { type: 'forward' }\n if (parsed.kind !== 'req') return { type: 'drop', reason: 'unexpected-kind' }\n if (!isTrustedUrl(sourceUrl, trustedHosts)) return { type: 'drop', reason: 'untrusted-origin' }\n\n return { type: 'response', response: runHandler(parsed, handlers) }\n}\n\nexport async function runHandler(request: RequestMessage, handlers: Handlers): Promise<ResponseMessage> {\n // Own-property check so names like \"toString\" never reach Object.prototype.\n const handler = Object.prototype.hasOwnProperty.call(handlers, request.method) ? handlers[request.method] : undefined\n if (!handler) {\n return failure(request.id, { code: ErrorCode.UNKNOWN_METHOD, message: `No handler registered for \"${request.method}\"` })\n }\n\n let result: unknown\n try {\n result = await handler(request.params)\n } catch (error) {\n return failure(request.id, toErrorPayload(error))\n }\n\n try {\n JSON.stringify(result)\n } catch {\n return failure(request.id, {\n code: ErrorCode.HANDLER_ERROR,\n message: `Result of \"${request.method}\" is not JSON-serializable`,\n })\n }\n\n return { bridge: 1, kind: 'res', id: request.id, ok: true, result }\n}\n\nfunction failure(id: string, error: ErrorPayload): ResponseMessage {\n return { bridge: 1, kind: 'res', id, ok: false, error }\n}\n\nfunction toErrorPayload(error: unknown): ErrorPayload {\n if (typeof error === 'object' && error !== null) {\n const { code, message } = error as { code?: unknown; message?: unknown }\n return {\n code: typeof code === 'string' ? code : ErrorCode.HANDLER_ERROR,\n message: typeof message === 'string' ? message : String(error),\n }\n }\n return { code: ErrorCode.HANDLER_ERROR, message: String(error) }\n}\n","import { BADGE_KEY, BRIDGE_EVENT, type AppInfo, type EventMessage, type ResponseMessage } from '../shared/protocol'\n\n/** JSON that is also safe to embed inside injected JavaScript. */\nexport function serializeForScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n\n/**\n * Sets the badge. BridgeWebView runs it before content loads AND again after load, because on Android\n * react-native-webview injects the \"before content loaded\" script in a race with document creation.\n */\nexport function buildBadgeScript(info: AppInfo): string {\n return `(function () { window.${BADGE_KEY} = ${serializeForScript(info)}; })();`\n}\n\n/**\n * Delivers a response or event to the page, but only if the page's host is trusted: pages that\n * load in the app without bridge access (inAppHosts, consumer-allowed third parties) get nothing.\n * Host matching mirrors isTrustedHostname() in hosts.ts.\n */\nexport function buildDispatchScript(message: ResponseMessage | EventMessage, trustedHosts: readonly string[]): string {\n return [\n '(function () {',\n \" var host = String(location.hostname || '').toLowerCase().replace(/\\\\.$/, '');\",\n ` var trusted = ${serializeForScript(trustedHosts.map((entry) => entry.toLowerCase()))};`,\n ' var allowed = trusted.some(function (entry) {',\n \" if (entry.indexOf('*.') === 0) return host.slice(-(entry.length - 1)) === entry.slice(1);\",\n ' return host === entry;',\n ' });',\n ' if (!allowed) return;',\n ` window.dispatchEvent(new CustomEvent(${JSON.stringify(BRIDGE_EVENT)}, { detail: ${serializeForScript(message)} }));`,\n '})();',\n 'true;',\n ].join('\\n')\n}\n\n/** Badge first, then the consumer's own script. Separators keep trailing comments harmless. */\nexport function composeInjectedScript(info: AppInfo, consumerScript?: string): string {\n return [buildBadgeScript(info), consumerScript ?? '', 'true;'].join('\\n;\\n')\n}\n","import { useRef } from 'react'\nimport { createEmitterCore, type BridgeEmitter } from './emitter'\n\n/** Stable emitter for sending events to the page. Pass it to <BridgeWebView emitter={...} />. */\nexport function useBridgeEmitter(): BridgeEmitter {\n const ref = useRef<BridgeEmitter | null>(null)\n if (ref.current === null) ref.current = createEmitterCore()\n return ref.current\n}\n","import type { EventMessage } from '../shared/protocol'\n\nexport interface BridgeEmitter {\n /**\n * Sends an event to the page right away, like the Agendart bridge. Returns false when no WebView is\n * connected; the event is then dropped. Anything that must not be lost (e.g. the notification that\n * opened the app) belongs in a handler the page calls when it is ready.\n */\n emit(name: string, data?: unknown): boolean\n /** @internal Called by BridgeWebView. */\n connect(send: (message: EventMessage) => void): () => void\n}\n\nexport function createEmitterCore(): BridgeEmitter {\n let send: ((message: EventMessage) => void) | null = null\n\n return {\n emit(name, data) {\n if (!send) return false\n const message: EventMessage = { bridge: 1, kind: 'evt', name }\n if (data !== undefined) message.data = data\n send(message)\n return true\n },\n\n connect(nextSend) {\n send = nextSend\n return () => {\n if (send === nextSend) send = null\n }\n },\n }\n}\n"],"mappings":"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","names":["BridgeWebView","Constants","import_react"]}
@@ -0,0 +1,39 @@
1
+ import * as react from 'react';
2
+ import { WebViewProps, WebView } from 'react-native-webview';
3
+ import { E as EventMessage, A as AppInfo } from '../protocol-DcOkx09n.cjs';
4
+
5
+ interface BridgeEmitter {
6
+ /**
7
+ * Sends an event to the page right away, like the Agendart bridge. Returns false when no WebView is
8
+ * connected; the event is then dropped. Anything that must not be lost (e.g. the notification that
9
+ * opened the app) belongs in a handler the page calls when it is ready.
10
+ */
11
+ emit(name: string, data?: unknown): boolean;
12
+ /** @internal Called by BridgeWebView. */
13
+ connect(send: (message: EventMessage) => void): () => void;
14
+ }
15
+
16
+ type Handler = (params: any) => unknown;
17
+ type Handlers = Record<string, Handler>;
18
+
19
+ type ShouldStartLoadRequest = Parameters<NonNullable<WebViewProps['onShouldStartLoadWithRequest']>>[0];
20
+ interface BridgeWebViewProps extends Omit<WebViewProps, 'onShouldStartLoadWithRequest' | 'injectedJavaScriptForMainFrameOnly' | 'injectedJavaScriptBeforeContentLoadedForMainFrameOnly'> {
21
+ /** Hostnames allowed to call handlers and to load inside the WebView. `*.x.com` matches subdomains. */
22
+ trustedHosts: readonly string[];
23
+ /** Hostnames that load inside the WebView WITHOUT bridge access (file previews, OAuth, 3-D Secure). */
24
+ inAppHosts?: readonly string[];
25
+ /** Methods the page can call with `bridge.call(method, params)`. */
26
+ handlers?: Handlers;
27
+ /** From `useBridgeEmitter()`, to send events to the page. */
28
+ emitter?: BridgeEmitter;
29
+ /** Overrides for the badge values read from expo-constants. */
30
+ appInfo?: Partial<AppInfo>;
31
+ /** Runs before the default policy. Return true/false to decide, or undefined to fall back to it. */
32
+ onShouldStartLoadWithRequest?: (request: ShouldStartLoadRequest) => boolean | undefined;
33
+ }
34
+ declare const BridgeWebView: react.ForwardRefExoticComponent<BridgeWebViewProps & react.RefAttributes<WebView<{}>>>;
35
+
36
+ /** Stable emitter for sending events to the page. Pass it to <BridgeWebView emitter={...} />. */
37
+ declare function useBridgeEmitter(): BridgeEmitter;
38
+
39
+ export { AppInfo, type BridgeEmitter, BridgeWebView, type BridgeWebViewProps, type Handler, type Handlers, useBridgeEmitter };