@webview-bridge/react-native 1.7.0-rc.0 → 1.7.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/package.json +4 -3
- package/src/createWebView.tsx +346 -0
- package/src/error.ts +6 -0
- package/src/index.ts +6 -0
- package/src/integrations/bridge.ts +138 -0
- package/src/integrations/console.ts +57 -0
- package/src/integrations/handleRegisterWebMethod.ts +46 -0
- package/src/integrations/postMessageSchema.ts +8 -0
- package/src/types/webview.ts +7 -0
- package/src/useBridge.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webview-bridge/react-native",
|
|
3
|
-
"version": "1.7.0
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fully Type-Safe Integration for React Native WebView and Web",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
|
+
"src",
|
|
27
28
|
"package.json"
|
|
28
29
|
],
|
|
29
30
|
"devDependencies": {
|
|
@@ -38,8 +39,8 @@
|
|
|
38
39
|
"react-native-webview": "*"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@webview-bridge/utils": "1.7.0
|
|
42
|
-
"@webview-bridge/types": "1.7.0
|
|
42
|
+
"@webview-bridge/utils": "1.7.0",
|
|
43
|
+
"@webview-bridge/types": "1.7.0",
|
|
43
44
|
"use-sync-external-store": "^1.2.0"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Bridge,
|
|
3
|
+
BridgeStore,
|
|
4
|
+
KeyOfOrString,
|
|
5
|
+
Parser,
|
|
6
|
+
ParserSchema,
|
|
7
|
+
Primitive,
|
|
8
|
+
} from "@webview-bridge/types";
|
|
9
|
+
import { createEvents } from "@webview-bridge/utils";
|
|
10
|
+
import React, {
|
|
11
|
+
forwardRef,
|
|
12
|
+
useEffect,
|
|
13
|
+
useImperativeHandle,
|
|
14
|
+
useLayoutEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
} from "react";
|
|
18
|
+
import type { WebViewMessageEvent, WebViewProps } from "react-native-webview";
|
|
19
|
+
import WebView from "react-native-webview";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
handleBridge,
|
|
23
|
+
INJECT_BRIDGE_METHODS,
|
|
24
|
+
INJECT_BRIDGE_STATE,
|
|
25
|
+
SAFE_NATIVE_EMITTER_EMIT,
|
|
26
|
+
} from "./integrations/bridge";
|
|
27
|
+
import { handleLog, INJECT_DEBUG, LogType } from "./integrations/console";
|
|
28
|
+
import { handleRegisterWebMethod } from "./integrations/handleRegisterWebMethod";
|
|
29
|
+
import type { BridgeWebView } from "./types/webview";
|
|
30
|
+
|
|
31
|
+
export type CreateWebViewArgs<
|
|
32
|
+
BridgeObject extends Bridge,
|
|
33
|
+
PostMessageSchema extends ParserSchema<any>,
|
|
34
|
+
> = {
|
|
35
|
+
/**
|
|
36
|
+
* The bridge object to be used in the WebView.
|
|
37
|
+
* @example
|
|
38
|
+
import { createWebView, bridge } from "@webview-bridge/react-native";
|
|
39
|
+
import InAppBrowser from "react-native-inappbrowser-reborn";
|
|
40
|
+
|
|
41
|
+
// Register functions in the bridge object in your React Native code
|
|
42
|
+
export const appBridge = bridge({
|
|
43
|
+
async getMessage() {
|
|
44
|
+
return "Hello, I'm native";
|
|
45
|
+
},
|
|
46
|
+
async sum(a: number, b: number) {
|
|
47
|
+
return a + b;
|
|
48
|
+
},
|
|
49
|
+
async openInAppBrowser(url: string) {
|
|
50
|
+
if (await InAppBrowser.isAvailable()) {
|
|
51
|
+
await InAppBrowser.open(url);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
// ... Add more functions as needed
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const { WebView } = createWebView({
|
|
58
|
+
bridge: appBridge,
|
|
59
|
+
debug: true, // Enable console.log visibility in the native WebView
|
|
60
|
+
});
|
|
61
|
+
*/
|
|
62
|
+
bridge: BridgeStore<BridgeObject>;
|
|
63
|
+
/**
|
|
64
|
+
* If `true`, the console.log visibility in the WebView is enabled.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
debug?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Set the timeout in milliseconds for the response from the web method.
|
|
70
|
+
* @default 2000
|
|
71
|
+
*/
|
|
72
|
+
responseTimeout?: number;
|
|
73
|
+
/**
|
|
74
|
+
* The schema for the postMessage method.
|
|
75
|
+
* @link https://gronxb.github.io/webview-bridge/using-a-post-message.html
|
|
76
|
+
*/
|
|
77
|
+
postMessageSchema?: PostMessageSchema;
|
|
78
|
+
/**
|
|
79
|
+
* Callback function when a method that is not defined in the bridge is called.
|
|
80
|
+
* @link https://gronxb.github.io/webview-bridge/backward-compatibility/new-method.html#react-native-part
|
|
81
|
+
*/
|
|
82
|
+
fallback?: (method: keyof BridgeObject) => void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type WebMethod<T> = T & {
|
|
86
|
+
isReady: boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a WebView component that can communicate with the bridge.
|
|
91
|
+
* @link https://gronxb.github.io/webview-bridge/getting-started.html
|
|
92
|
+
* @example
|
|
93
|
+
import { createWebView, bridge } from "@webview-bridge/react-native";
|
|
94
|
+
import InAppBrowser from "react-native-inappbrowser-reborn";
|
|
95
|
+
|
|
96
|
+
// Register functions in the bridge object in your React Native code
|
|
97
|
+
export const appBridge = bridge({
|
|
98
|
+
async getMessage() {
|
|
99
|
+
return "Hello, I'm native";
|
|
100
|
+
},
|
|
101
|
+
async sum(a: number, b: number) {
|
|
102
|
+
return a + b;
|
|
103
|
+
},
|
|
104
|
+
async openInAppBrowser(url: string) {
|
|
105
|
+
if (await InAppBrowser.isAvailable()) {
|
|
106
|
+
await InAppBrowser.open(url);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
// ... Add more functions as needed
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const { WebView } = createWebView({
|
|
113
|
+
bridge: appBridge,
|
|
114
|
+
debug: true, // Enable console.log visibility in the native WebView
|
|
115
|
+
});
|
|
116
|
+
*/
|
|
117
|
+
export const createWebView = <
|
|
118
|
+
BridgeObject extends Bridge,
|
|
119
|
+
PostMessageSchema extends ParserSchema<any>,
|
|
120
|
+
>({
|
|
121
|
+
bridge,
|
|
122
|
+
debug,
|
|
123
|
+
responseTimeout = 2000,
|
|
124
|
+
postMessageSchema,
|
|
125
|
+
fallback,
|
|
126
|
+
}: CreateWebViewArgs<BridgeObject, PostMessageSchema>) => {
|
|
127
|
+
const WebMethod = {
|
|
128
|
+
current: {
|
|
129
|
+
isReady: false,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const webviewRefList: React.RefObject<BridgeWebView>[] = [];
|
|
134
|
+
const emitter = createEvents();
|
|
135
|
+
|
|
136
|
+
bridge.subscribe((state) => {
|
|
137
|
+
for (const ref of webviewRefList) {
|
|
138
|
+
ref?.current?.injectJavaScript(
|
|
139
|
+
SAFE_NATIVE_EMITTER_EMIT("bridgeStateChange", state),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
/**
|
|
146
|
+
* Sends an event from React Native to the Web.
|
|
147
|
+
* @link https://gronxb.github.io/webview-bridge/using-a-post-message.html
|
|
148
|
+
*/
|
|
149
|
+
postMessage: <
|
|
150
|
+
EventName extends KeyOfOrString<PostMessageSchema>,
|
|
151
|
+
Args extends Parser<PostMessageSchema, EventName>,
|
|
152
|
+
>(
|
|
153
|
+
eventName: EventName,
|
|
154
|
+
args: Args,
|
|
155
|
+
) => {
|
|
156
|
+
let _args: any = args;
|
|
157
|
+
if (postMessageSchema) {
|
|
158
|
+
_args = postMessageSchema[eventName].validate(args);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const ref of webviewRefList) {
|
|
162
|
+
ref?.current?.injectJavaScript(
|
|
163
|
+
SAFE_NATIVE_EMITTER_EMIT(`postMessage/${String(eventName)}`, _args),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
WebView: forwardRef<BridgeWebView, WebViewProps>((props, ref) => {
|
|
168
|
+
const webviewRef = useRef<WebView>(null);
|
|
169
|
+
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
webviewRefList.push(webviewRef);
|
|
172
|
+
return () => {
|
|
173
|
+
webviewRefList.pop();
|
|
174
|
+
};
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
const initData = useMemo(() => {
|
|
178
|
+
const bridgeMethods = Object.entries(bridge.getState() ?? {})
|
|
179
|
+
.filter(([_, bridge]) => typeof bridge === "function")
|
|
180
|
+
.map(([name]) => name);
|
|
181
|
+
const initialState = Object.fromEntries(
|
|
182
|
+
Object.entries(bridge.getState() ?? {}).filter(
|
|
183
|
+
([_, value]) => typeof value !== "function",
|
|
184
|
+
),
|
|
185
|
+
) as Record<string, Primitive>;
|
|
186
|
+
return { bridgeMethods, initialState };
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
webviewRef.current?.injectJavaScript(
|
|
191
|
+
SAFE_NATIVE_EMITTER_EMIT("hydrate", initData),
|
|
192
|
+
);
|
|
193
|
+
}, [initData]);
|
|
194
|
+
|
|
195
|
+
useImperativeHandle(ref, () => webviewRef.current as BridgeWebView, []);
|
|
196
|
+
|
|
197
|
+
const handleMessage = async (event: WebViewMessageEvent) => {
|
|
198
|
+
props.onMessage?.(event);
|
|
199
|
+
|
|
200
|
+
if (!webviewRef.current) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { type, body } = JSON.parse(event.nativeEvent.data);
|
|
204
|
+
|
|
205
|
+
switch (type) {
|
|
206
|
+
case "log": {
|
|
207
|
+
const { method, args } = body as {
|
|
208
|
+
method: LogType;
|
|
209
|
+
args: string;
|
|
210
|
+
};
|
|
211
|
+
debug && handleLog(method, args);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case "bridge": {
|
|
215
|
+
const { method, args, eventId } = body as {
|
|
216
|
+
method: string;
|
|
217
|
+
args: unknown[];
|
|
218
|
+
eventId: string;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
handleBridge({
|
|
222
|
+
bridge,
|
|
223
|
+
method,
|
|
224
|
+
args,
|
|
225
|
+
eventId,
|
|
226
|
+
webview: webviewRef.current,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
case "getBridgeState": {
|
|
231
|
+
for (const ref of webviewRefList) {
|
|
232
|
+
ref?.current?.injectJavaScript(
|
|
233
|
+
SAFE_NATIVE_EMITTER_EMIT(
|
|
234
|
+
"bridgeStateChange",
|
|
235
|
+
bridge.getState(),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
case "registerWebMethod": {
|
|
242
|
+
const { bridgeNames } = body as {
|
|
243
|
+
bridgeNames: string[];
|
|
244
|
+
};
|
|
245
|
+
Object.assign(
|
|
246
|
+
WebMethod.current,
|
|
247
|
+
handleRegisterWebMethod(
|
|
248
|
+
emitter,
|
|
249
|
+
webviewRef.current,
|
|
250
|
+
bridgeNames,
|
|
251
|
+
responseTimeout,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
WebMethod.current.isReady = true;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case "webMethodResponse": {
|
|
258
|
+
const { eventId, funcName, value } = body as {
|
|
259
|
+
eventId: string;
|
|
260
|
+
funcName: string;
|
|
261
|
+
value: unknown;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
emitter.emit(`${funcName}-${eventId}`, value);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "webMethodError": {
|
|
268
|
+
const { eventId, funcName } = body as {
|
|
269
|
+
eventId: string;
|
|
270
|
+
funcName: string;
|
|
271
|
+
value: unknown;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
emitter.emit(`${funcName}-${eventId}`, {}, true);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
case "fallback": {
|
|
278
|
+
const { method } = body as {
|
|
279
|
+
method: keyof BridgeObject;
|
|
280
|
+
};
|
|
281
|
+
fallback?.(method);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<WebView
|
|
289
|
+
{...props}
|
|
290
|
+
ref={webviewRef}
|
|
291
|
+
onMessage={handleMessage}
|
|
292
|
+
injectedJavaScriptBeforeContentLoaded={[
|
|
293
|
+
INJECT_BRIDGE_METHODS(initData.bridgeMethods),
|
|
294
|
+
INJECT_BRIDGE_STATE(initData.initialState),
|
|
295
|
+
props.injectedJavaScriptBeforeContentLoaded,
|
|
296
|
+
"true;",
|
|
297
|
+
]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join("\n")}
|
|
300
|
+
injectedJavaScript={[
|
|
301
|
+
debug && INJECT_DEBUG,
|
|
302
|
+
props.injectedJavaScript,
|
|
303
|
+
"true;",
|
|
304
|
+
]
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.join("\n")}
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
}),
|
|
310
|
+
/**
|
|
311
|
+
* @deprecated Use `postMessage` instead. And complete the type through the `postMessageSchema` option.
|
|
312
|
+
* @see https://gronxb.github.io/webview-bridge/using-a-post-message.html
|
|
313
|
+
* @example
|
|
314
|
+
import { createWebView, postMessageSchema } from "@webview-bridge/react-native";
|
|
315
|
+
import { z } from "zod";
|
|
316
|
+
|
|
317
|
+
const appPostMessageSchema = postMessageSchema({
|
|
318
|
+
eventName1: z.object({
|
|
319
|
+
message: z.string(),
|
|
320
|
+
}),
|
|
321
|
+
eventName2: z.string(),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
// Export the event schema to be used in the web application
|
|
326
|
+
export type AppPostMessageSchema = typeof appPostMessageSchema;
|
|
327
|
+
|
|
328
|
+
// When you bridge a webview, a postMessage is extracted.
|
|
329
|
+
export const { postMessage } = createWebView({
|
|
330
|
+
postMessageSchema: appPostMessageSchema, // Pass in the your schema. This is optional, so if the type doesn't matter to you, you don't need to include it.
|
|
331
|
+
// ..
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// usage
|
|
335
|
+
postMessage("eventName1", {
|
|
336
|
+
message: "test",
|
|
337
|
+
});
|
|
338
|
+
postMessage("eventName2", "test");
|
|
339
|
+
*/
|
|
340
|
+
linkWebMethod<T>() {
|
|
341
|
+
return WebMethod as {
|
|
342
|
+
current: WebMethod<T>;
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
};
|
package/src/error.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Bridge,
|
|
3
|
+
BridgeStore,
|
|
4
|
+
OnlyJSON,
|
|
5
|
+
Primitive,
|
|
6
|
+
} from "@webview-bridge/types";
|
|
7
|
+
import { equals, removeUndefinedKeys } from "@webview-bridge/utils";
|
|
8
|
+
import WebView from "react-native-webview";
|
|
9
|
+
|
|
10
|
+
export type StoreCallback<T> = ({
|
|
11
|
+
get,
|
|
12
|
+
set,
|
|
13
|
+
}: {
|
|
14
|
+
get: () => T;
|
|
15
|
+
set: (newState: Partial<OnlyJSON<T>>) => void;
|
|
16
|
+
}) => T;
|
|
17
|
+
|
|
18
|
+
export const bridge = <T extends Bridge>(
|
|
19
|
+
procedures: T | StoreCallback<T>,
|
|
20
|
+
): BridgeStore<T> => {
|
|
21
|
+
const getState = () => state;
|
|
22
|
+
|
|
23
|
+
const setState = (newState: Partial<OnlyJSON<T>>) => {
|
|
24
|
+
const _newState = {
|
|
25
|
+
...state,
|
|
26
|
+
...removeUndefinedKeys(newState),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (equals(state, _newState)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const prevState = state;
|
|
34
|
+
state = _newState;
|
|
35
|
+
emitChange(state, prevState);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let state: T =
|
|
39
|
+
typeof procedures === "function"
|
|
40
|
+
? procedures({
|
|
41
|
+
get: getState,
|
|
42
|
+
set: setState,
|
|
43
|
+
})
|
|
44
|
+
: procedures;
|
|
45
|
+
|
|
46
|
+
const listeners = new Set<(newState: T, prevState: T) => void>();
|
|
47
|
+
|
|
48
|
+
const emitChange = (newState: T, prevState: T) => {
|
|
49
|
+
for (const listener of listeners) {
|
|
50
|
+
listener(newState, prevState);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const subscribe = (listener: (newState: T, prevState: T) => void) => {
|
|
55
|
+
listeners.add(listener);
|
|
56
|
+
return () => listeners.delete(listener);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
getState,
|
|
61
|
+
setState,
|
|
62
|
+
subscribe,
|
|
63
|
+
} as BridgeStore<T>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type HandleBridgeArgs<ArgType = unknown> = {
|
|
67
|
+
bridge: BridgeStore<Bridge>;
|
|
68
|
+
method: string;
|
|
69
|
+
args?: ArgType[];
|
|
70
|
+
webview: WebView;
|
|
71
|
+
eventId: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const handleBridge = async ({
|
|
75
|
+
bridge,
|
|
76
|
+
method,
|
|
77
|
+
args,
|
|
78
|
+
webview,
|
|
79
|
+
eventId,
|
|
80
|
+
}: HandleBridgeArgs) => {
|
|
81
|
+
const _bridge = bridge.getState();
|
|
82
|
+
|
|
83
|
+
const _method = _bridge[method];
|
|
84
|
+
const handleThrow = () => {
|
|
85
|
+
webview.injectJavaScript(SAFE_NATIVE_EMITTER_THROW(`${method}-${eventId}`));
|
|
86
|
+
};
|
|
87
|
+
if (!(method in _bridge)) {
|
|
88
|
+
handleThrow();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (typeof _method !== "function") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await _method?.(...(args ?? []));
|
|
97
|
+
|
|
98
|
+
webview.injectJavaScript(
|
|
99
|
+
SAFE_NATIVE_EMITTER_EMIT(`${method}-${eventId}`, response),
|
|
100
|
+
);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
handleThrow();
|
|
103
|
+
console.error(error);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const INJECT_BRIDGE_METHODS = (bridgeNames: string[]) => `
|
|
108
|
+
window.__bridgeMethods__ = ${JSON.stringify(bridgeNames)};
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
export const INJECT_BRIDGE_STATE = (
|
|
112
|
+
initialState: Record<string, Primitive>,
|
|
113
|
+
) => `
|
|
114
|
+
window.__bridgeInitialState__ = ${JSON.stringify(initialState)};
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
export const SAFE_NATIVE_EMITTER_EMIT = (eventName: string, data: unknown) => {
|
|
118
|
+
const dataString = JSON.stringify(data);
|
|
119
|
+
return `
|
|
120
|
+
if (window.nativeEmitter) {
|
|
121
|
+
window.nativeEmitter.emit('${eventName}', ${dataString});
|
|
122
|
+
} else {
|
|
123
|
+
window.nativeBatchedEvents = window.nativeBatchedEvents || [];
|
|
124
|
+
window.nativeBatchedEvents.push(['${eventName}', ${dataString}]);
|
|
125
|
+
}
|
|
126
|
+
true;
|
|
127
|
+
`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const SAFE_NATIVE_EMITTER_THROW = (eventName: string) => `
|
|
131
|
+
if (window.nativeEmitter) {
|
|
132
|
+
window.nativeEmitter.emit('${eventName}', {}, true);
|
|
133
|
+
} else {
|
|
134
|
+
window.nativeBatchedEvents = window.nativeBatchedEvents || [];
|
|
135
|
+
window.nativeBatchedEvents.push(['${eventName}', {}, true]);
|
|
136
|
+
}
|
|
137
|
+
true;
|
|
138
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const INJECT_DEBUG = `
|
|
2
|
+
{
|
|
3
|
+
const originalConsoleLog = console.log;
|
|
4
|
+
const originalConsoleError = console.error;
|
|
5
|
+
const originalConsoleWarn = console.warn;
|
|
6
|
+
|
|
7
|
+
console.log = function() {
|
|
8
|
+
var message = JSON.stringify(Array.from(arguments));
|
|
9
|
+
window.ReactNativeWebView?.postMessage(
|
|
10
|
+
JSON.stringify({ type: "log", body: { method: "log", args: message } }),
|
|
11
|
+
);
|
|
12
|
+
originalConsoleLog.apply(console, arguments);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
console.error = function() {
|
|
16
|
+
var message = JSON.stringify(Array.from(arguments));
|
|
17
|
+
window.ReactNativeWebView?.postMessage(
|
|
18
|
+
JSON.stringify({ type: "log", body: { method: "error", args: message } }),
|
|
19
|
+
);
|
|
20
|
+
originalConsoleError.apply(console, arguments);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
console.warn = function() {
|
|
24
|
+
var message = JSON.stringify(Array.from(arguments));
|
|
25
|
+
window.ReactNativeWebView?.postMessage(
|
|
26
|
+
JSON.stringify({ type: "log", body: { method: "warn", args: message } }),
|
|
27
|
+
);
|
|
28
|
+
originalConsoleWarn.apply(console, arguments);
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export type LogType = "log" | "error" | "warn";
|
|
34
|
+
|
|
35
|
+
export const handleLog = (type: LogType, message: string) => {
|
|
36
|
+
const [formatMessage, ...parsedMessage] = JSON.parse(message);
|
|
37
|
+
const webviewMark = "(WebView) ";
|
|
38
|
+
|
|
39
|
+
const webviewMarkedMessage =
|
|
40
|
+
typeof formatMessage === "string"
|
|
41
|
+
? [webviewMark + formatMessage, ...parsedMessage]
|
|
42
|
+
: [webviewMark, formatMessage, ...parsedMessage];
|
|
43
|
+
switch (type) {
|
|
44
|
+
case "log": {
|
|
45
|
+
console.log(...webviewMarkedMessage);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "error": {
|
|
49
|
+
console.error(...webviewMarkedMessage);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "warn": {
|
|
53
|
+
console.warn(...webviewMarkedMessage);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Bridge } from "@webview-bridge/types";
|
|
2
|
+
import {
|
|
3
|
+
createRandomId,
|
|
4
|
+
createResolver,
|
|
5
|
+
type EventEmitter,
|
|
6
|
+
timeout,
|
|
7
|
+
} from "@webview-bridge/utils";
|
|
8
|
+
import WebView from "react-native-webview";
|
|
9
|
+
|
|
10
|
+
import { WebMethodError } from "../error";
|
|
11
|
+
|
|
12
|
+
export const handleRegisterWebMethod = (
|
|
13
|
+
emitter: EventEmitter,
|
|
14
|
+
webview: WebView,
|
|
15
|
+
bridgeNames: string[],
|
|
16
|
+
responseTimeout: number,
|
|
17
|
+
) => {
|
|
18
|
+
return bridgeNames.reduce((acc, methodName) => {
|
|
19
|
+
acc[methodName] = async (...args: unknown[]) => {
|
|
20
|
+
const eventId = createRandomId();
|
|
21
|
+
|
|
22
|
+
return Promise.race([
|
|
23
|
+
createResolver({
|
|
24
|
+
emitter,
|
|
25
|
+
methodName,
|
|
26
|
+
eventId,
|
|
27
|
+
evaluate: () => {
|
|
28
|
+
webview.injectJavaScript(
|
|
29
|
+
`
|
|
30
|
+
window.webEmitter.emit('${methodName}', '${eventId}', ${JSON.stringify(
|
|
31
|
+
args,
|
|
32
|
+
)});
|
|
33
|
+
|
|
34
|
+
true;
|
|
35
|
+
`,
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
failHandler: new WebMethodError(methodName),
|
|
39
|
+
}),
|
|
40
|
+
timeout(responseTimeout),
|
|
41
|
+
]);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return acc;
|
|
45
|
+
}, {} as Bridge);
|
|
46
|
+
};
|
package/src/useBridge.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Bridge, BridgeStore, ExtractStore } from "@webview-bridge/types";
|
|
2
|
+
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector.js";
|
|
3
|
+
|
|
4
|
+
export function useBridge<T extends Bridge>(
|
|
5
|
+
store: BridgeStore<T>,
|
|
6
|
+
): ExtractStore<BridgeStore<T>>;
|
|
7
|
+
|
|
8
|
+
export function useBridge<
|
|
9
|
+
T extends Bridge,
|
|
10
|
+
U extends ExtractStore<BridgeStore<T>>,
|
|
11
|
+
V,
|
|
12
|
+
>(store: BridgeStore<T>, selector?: (state: U) => V): V;
|
|
13
|
+
|
|
14
|
+
export function useBridge<T extends Bridge, U>(
|
|
15
|
+
store: BridgeStore<T>,
|
|
16
|
+
selector?: (state: T) => U,
|
|
17
|
+
): U {
|
|
18
|
+
const $selector = selector ?? ((state: T) => state as unknown as U);
|
|
19
|
+
|
|
20
|
+
return useSyncExternalStoreWithSelector(
|
|
21
|
+
store.subscribe,
|
|
22
|
+
store.getState,
|
|
23
|
+
store.getState,
|
|
24
|
+
$selector,
|
|
25
|
+
);
|
|
26
|
+
}
|