@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.
@@ -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.js';
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 };
@@ -0,0 +1,266 @@
1
+ import {
2
+ BADGE_KEY,
3
+ BRIDGE_EVENT,
4
+ ErrorCode,
5
+ isBridgeMessage
6
+ } from "../chunk-SEQSUJLW.js";
7
+
8
+ // src/native/BridgeWebView.tsx
9
+ import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
10
+ import { Linking, Platform } from "react-native";
11
+ import Constants from "expo-constants";
12
+ import { WebView } from "react-native-webview";
13
+
14
+ // src/native/app-info.ts
15
+ function resolveAppInfo(platformOS, expoConfig, override = {}) {
16
+ const platform = platformOS === "android" ? "android" : "ios";
17
+ const buildNumber = platform === "ios" ? expoConfig?.ios?.buildNumber : expoConfig?.android?.versionCode?.toString();
18
+ return {
19
+ platform,
20
+ appVersion: expoConfig?.version ?? "0.0.0",
21
+ buildNumber: buildNumber ?? "0",
22
+ ...override
23
+ };
24
+ }
25
+
26
+ // src/native/hosts.ts
27
+ var URL_PATTERN = /^([a-z][a-z0-9+.-]*):(?:\/\/(?:[^@/\\?#]*@)?(\[[^\]]*\]|[^:/\\?#]*))?/i;
28
+ function schemeOf(url) {
29
+ const match = URL_PATTERN.exec(url.trim());
30
+ return match ? match[1].toLowerCase() : null;
31
+ }
32
+ function hostnameOf(url) {
33
+ const host = URL_PATTERN.exec(url.trim())?.[2];
34
+ if (!host) return null;
35
+ return host.toLowerCase().replace(/\.$/, "");
36
+ }
37
+ function isTrustedHostname(hostname, trustedHosts) {
38
+ const host = hostname.toLowerCase().replace(/\.$/, "");
39
+ return trustedHosts.some((entry) => {
40
+ const pattern = entry.toLowerCase();
41
+ if (pattern.startsWith("*.")) return host.endsWith(`.${pattern.slice(2)}`);
42
+ return host === pattern;
43
+ });
44
+ }
45
+ function isTrustedUrl(url, trustedHosts) {
46
+ const hostname = hostnameOf(url);
47
+ return hostname !== null && isTrustedHostname(hostname, trustedHosts);
48
+ }
49
+
50
+ // src/native/navigation.ts
51
+ var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["about", "blob", "file", "javascript", "data"]);
52
+ function decideNavigation(request, hosts) {
53
+ const scheme = schemeOf(request.url);
54
+ const isHttp = scheme === "http" || scheme === "https";
55
+ const isSubframe = request.isTopFrame === false;
56
+ if (isHttp && (isTrustedUrl(request.url, hosts.trustedHosts) || isTrustedUrl(request.url, hosts.inAppHosts))) {
57
+ return "load";
58
+ }
59
+ if (isSubframe && (isHttp || scheme === "about")) return "load";
60
+ if (scheme === null || BLOCKED_SCHEMES.has(scheme)) return "block";
61
+ return "open-external";
62
+ }
63
+ function composeNavigationHandler(consumer, getHosts, openExternal) {
64
+ return (request) => {
65
+ const custom = consumer?.(request);
66
+ if (typeof custom === "boolean") return custom;
67
+ const decision = decideNavigation(request, getHosts());
68
+ if (decision === "open-external") openExternal(request.url);
69
+ return decision === "load";
70
+ };
71
+ }
72
+
73
+ // src/native/router.ts
74
+ function routeMessage(raw, sourceUrl, handlers, trustedHosts) {
75
+ let parsed;
76
+ try {
77
+ parsed = JSON.parse(raw);
78
+ } catch {
79
+ return { type: "forward" };
80
+ }
81
+ if (!isBridgeMessage(parsed)) return { type: "forward" };
82
+ if (parsed.kind !== "req") return { type: "drop", reason: "unexpected-kind" };
83
+ if (!isTrustedUrl(sourceUrl, trustedHosts)) return { type: "drop", reason: "untrusted-origin" };
84
+ return { type: "response", response: runHandler(parsed, handlers) };
85
+ }
86
+ async function runHandler(request, handlers) {
87
+ const handler = Object.prototype.hasOwnProperty.call(handlers, request.method) ? handlers[request.method] : void 0;
88
+ if (!handler) {
89
+ return failure(request.id, { code: ErrorCode.UNKNOWN_METHOD, message: `No handler registered for "${request.method}"` });
90
+ }
91
+ let result;
92
+ try {
93
+ result = await handler(request.params);
94
+ } catch (error) {
95
+ return failure(request.id, toErrorPayload(error));
96
+ }
97
+ try {
98
+ JSON.stringify(result);
99
+ } catch {
100
+ return failure(request.id, {
101
+ code: ErrorCode.HANDLER_ERROR,
102
+ message: `Result of "${request.method}" is not JSON-serializable`
103
+ });
104
+ }
105
+ return { bridge: 1, kind: "res", id: request.id, ok: true, result };
106
+ }
107
+ function failure(id, error) {
108
+ return { bridge: 1, kind: "res", id, ok: false, error };
109
+ }
110
+ function toErrorPayload(error) {
111
+ if (typeof error === "object" && error !== null) {
112
+ const { code, message } = error;
113
+ return {
114
+ code: typeof code === "string" ? code : ErrorCode.HANDLER_ERROR,
115
+ message: typeof message === "string" ? message : String(error)
116
+ };
117
+ }
118
+ return { code: ErrorCode.HANDLER_ERROR, message: String(error) };
119
+ }
120
+
121
+ // src/native/scripts.ts
122
+ function serializeForScript(value) {
123
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
124
+ }
125
+ function buildBadgeScript(info) {
126
+ return `(function () { window.${BADGE_KEY} = ${serializeForScript(info)}; })();`;
127
+ }
128
+ function buildDispatchScript(message, trustedHosts) {
129
+ return [
130
+ "(function () {",
131
+ " var host = String(location.hostname || '').toLowerCase().replace(/\\.$/, '');",
132
+ ` var trusted = ${serializeForScript(trustedHosts.map((entry) => entry.toLowerCase()))};`,
133
+ " var allowed = trusted.some(function (entry) {",
134
+ " if (entry.indexOf('*.') === 0) return host.slice(-(entry.length - 1)) === entry.slice(1);",
135
+ " return host === entry;",
136
+ " });",
137
+ " if (!allowed) return;",
138
+ ` window.dispatchEvent(new CustomEvent(${JSON.stringify(BRIDGE_EVENT)}, { detail: ${serializeForScript(message)} }));`,
139
+ "})();",
140
+ "true;"
141
+ ].join("\n");
142
+ }
143
+ function composeInjectedScript(info, consumerScript) {
144
+ return [buildBadgeScript(info), consumerScript ?? "", "true;"].join("\n;\n");
145
+ }
146
+
147
+ // src/native/BridgeWebView.tsx
148
+ import { jsx } from "react/jsx-runtime";
149
+ function assignRef(ref, value) {
150
+ if (typeof ref === "function") ref(value);
151
+ else if (ref) ref.current = value;
152
+ }
153
+ var BridgeWebView = forwardRef(function BridgeWebView2(props, ref) {
154
+ const {
155
+ trustedHosts,
156
+ inAppHosts,
157
+ handlers,
158
+ emitter,
159
+ appInfo,
160
+ onMessage,
161
+ onShouldStartLoadWithRequest,
162
+ injectedJavaScriptBeforeContentLoaded,
163
+ injectedJavaScript,
164
+ ...webViewProps
165
+ } = props;
166
+ const webViewRef = useRef(null);
167
+ const handlersRef = useRef(handlers ?? {});
168
+ const trustedHostsRef = useRef(trustedHosts);
169
+ const inAppHostsRef = useRef(inAppHosts ?? []);
170
+ handlersRef.current = handlers ?? {};
171
+ trustedHostsRef.current = trustedHosts;
172
+ inAppHostsRef.current = inAppHosts ?? [];
173
+ const setRef = useCallback(
174
+ (instance) => {
175
+ webViewRef.current = instance;
176
+ assignRef(ref, instance);
177
+ },
178
+ [ref]
179
+ );
180
+ const send = useCallback((message) => {
181
+ webViewRef.current?.injectJavaScript(buildDispatchScript(message, trustedHostsRef.current));
182
+ }, []);
183
+ useEffect(() => emitter?.connect(send), [emitter, send]);
184
+ const appInfoKey = JSON.stringify(appInfo ?? {});
185
+ const info = useMemo(() => resolveAppInfo(Platform.OS, Constants.expoConfig, appInfo), [appInfoKey]);
186
+ const injectedBeforeLoad = useMemo(
187
+ () => composeInjectedScript(info, injectedJavaScriptBeforeContentLoaded),
188
+ [info, injectedJavaScriptBeforeContentLoaded]
189
+ );
190
+ const injectedAfterLoad = useMemo(() => composeInjectedScript(info, injectedJavaScript), [info, injectedJavaScript]);
191
+ const handleMessage = useCallback(
192
+ (event) => {
193
+ const { data, url } = event.nativeEvent;
194
+ const route = routeMessage(data, url, handlersRef.current, trustedHostsRef.current);
195
+ if (route.type === "forward") {
196
+ onMessage?.(event);
197
+ return;
198
+ }
199
+ if (route.type === "drop") {
200
+ if (__DEV__) console.warn(`[mobile-app-bridge] dropped message (${route.reason}) from ${url}`);
201
+ return;
202
+ }
203
+ void route.response.then(send);
204
+ },
205
+ [onMessage, send]
206
+ );
207
+ const handleShouldStart = useMemo(
208
+ () => composeNavigationHandler(
209
+ onShouldStartLoadWithRequest,
210
+ () => ({ trustedHosts: trustedHostsRef.current, inAppHosts: inAppHostsRef.current }),
211
+ (url) => {
212
+ Linking.openURL(url).catch(() => {
213
+ });
214
+ }
215
+ ),
216
+ [onShouldStartLoadWithRequest]
217
+ );
218
+ return /* @__PURE__ */ jsx(
219
+ WebView,
220
+ {
221
+ ...webViewProps,
222
+ ref: setRef,
223
+ onMessage: handleMessage,
224
+ onShouldStartLoadWithRequest: handleShouldStart,
225
+ injectedJavaScriptBeforeContentLoaded: injectedBeforeLoad,
226
+ injectedJavaScript: injectedAfterLoad,
227
+ injectedJavaScriptForMainFrameOnly: true,
228
+ injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true
229
+ }
230
+ );
231
+ });
232
+
233
+ // src/native/useBridgeEmitter.ts
234
+ import { useRef as useRef2 } from "react";
235
+
236
+ // src/native/emitter.ts
237
+ function createEmitterCore() {
238
+ let send = null;
239
+ return {
240
+ emit(name, data) {
241
+ if (!send) return false;
242
+ const message = { bridge: 1, kind: "evt", name };
243
+ if (data !== void 0) message.data = data;
244
+ send(message);
245
+ return true;
246
+ },
247
+ connect(nextSend) {
248
+ send = nextSend;
249
+ return () => {
250
+ if (send === nextSend) send = null;
251
+ };
252
+ }
253
+ };
254
+ }
255
+
256
+ // src/native/useBridgeEmitter.ts
257
+ function useBridgeEmitter() {
258
+ const ref = useRef2(null);
259
+ if (ref.current === null) ref.current = createEmitterCore();
260
+ return ref.current;
261
+ }
262
+ export {
263
+ BridgeWebView,
264
+ useBridgeEmitter
265
+ };
266
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/native/BridgeWebView.tsx","../../src/native/app-info.ts","../../src/native/hosts.ts","../../src/native/navigation.ts","../../src/native/router.ts","../../src/native/scripts.ts","../../src/native/useBridgeEmitter.ts","../../src/native/emitter.ts"],"sourcesContent":["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","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","useRef","useRef"]}
@@ -0,0 +1,22 @@
1
+ declare const PROTOCOL_MARKER: 1;
2
+ declare const ErrorCode: {
3
+ readonly NOT_IN_APP: "NOT_IN_APP";
4
+ readonly TIMEOUT: "TIMEOUT";
5
+ readonly UNKNOWN_METHOD: "UNKNOWN_METHOD";
6
+ readonly HANDLER_ERROR: "HANDLER_ERROR";
7
+ readonly INVALID_PARAMS: "INVALID_PARAMS";
8
+ };
9
+ type Platform = 'ios' | 'android';
10
+ interface AppInfo {
11
+ platform: Platform;
12
+ appVersion: string;
13
+ buildNumber: string;
14
+ }
15
+ interface EventMessage {
16
+ bridge: typeof PROTOCOL_MARKER;
17
+ kind: 'evt';
18
+ name: string;
19
+ data?: unknown;
20
+ }
21
+
22
+ export { type AppInfo as A, type EventMessage as E, ErrorCode as a };
@@ -0,0 +1,22 @@
1
+ declare const PROTOCOL_MARKER: 1;
2
+ declare const ErrorCode: {
3
+ readonly NOT_IN_APP: "NOT_IN_APP";
4
+ readonly TIMEOUT: "TIMEOUT";
5
+ readonly UNKNOWN_METHOD: "UNKNOWN_METHOD";
6
+ readonly HANDLER_ERROR: "HANDLER_ERROR";
7
+ readonly INVALID_PARAMS: "INVALID_PARAMS";
8
+ };
9
+ type Platform = 'ios' | 'android';
10
+ interface AppInfo {
11
+ platform: Platform;
12
+ appVersion: string;
13
+ buildNumber: string;
14
+ }
15
+ interface EventMessage {
16
+ bridge: typeof PROTOCOL_MARKER;
17
+ kind: 'evt';
18
+ name: string;
19
+ data?: unknown;
20
+ }
21
+
22
+ export { type AppInfo as A, type EventMessage as E, ErrorCode as a };
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/web/index.ts
21
+ var web_exports = {};
22
+ __export(web_exports, {
23
+ BridgeError: () => BridgeError,
24
+ ErrorCode: () => ErrorCode,
25
+ bridge: () => bridge,
26
+ createBridgeClient: () => createBridgeClient
27
+ });
28
+ module.exports = __toCommonJS(web_exports);
29
+
30
+ // src/shared/protocol.ts
31
+ var PROTOCOL_MARKER = 1;
32
+ var BRIDGE_EVENT = "mobile-app-bridge";
33
+ var DEFAULT_TIMEOUT = 3e4;
34
+ var ErrorCode = {
35
+ NOT_IN_APP: "NOT_IN_APP",
36
+ TIMEOUT: "TIMEOUT",
37
+ UNKNOWN_METHOD: "UNKNOWN_METHOD",
38
+ HANDLER_ERROR: "HANDLER_ERROR",
39
+ INVALID_PARAMS: "INVALID_PARAMS"
40
+ };
41
+ function isBridgeMessage(value) {
42
+ if (typeof value !== "object" || value === null) return false;
43
+ const message = value;
44
+ if (message.bridge !== PROTOCOL_MARKER) return false;
45
+ switch (message.kind) {
46
+ case "req":
47
+ return typeof message.id === "string" && typeof message.method === "string";
48
+ case "res":
49
+ if (typeof message.id !== "string" || typeof message.ok !== "boolean") return false;
50
+ return message.ok || typeof message.error === "object" && message.error !== null;
51
+ case "evt":
52
+ return typeof message.name === "string";
53
+ default:
54
+ return false;
55
+ }
56
+ }
57
+
58
+ // src/web/errors.ts
59
+ var BridgeError = class extends Error {
60
+ code;
61
+ constructor(code, message) {
62
+ super(message);
63
+ this.name = "BridgeError";
64
+ this.code = code;
65
+ }
66
+ };
67
+
68
+ // src/web/client.ts
69
+ function createBridgeClient(getWindow) {
70
+ const pending = /* @__PURE__ */ new Map();
71
+ const listeners = /* @__PURE__ */ new Map();
72
+ let attachedTo;
73
+ let counter = 0;
74
+ function appWindow() {
75
+ const win = getWindow();
76
+ return win?.ReactNativeWebView ? win : void 0;
77
+ }
78
+ function handle(detail) {
79
+ if (!isBridgeMessage(detail)) return;
80
+ if (detail.kind === "evt") {
81
+ const subscribers = listeners.get(detail.name);
82
+ if (subscribers) for (const listener of [...subscribers]) listener(detail.data);
83
+ return;
84
+ }
85
+ if (detail.kind !== "res") return;
86
+ const entry = pending.get(detail.id);
87
+ if (!entry) return;
88
+ pending.delete(detail.id);
89
+ if (entry.timer !== void 0) clearTimeout(entry.timer);
90
+ if (detail.ok) entry.resolve(detail.result);
91
+ else entry.reject(new BridgeError(detail.error.code, detail.error.message));
92
+ }
93
+ function attach(win) {
94
+ if (attachedTo === win) return;
95
+ attachedTo = win;
96
+ win.addEventListener(BRIDGE_EVENT, (event) => handle(event.detail));
97
+ }
98
+ function nextId() {
99
+ return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${(counter++).toString(36)}`;
100
+ }
101
+ return {
102
+ get isApp() {
103
+ return appWindow() !== void 0;
104
+ },
105
+ get info() {
106
+ return appWindow()?.__MOBILE_APP_BRIDGE__ ?? null;
107
+ },
108
+ call(method, params, callOptions = {}) {
109
+ const win = appWindow();
110
+ if (!win?.ReactNativeWebView) {
111
+ return Promise.reject(new BridgeError(ErrorCode.NOT_IN_APP, `Cannot call "${method}" outside the mobile app`));
112
+ }
113
+ attach(win);
114
+ const id = nextId();
115
+ const request = { bridge: 1, kind: "req", id, method };
116
+ if (params !== void 0) request.params = params;
117
+ let payload;
118
+ try {
119
+ payload = JSON.stringify(request);
120
+ } catch {
121
+ return Promise.reject(new BridgeError(ErrorCode.INVALID_PARAMS, `Params for "${method}" are not JSON-serializable`));
122
+ }
123
+ const timeout = callOptions.timeout ?? DEFAULT_TIMEOUT;
124
+ const postMessage = win.ReactNativeWebView.postMessage.bind(win.ReactNativeWebView);
125
+ return new Promise((resolve, reject) => {
126
+ const timer = timeout > 0 ? setTimeout(() => {
127
+ if (pending.delete(id)) {
128
+ reject(new BridgeError(ErrorCode.TIMEOUT, `"${method}" did not respond within ${timeout}ms`));
129
+ }
130
+ }, timeout) : void 0;
131
+ pending.set(id, { resolve, reject, timer });
132
+ postMessage(payload);
133
+ });
134
+ },
135
+ on(name, listener) {
136
+ const win = appWindow();
137
+ if (win) attach(win);
138
+ let subscribers = listeners.get(name);
139
+ if (!subscribers) {
140
+ subscribers = /* @__PURE__ */ new Set();
141
+ listeners.set(name, subscribers);
142
+ }
143
+ const typed = listener;
144
+ subscribers.add(typed);
145
+ return () => {
146
+ subscribers.delete(typed);
147
+ };
148
+ }
149
+ };
150
+ }
151
+
152
+ // src/web/index.ts
153
+ var bridge = createBridgeClient(
154
+ () => typeof window === "undefined" ? void 0 : window
155
+ );
156
+ // Annotate the CommonJS export names for ESM import in node:
157
+ 0 && (module.exports = {
158
+ BridgeError,
159
+ ErrorCode,
160
+ bridge,
161
+ createBridgeClient
162
+ });
163
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/web/index.ts","../../src/shared/protocol.ts","../../src/web/errors.ts","../../src/web/client.ts"],"sourcesContent":["import { createBridgeClient, type BridgeWindow } from './client'\n\n/** Shared client for the current page. Safe to import during SSR: it only reads `window` when used. */\nexport const bridge = createBridgeClient(() =>\n typeof window === 'undefined' ? undefined : (window as unknown as BridgeWindow),\n)\n\nexport { createBridgeClient } from './client'\nexport type { BridgeClient, BridgeWindow, CallOptions } from './client'\nexport { BridgeError } from './errors'\nexport { ErrorCode } from '../shared/protocol'\nexport type { AppInfo } from '../shared/protocol'\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","export class BridgeError extends Error {\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'BridgeError'\n this.code = code\n }\n}\n","import {\n BRIDGE_EVENT,\n DEFAULT_TIMEOUT,\n ErrorCode,\n isBridgeMessage,\n type AppInfo,\n type RequestMessage,\n} from '../shared/protocol'\nimport { BridgeError } from './errors'\n\nexport interface BridgeWindow {\n __MOBILE_APP_BRIDGE__?: AppInfo\n ReactNativeWebView?: { postMessage(message: string): void }\n addEventListener(type: string, listener: (event: Event) => void): void\n}\n\nexport interface CallOptions {\n /** Milliseconds to wait for the native response. Defaults to 30s; 0 disables the timeout. */\n timeout?: number\n}\n\nexport interface BridgeClient {\n readonly isApp: boolean\n readonly info: AppInfo | null\n call<T = unknown>(method: string, params?: unknown, options?: CallOptions): Promise<T>\n on<T = unknown>(name: string, listener: (data: T) => void): () => void\n}\n\ninterface PendingCall {\n resolve: (value: unknown) => void\n reject: (error: BridgeError) => void\n timer: ReturnType<typeof setTimeout> | undefined\n}\n\ntype Listener = (data: unknown) => void\n\nexport function createBridgeClient(\n getWindow: () => BridgeWindow | undefined,\n): BridgeClient {\n const pending = new Map<string, PendingCall>()\n const listeners = new Map<string, Set<Listener>>()\n let attachedTo: BridgeWindow | undefined\n let counter = 0\n\n /** Inside the app whenever react-native-webview's bridge object exists (same rule as the Agendart bridge). */\n function appWindow(): BridgeWindow | undefined {\n const win = getWindow()\n return win?.ReactNativeWebView ? win : undefined\n }\n\n function handle(detail: unknown) {\n if (!isBridgeMessage(detail)) return\n if (detail.kind === 'evt') {\n // Events are fire-and-forget: without a subscriber they are dropped. Anything that must not be\n // lost (e.g. \"app opened from a notification\") is exposed as a method the page calls when ready.\n const subscribers = listeners.get(detail.name)\n if (subscribers) for (const listener of [...subscribers]) listener(detail.data)\n return\n }\n if (detail.kind !== 'res') return\n\n const entry = pending.get(detail.id)\n if (!entry) return\n pending.delete(detail.id)\n if (entry.timer !== undefined) clearTimeout(entry.timer)\n if (detail.ok) entry.resolve(detail.result)\n else entry.reject(new BridgeError(detail.error.code, detail.error.message))\n }\n\n function attach(win: BridgeWindow) {\n if (attachedTo === win) return\n attachedTo = win\n win.addEventListener(BRIDGE_EVENT, (event) => handle((event as CustomEvent).detail))\n }\n\n function nextId(): string {\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${(counter++).toString(36)}`\n }\n\n return {\n get isApp() {\n return appWindow() !== undefined\n },\n\n get info() {\n return appWindow()?.__MOBILE_APP_BRIDGE__ ?? null\n },\n\n call<T = unknown>(method: string, params?: unknown, callOptions: CallOptions = {}): Promise<T> {\n const win = appWindow()\n if (!win?.ReactNativeWebView) {\n return Promise.reject(new BridgeError(ErrorCode.NOT_IN_APP, `Cannot call \"${method}\" outside the mobile app`))\n }\n attach(win)\n\n const id = nextId()\n const request: RequestMessage = { bridge: 1, kind: 'req', id, method }\n if (params !== undefined) request.params = params\n\n let payload: string\n try {\n payload = JSON.stringify(request)\n } catch {\n return Promise.reject(new BridgeError(ErrorCode.INVALID_PARAMS, `Params for \"${method}\" are not JSON-serializable`))\n }\n\n const timeout = callOptions.timeout ?? DEFAULT_TIMEOUT\n const postMessage = win.ReactNativeWebView.postMessage.bind(win.ReactNativeWebView)\n\n return new Promise<T>((resolve, reject) => {\n const timer =\n timeout > 0\n ? setTimeout(() => {\n if (pending.delete(id)) {\n reject(new BridgeError(ErrorCode.TIMEOUT, `\"${method}\" did not respond within ${timeout}ms`))\n }\n }, timeout)\n : undefined\n pending.set(id, { resolve: resolve as (value: unknown) => void, reject, timer })\n postMessage(payload)\n })\n },\n\n on<T = unknown>(name: string, listener: (data: T) => void): () => void {\n const win = appWindow()\n if (win) attach(win)\n\n let subscribers = listeners.get(name)\n if (!subscribers) {\n subscribers = new Set()\n listeners.set(name, subscribers)\n }\n const typed = listener as Listener\n subscribers.add(typed)\n\n return () => {\n subscribers.delete(typed)\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,31 @@
1
+ import { A as AppInfo } from '../protocol-DcOkx09n.cjs';
2
+ export { a as ErrorCode } from '../protocol-DcOkx09n.cjs';
3
+
4
+ interface BridgeWindow {
5
+ __MOBILE_APP_BRIDGE__?: AppInfo;
6
+ ReactNativeWebView?: {
7
+ postMessage(message: string): void;
8
+ };
9
+ addEventListener(type: string, listener: (event: Event) => void): void;
10
+ }
11
+ interface CallOptions {
12
+ /** Milliseconds to wait for the native response. Defaults to 30s; 0 disables the timeout. */
13
+ timeout?: number;
14
+ }
15
+ interface BridgeClient {
16
+ readonly isApp: boolean;
17
+ readonly info: AppInfo | null;
18
+ call<T = unknown>(method: string, params?: unknown, options?: CallOptions): Promise<T>;
19
+ on<T = unknown>(name: string, listener: (data: T) => void): () => void;
20
+ }
21
+ declare function createBridgeClient(getWindow: () => BridgeWindow | undefined): BridgeClient;
22
+
23
+ declare class BridgeError extends Error {
24
+ readonly code: string;
25
+ constructor(code: string, message: string);
26
+ }
27
+
28
+ /** Shared client for the current page. Safe to import during SSR: it only reads `window` when used. */
29
+ declare const bridge: BridgeClient;
30
+
31
+ export { AppInfo, type BridgeClient, BridgeError, type BridgeWindow, type CallOptions, bridge, createBridgeClient };
@@ -0,0 +1,31 @@
1
+ import { A as AppInfo } from '../protocol-DcOkx09n.js';
2
+ export { a as ErrorCode } from '../protocol-DcOkx09n.js';
3
+
4
+ interface BridgeWindow {
5
+ __MOBILE_APP_BRIDGE__?: AppInfo;
6
+ ReactNativeWebView?: {
7
+ postMessage(message: string): void;
8
+ };
9
+ addEventListener(type: string, listener: (event: Event) => void): void;
10
+ }
11
+ interface CallOptions {
12
+ /** Milliseconds to wait for the native response. Defaults to 30s; 0 disables the timeout. */
13
+ timeout?: number;
14
+ }
15
+ interface BridgeClient {
16
+ readonly isApp: boolean;
17
+ readonly info: AppInfo | null;
18
+ call<T = unknown>(method: string, params?: unknown, options?: CallOptions): Promise<T>;
19
+ on<T = unknown>(name: string, listener: (data: T) => void): () => void;
20
+ }
21
+ declare function createBridgeClient(getWindow: () => BridgeWindow | undefined): BridgeClient;
22
+
23
+ declare class BridgeError extends Error {
24
+ readonly code: string;
25
+ constructor(code: string, message: string);
26
+ }
27
+
28
+ /** Shared client for the current page. Safe to import during SSR: it only reads `window` when used. */
29
+ declare const bridge: BridgeClient;
30
+
31
+ export { AppInfo, type BridgeClient, BridgeError, type BridgeWindow, type CallOptions, bridge, createBridgeClient };