@fawadmahmoodwashmen/washmen-js-native-bridge
v1.5.36
Published
WM bidirectional JSON-RPC WebView bridge for React (web) + React Native (wm:2 protocol)
Maintainers
Readme
washmen-js-native-bridge
Reusable bidirectional JSON-RPC bridge between a React site (running inside react-native-webview) and React Native.
- Protocol:
wm: 2withrpc: 1(web → native) andrpc: 3(web reply to nativeinvokeWebMethod). - Canonical implementation surface:
src/wm-webview-bridge.ts(bootstrap string, parsers, RN hook).
Published to the public npm registry — default install, no .npmrc required.
npm install @fawadmahmoodwashmen/washmen-js-native-bridgePeer: react ≥ 18. Optional: react-native, react-native-webview (RN app).
Entry points
| Import path | Use in |
|-------------|--------|
| @fawadmahmoodwashmen/washmen-js-native-bridge | WebView bundles — Shoe Care, Finery, order-tracking-live: createNativeRpcClient, contracts, instruction RPCs |
| @fawadmahmoodwashmen/washmen-js-native-bridge/native | React Native host only (customer-app): useWmWebViewBridge, RPC parsers, orderTrackingWebViewNativeMethodIds |
Do not import /native from Vite web apps. Do not import the main entry from Metro for RN host code (use /native).
Order tracking: order-tracking-native-contracts.ts includes OrderTrackingOrderData.instructions, updateOrderInstructions, uploadSpecialAttentionImage, Services Selection navigation RPCs (openOrderPricingPage, openWashFoldPlusPage, openWashFoldTermsPage), and the Before & After share RPC shareBeforeAfterImage (OrderTrackingShareBeforeAfterImageArgs) wire types + method ids. Business mapping lives in customer-app.
1.6.0 (breaking): portal hosting moved into order-tracking-live. Removed
openOrderPortalConfirmationPage,setCustomerPortalV2WarmTarget,openBeforeAfterPage(+ their types/parsers); addedshareBeforeAfterImage. Seedocs/washmen-webview-bridge-architecture.md.
Hidden order statuses (real-data): ORDER_TRACKING_CUSTOMER_FACING_STATUS + resolveCustomerFacingOrderStatus() collapse internal backend statuses (pickup_assigned, dropoff_assigned, dropoff_completed, payment_fixed_order_delivery_on_pending) to customer-facing primaries before list labels, heroes, and native detail URLs. QA demo routes in ORDER_TRACKING_STATUS_TO_ROUTE (e.g. /pickup-assigned) remain for /orders2 review only.
Shoe Care / Finery web (typical):
import {
createNativeRpcClient,
registerWebRpcHandlers,
shoecareEmbeddedSessionWebRpcMethodIds,
normalizeLaundryShoecareDriverTipFromPreference, // Shoe Care
normalizeFineryDriverTipFromPreference, // Finery
} from "@fawadmahmoodwashmen/washmen-js-native-bridge";Web → native (rpc: 1)
- RN injects
buildWmRpcBridgeBootstrapScript({ globalName: "globalRef" })before content loads. - That script defines:
window.__WM_RPC__._deliver(id, ok, jsonStringPayload)window.__WM_BIDIR__.register / unregister / fromNativewindow.globalRef(default) — aProxyso anyglobalRef.someMethod(...args)returns aPromiseand posts:
{ "wm": 2, "rpc": 1, "id": "<id>", "m": "<methodName>", "a": [/* args, JSON-serializable */] }- RN
onMessage(viauseWmWebViewBridge) runs your handler, theninjectJavaScript:
window.__WM_RPC__._deliver("id", true, "{\"quotes\":[]}"); true;(buildWmRpcDeliverScript does this safely.)
Web (typed):
import { createNativeRpcClient, WmNativeApi } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
// Augment WmNativeApi in your app (declaration merge) for IDE types.
const bridge = createNativeRpcClient<WmNativeApi>();
const quotes = await bridge.getQuotes({ author: "x" });If ReactNativeWebView is missing (Storybook / browser), createNativeRpcClient returns a dead proxy whose methods reject with a clear error.
Native shell back helper
import { BACK_BUTTON_ARIA_LABEL, useNativeShell } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
function FooterBackButton() {
const { requestBack } = useNativeShell();
return (
<button
type="button"
aria-label={BACK_BUTTON_ARIA_LABEL}
onClick={() => void requestBack()}
>
Back
</button>
);
}In SPAs, always use a real <button type="button"> and call requestBack() in onClick.
Do not rely on injected click-capture scripts for first-party React UI controls.
Native → web (invokeWebMethod + rpc: 3)
- Same bootstrap defines
__WM_BIDIR__.fromNative(callId, method, argsJsonStr). - RN calls
invokeWebMethod("getPageSummaryForNative", []), which **injectJavaScript**sfromNative(...). - The page runs the registered handler (see below), then
postMessage:
{ "wm": 2, "rpc": 3, "callId": "<id>", "ok": true, "payload": "<JSON string of result>" }On failure: "ok": false, payload is a JSON string like {"message":"…"}.
Web — register handlers (React):
import { WmWebBridgeProvider } from "@fawadmahmoodwashmen/washmen-js-native-bridge";
<WmWebBridgeProvider
handlers={{
getPageSummaryForNative: async () => [{ title: "Intro" }],
}}
>
<App />
</WmWebBridgeProvider>Or imperative: registerWebRpcHandlers({ ... }) → returns unregister().
React Native — useWmWebViewBridge
import WebView from "react-native-webview";
import { useRef, useCallback } from "react";
import { useWmWebViewBridge } from "@fawadmahmoodwashmen/washmen-js-native-bridge/native";
export function BridgeWebView() {
const ref = useRef<WebView>(null);
const getHandlers = useCallback(
() => ({
getQuotes: async (_ctx, args: unknown[]) => {
const q = args[0] as { author: string };
return [{ id: "1", text: `Hello ${q.author}` }];
},
}),
[],
);
const { injectedJavaScriptBeforeContentLoaded, onMessage, invokeWebMethod } =
useWmWebViewBridge(getHandlers, ref, { globalName: "globalRef" });
async function askWeb() {
const summary = await invokeWebMethod("getPageSummaryForNative", []);
console.log(summary);
}
return (
<WebView
ref={ref}
source={{ uri: "https://your-web-app.com" }}
injectedJavaScriptBeforeContentLoaded={injectedJavaScriptBeforeContentLoaded}
onMessage={onMessage}
/>
);
}onMessage ordering: useWmWebViewBridge / handleWmWebViewBridgeMessage parses rpc: 3 first, then rpc: 1, so web replies never look like new web-initiated RPCs.
Layout & scrolling (Android)
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.
Modular RN handlers (recommended)
Keep getHandlers as useCallback returning a small map; implement each method in its own module and compose:
const getHandlers = useCallback(
() => ({
...quotesHandlers(queryClient),
...authHandlers(store),
}),
[queryClient, store],
);Do not paste large business logic into the bootstrap string — only buildWmRpcBridgeBootstrapScript() (transport + Proxy).
Types: WmNativeApi / WmWebApi
Export interfaces in the package; augment in your apps so createNativeRpcClient / invokeWebMethod stay aligned:
declare module "@fawadmahmoodwashmen/washmen-js-native-bridge" {
interface WmNativeApi {
getQuotes(input: { author: string }): Promise<{ id: string; text: string }[]>;
}
interface WmWebApi {
getPageSummaryForNative(): Promise<{ title: string }[]>;
}
}(Optional next step: codegen from a shared bridge.contract.ts in a monorepo.)
Flow (bullets)
Web → native
- Web:
await globalRef.method(...args)(Proxy) →postMessagerpc:1. - Native: handler →
injectJavaScript→__WM_RPC__._deliver→ Promise resolves on web.
Native → web
- Native:
invokeWebMethod(method, args)→injectJavaScript→__WM_BIDIR__.fromNative. - Web: registered handler →
postMessagerpc:3. - Native:
onMessagematchescallId, resolvesinvokeWebMethodPromise.
Non-goals
No binary streams, no non-JSON payloads, no synchronous return across the WebView boundary (async-only from RN’s perspective for cross-runtime calls).
Releasing
Requires Node.js ≥ 20.12 (for release-it). No GITHUB_TOKEN needed: GitHub Releases are disabled; publishing is npm only.
GitHub environment Publishing with secret NPM_TOKEN. The workflow uses environment: Publishing so that secret is injected.
If your npm account uses 2FA, CI cannot enter an OTP. Create a classic Automation token (npm → Access Tokens → Generate New Token → Classic → type Automation). Automation tokens can publish from CI without --otp. Do not use a Publish-type token for unattended pipelines if npm returns EOTP. Update NPM_TOKEN in the Publishing environment with that value.
From a clean main working tree:
npm run releaseYou 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.
Dry run: npm run release:dry
License
MIT
