@fawadmahmoodwashmen/washmen-js-native-bridge 1.0.5
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 +21 -0
- package/README.md +237 -0
- package/dist/index.cjs +457 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +205 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +433 -0
- package/dist/index.js.map +1 -0
- package/dist/wm-webview-bridge.cjs +252 -0
- package/dist/wm-webview-bridge.cjs.map +1 -0
- package/dist/wm-webview-bridge.d.cts +114 -0
- package/dist/wm-webview-bridge.d.ts +114 -0
- package/dist/wm-webview-bridge.js +241 -0
- package/dist/wm-webview-bridge.js.map +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Washmen
|
|
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,237 @@
|
|
|
1
|
+
# washmen-js-native-bridge
|
|
2
|
+
|
|
3
|
+
Reusable **bidirectional JSON-RPC** bridge between a **React** site (running inside `react-native-webview`) and **React Native**.
|
|
4
|
+
|
|
5
|
+
- **Protocol:** `wm: 2` with `rpc: 1` (web → native) and `rpc: 3` (web reply to native `invokeWebMethod`).
|
|
6
|
+
- **Canonical implementation surface:** [`src/wm-webview-bridge.ts`](./src/wm-webview-bridge.ts) (bootstrap string, parsers, RN hook).
|
|
7
|
+
|
|
8
|
+
Published to the **public npm registry** — default install, no `.npmrc` required.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @fawadmahmoodwashmen/washmen-js-native-bridge
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Peer:** `react` ≥ 18. Optional: `react-native`, `react-native-webview` (RN app).
|
|
15
|
+
|
|
16
|
+
**Entry points**
|
|
17
|
+
|
|
18
|
+
| Import path | Use in |
|
|
19
|
+
|-------------|--------|
|
|
20
|
+
| `@fawadmahmoodwashmen/washmen-js-native-bridge` | React **web** (in WebView) + re-exports protocol helpers |
|
|
21
|
+
| `@fawadmahmoodwashmen/washmen-js-native-bridge/native` or `…/wm-webview-bridge` | React **Native** (bootstrap, parsers, `useWmWebViewBridge`) |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Web → native (`rpc: 1`)
|
|
26
|
+
|
|
27
|
+
1. RN injects `buildWmRpcBridgeBootstrapScript({ globalName: "globalRef" })` **before** content loads.
|
|
28
|
+
2. That script defines:
|
|
29
|
+
- `window.__WM_RPC__._deliver(id, ok, jsonStringPayload)`
|
|
30
|
+
- `window.__WM_BIDIR__.register / unregister / fromNative`
|
|
31
|
+
- `window.globalRef` (default) — a **`Proxy`** so **any** `globalRef.someMethod(...args)` returns a `Promise` and posts:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "wm": 2, "rpc": 1, "id": "<id>", "m": "<methodName>", "a": [/* args, JSON-serializable */] }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. RN `onMessage` (via `useWmWebViewBridge`) runs your handler, then **`injectJavaScript`**:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
window.__WM_RPC__._deliver("id", true, "{\"quotes\":[]}"); true;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
(`buildWmRpcDeliverScript` does this safely.)
|
|
44
|
+
|
|
45
|
+
**Web (typed):**
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { createNativeRpcClient, WmNativeApi } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
|
|
49
|
+
|
|
50
|
+
// Augment WmNativeApi in your app (declaration merge) for IDE types.
|
|
51
|
+
const bridge = createNativeRpcClient<WmNativeApi>();
|
|
52
|
+
const quotes = await bridge.getQuotes({ author: "x" });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If `ReactNativeWebView` is missing (Storybook / browser), `createNativeRpcClient` returns a **dead** proxy whose methods reject with a clear error.
|
|
56
|
+
|
|
57
|
+
### Native shell back helper
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { BACK_BUTTON_ARIA_LABEL, useNativeShell } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
|
|
61
|
+
|
|
62
|
+
function FooterBackButton() {
|
|
63
|
+
const { requestBack } = useNativeShell();
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
aria-label={BACK_BUTTON_ARIA_LABEL}
|
|
68
|
+
onClick={() => void requestBack()}
|
|
69
|
+
>
|
|
70
|
+
Back
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
In SPAs, always use a real `<button type="button">` and call `requestBack()` in `onClick`.
|
|
77
|
+
Do not rely on injected click-capture scripts for first-party React UI controls.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Native → web (`invokeWebMethod` + `rpc: 3`)
|
|
82
|
+
|
|
83
|
+
1. Same bootstrap defines `__WM_BIDIR__.fromNative(callId, method, argsJsonStr)`.
|
|
84
|
+
2. RN calls `invokeWebMethod("getPageSummaryForNative", [])`, which **`injectJavaScript`**s `fromNative(...)`.
|
|
85
|
+
3. The page runs the registered handler (see below), then **`postMessage`**:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{ "wm": 2, "rpc": 3, "callId": "<id>", "ok": true, "payload": "<JSON string of result>" }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
On failure: `"ok": false`, `payload` is a JSON string like `{"message":"…"}`.
|
|
92
|
+
|
|
93
|
+
**Web — register handlers (React):**
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { WmWebBridgeProvider } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
|
|
97
|
+
|
|
98
|
+
<WmWebBridgeProvider
|
|
99
|
+
handlers={{
|
|
100
|
+
getPageSummaryForNative: async () => [{ title: "Intro" }],
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<App />
|
|
104
|
+
</WmWebBridgeProvider>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or imperative: `registerWebRpcHandlers({ ... })` → returns `unregister()`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## React Native — `useWmWebViewBridge`
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import WebView from "react-native-webview";
|
|
115
|
+
import { useRef, useCallback } from "react";
|
|
116
|
+
import { useWmWebViewBridge } from "@fawadmahmoodwashmen/washmen-js-native-bridge/native";
|
|
117
|
+
|
|
118
|
+
export function BridgeWebView() {
|
|
119
|
+
const ref = useRef<WebView>(null);
|
|
120
|
+
const getHandlers = useCallback(
|
|
121
|
+
() => ({
|
|
122
|
+
getQuotes: async (_ctx, args: unknown[]) => {
|
|
123
|
+
const q = args[0] as { author: string };
|
|
124
|
+
return [{ id: "1", text: `Hello ${q.author}` }];
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
[],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const { injectedJavaScriptBeforeContentLoaded, onMessage, invokeWebMethod } =
|
|
131
|
+
useWmWebViewBridge(getHandlers, ref, { globalName: "globalRef" });
|
|
132
|
+
|
|
133
|
+
async function askWeb() {
|
|
134
|
+
const summary = await invokeWebMethod("getPageSummaryForNative", []);
|
|
135
|
+
console.log(summary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<WebView
|
|
140
|
+
ref={ref}
|
|
141
|
+
source={{ uri: "https://your-web-app.com" }}
|
|
142
|
+
injectedJavaScriptBeforeContentLoaded={injectedJavaScriptBeforeContentLoaded}
|
|
143
|
+
onMessage={onMessage}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**`onMessage` ordering:** `useWmWebViewBridge` / `handleWmWebViewBridgeMessage` parses **`rpc: 3` first**, then **`rpc: 1`**, so web replies never look like new web-initiated RPCs.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Layout & scrolling (Android)
|
|
154
|
+
|
|
155
|
+
If the `WebView` sits **inside** a `ScrollView`, give the WebView a **fixed height** (or `flex:1` inside a bounded parent) and set **`nestedScrollEnabled`** on Android so nested scrolling behaves.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Modular RN handlers (recommended)
|
|
160
|
+
|
|
161
|
+
Keep `getHandlers` as `useCallback` returning a **small map**; implement each method in its own module and compose:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const getHandlers = useCallback(
|
|
165
|
+
() => ({
|
|
166
|
+
...quotesHandlers(queryClient),
|
|
167
|
+
...authHandlers(store),
|
|
168
|
+
}),
|
|
169
|
+
[queryClient, store],
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Do **not** paste large business logic into the bootstrap string — only `buildWmRpcBridgeBootstrapScript()` (transport + Proxy).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Types: `WmNativeApi` / `WmWebApi`
|
|
178
|
+
|
|
179
|
+
Export interfaces in the package; **augment** in your apps so `createNativeRpcClient` / `invokeWebMethod` stay aligned:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
declare module "@fawadmahmoodwashmen/washmen-js-native-bridge" {
|
|
183
|
+
interface WmNativeApi {
|
|
184
|
+
getQuotes(input: { author: string }): Promise<{ id: string; text: string }[]>;
|
|
185
|
+
}
|
|
186
|
+
interface WmWebApi {
|
|
187
|
+
getPageSummaryForNative(): Promise<{ title: string }[]>;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
(Optional next step: codegen from a shared `bridge.contract.ts` in a monorepo.)
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Flow (bullets)
|
|
197
|
+
|
|
198
|
+
**Web → native**
|
|
199
|
+
|
|
200
|
+
- Web: `await globalRef.method(...args)` (Proxy) → `postMessage` `rpc:1`.
|
|
201
|
+
- Native: handler → `injectJavaScript` → `__WM_RPC__._deliver` → Promise resolves on web.
|
|
202
|
+
|
|
203
|
+
**Native → web**
|
|
204
|
+
|
|
205
|
+
- Native: `invokeWebMethod(method, args)` → `injectJavaScript` → `__WM_BIDIR__.fromNative`.
|
|
206
|
+
- Web: registered handler → `postMessage` `rpc:3`.
|
|
207
|
+
- Native: `onMessage` matches `callId`, resolves `invokeWebMethod` Promise.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Non-goals
|
|
212
|
+
|
|
213
|
+
No binary streams, no non-JSON payloads, no synchronous return across the WebView boundary (async-only from RN’s perspective for cross-runtime calls).
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Releasing
|
|
218
|
+
|
|
219
|
+
Requires **Node.js ≥ 20.12** (for `release-it`). No **`GITHUB_TOKEN`** needed: GitHub Releases are disabled; publishing is **npm only**.
|
|
220
|
+
|
|
221
|
+
GitHub **environment** `Publishing` with secret **`NPM_TOKEN`** ([npm access token](https://docs.npmjs.com/creating-and-viewing-access-tokens) that can publish this scope). The workflow job uses `environment: Publishing` so that secret is available.
|
|
222
|
+
|
|
223
|
+
From a clean `main` working tree:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npm run release
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
You choose **patch / minor / major**, confirm steps. That runs tests + typecheck, bumps `package.json` + lockfile, commits, pushes, and creates tag **`vX.Y.Z`**. Pushing the tag triggers the **Publish npm** GitHub Action, which runs `npm publish` to **registry.npmjs.org**.
|
|
230
|
+
|
|
231
|
+
Dry run: `npm run release:dry`
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT
|