wovvmap-webview-bridge 1.0.29 → 1.0.39

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.
Files changed (74) hide show
  1. package/README.md +1563 -228
  2. package/dist/handlers/WebBridgeHandlers.d.ts +3 -15
  3. package/dist/handlers/WebBridgeHandlers.d.ts.map +1 -1
  4. package/dist/handlers/WebBridgeHandlers.js +94 -53
  5. package/dist/index.d.ts +19 -3
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +19 -3
  8. package/dist/types/types.d.ts +277 -316
  9. package/dist/types/types.d.ts.map +1 -1
  10. package/dist/web.d.ts +19 -3
  11. package/dist/web.d.ts.map +1 -1
  12. package/dist/web.js +19 -3
  13. package/dist/webviewBridge/BridgeService.d.ts +34 -17
  14. package/dist/webviewBridge/BridgeService.d.ts.map +1 -1
  15. package/dist/webviewBridge/BridgeService.js +227 -51
  16. package/dist/webviewBridge/WebViewBridgeRef.d.ts +3 -3
  17. package/dist/webviewBridge/WebViewBridgeRef.d.ts.map +1 -1
  18. package/dist/webviewBridge/WebViewBridgeRef.js +1 -1
  19. package/dist/webviewBridge/WebViewScreen.d.ts.map +1 -1
  20. package/dist/webviewBridge/services/DirectionBridgeService.d.ts +32 -0
  21. package/dist/webviewBridge/services/DirectionBridgeService.d.ts.map +1 -0
  22. package/dist/webviewBridge/services/DirectionBridgeService.js +72 -0
  23. package/dist/webviewBridge/services/FloorBridgeService.d.ts +4 -0
  24. package/dist/webviewBridge/services/FloorBridgeService.d.ts.map +1 -0
  25. package/dist/webviewBridge/services/FloorBridgeService.js +11 -0
  26. package/dist/webviewBridge/services/SelectionBridgeService.d.ts +10 -0
  27. package/dist/webviewBridge/services/SelectionBridgeService.d.ts.map +1 -0
  28. package/dist/webviewBridge/services/SelectionBridgeService.js +43 -0
  29. package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts +7 -0
  30. package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts.map +1 -0
  31. package/dist/webviewBridge/services/StepNavigationBridgeService.js +15 -0
  32. package/dist/webviewBridge/services/ViewerBridgeService.d.ts +8 -0
  33. package/dist/webviewBridge/services/ViewerBridgeService.d.ts.map +1 -0
  34. package/dist/webviewBridge/services/ViewerBridgeService.js +20 -0
  35. package/dist/webviewBridge/services/ZoomBridgeService.d.ts +5 -0
  36. package/dist/webviewBridge/services/ZoomBridgeService.d.ts.map +1 -0
  37. package/dist/webviewBridge/services/ZoomBridgeService.js +9 -0
  38. package/dist/webviewBridge/store/useAmenityStore.d.ts +20 -0
  39. package/dist/webviewBridge/store/useAmenityStore.d.ts.map +1 -0
  40. package/dist/webviewBridge/store/useAmenityStore.js +6 -0
  41. package/dist/webviewBridge/store/useBridgeStorage.d.ts +14 -0
  42. package/dist/webviewBridge/store/useBridgeStorage.d.ts.map +1 -0
  43. package/dist/webviewBridge/store/useBridgeStorage.js +13 -0
  44. package/dist/webviewBridge/store/useCategoryStore.d.ts +20 -0
  45. package/dist/webviewBridge/store/useCategoryStore.d.ts.map +1 -0
  46. package/dist/webviewBridge/store/useCategoryStore.js +6 -0
  47. package/dist/webviewBridge/store/useDirectionStore.d.ts +55 -0
  48. package/dist/webviewBridge/store/useDirectionStore.d.ts.map +1 -0
  49. package/dist/webviewBridge/store/useDirectionStore.js +35 -0
  50. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts +14 -0
  51. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts.map +1 -0
  52. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.js +25 -0
  53. package/dist/webviewBridge/store/useFloorStore.d.ts +18 -0
  54. package/dist/webviewBridge/store/useFloorStore.d.ts.map +1 -0
  55. package/dist/webviewBridge/store/useFloorStore.js +8 -0
  56. package/dist/webviewBridge/store/useNavigationStore.d.ts +23 -0
  57. package/dist/webviewBridge/store/useNavigationStore.d.ts.map +1 -0
  58. package/dist/webviewBridge/store/useNavigationStore.js +18 -0
  59. package/dist/webviewBridge/store/useNodePointStore.d.ts +42 -0
  60. package/dist/webviewBridge/store/useNodePointStore.d.ts.map +1 -0
  61. package/dist/webviewBridge/store/useNodePointStore.js +19 -0
  62. package/dist/webviewBridge/store/useSelectionStore.d.ts +35 -0
  63. package/dist/webviewBridge/store/useSelectionStore.d.ts.map +1 -0
  64. package/dist/webviewBridge/store/useSelectionStore.js +32 -0
  65. package/dist/webviewBridge/store/useSubCategoryStore.d.ts +20 -0
  66. package/dist/webviewBridge/store/useSubCategoryStore.d.ts.map +1 -0
  67. package/dist/webviewBridge/store/useSubCategoryStore.js +6 -0
  68. package/dist/webviewBridge/store/useViewerStore.d.ts +35 -0
  69. package/dist/webviewBridge/store/useViewerStore.d.ts.map +1 -0
  70. package/dist/webviewBridge/store/useViewerStore.js +35 -0
  71. package/dist/webviewBridge/useBridgeStorage.d.ts +45 -0
  72. package/dist/webviewBridge/useBridgeStorage.d.ts.map +1 -0
  73. package/dist/webviewBridge/useBridgeStorage.js +43 -0
  74. package/package.json +1 -1
package/README.md CHANGED
@@ -1,289 +1,1624 @@
1
1
  # wovvmap-webview-bridge
2
2
 
3
- A typed bridge between React Native and a WebView for Wovvmap maps. It provides:
4
- - WebViewScreen wrapper for React Native WebView
5
- - BridgeService helpers to send events to the WebView
6
- - Zustand store (useBridgeStorage) that keeps incoming state in sync
7
- - Strongly typed message contracts
3
+ `wovvmap-webview-bridge` is the host-side SDK for embedding a WovvMap viewer inside a web iframe or a React Native WebView.
4
+
5
+ It gives the host app:
6
+
7
+ - iframe/WebView components
8
+ - typed bridge command services
9
+ - synced Zustand stores for viewer data
10
+ - event handlers for viewer interactions
11
+ - TypeScript types for categories, stores, directions, floors, navigation, and viewer configuration
12
+
13
+ The package is designed so a host developer does not need to create bridge command services manually. UI code should call the exported `*BridgeService` classes and read state from the exported stores.
14
+
15
+ ## Entry Points
16
+
17
+ Use the web entry point in React web apps:
18
+
19
+ ```ts
20
+ import {
21
+ WebIframeScreen,
22
+ DirectionBridgeService,
23
+ useCategoryStore,
24
+ } from "wovvmap-webview-bridge/web";
25
+ ```
26
+
27
+ Use the root entry point in React Native apps:
28
+
29
+ ```ts
30
+ import {
31
+ WebViewScreen,
32
+ DirectionBridgeService,
33
+ useCategoryStore,
34
+ } from "wovvmap-webview-bridge";
35
+ ```
8
36
 
9
37
  ## Installation
10
38
 
39
+ For web iframe hosts:
40
+
11
41
  ```bash
12
- npm install react-native-webview zustand
13
- # or
14
- yarn add react-native-webview zustand
42
+ npm install wovvmap-webview-bridge zustand
15
43
  ```
16
44
 
17
- ## Quick start
45
+ For React Native hosts:
46
+
47
+ ```bash
48
+ npm install wovvmap-webview-bridge zustand react-native-webview
49
+ ```
18
50
 
19
- ### Web (React TS) usage
51
+ ## Basic Web Iframe Setup
20
52
 
21
- Use the web entrypoint so you don't need `react-native` or `react-native-webview` in your web app.
53
+ The host app owns the map id. The package does not store `mapId` because each host decides which map to open.
22
54
 
23
55
  ```tsx
24
- import React from "react";
25
56
  import { WebIframeScreen } from "wovvmap-webview-bridge/web";
26
57
 
27
- export default function App() {
58
+ export function MapHost({ mapId }: { mapId: string | null }) {
59
+ if (!mapId) {
60
+ return null;
61
+ }
62
+
28
63
  return (
29
64
  <WebIframeScreen
30
- url="https://your-map-app-url.com"
31
- origin="https://your-map-app-url.com"
32
- onload={() => console.log("iframe loaded")}
33
- style={{ width: "100%", height: "100vh" }}
65
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
66
+ origin="https://viewer.example.com"
67
+ onload={() => console.log("Viewer iframe loaded")}
68
+ style={{ width: "100%", height: "100%", border: "none" }}
34
69
  />
35
70
  );
36
71
  }
37
72
  ```
38
73
 
39
- You can also attach to your own iframe ref:
74
+ For local development this can look like:
40
75
 
41
76
  ```tsx
42
- import React, { useEffect, useRef } from "react";
43
- import {
44
- attachIframeBridge,
45
- registerBridgeHandler,
46
- sendStartPointToBridge,
47
- } from "wovvmap-webview-bridge/web";
77
+ <WebIframeScreen
78
+ url={`http://localhost:5174/viewer/${mapId}?v=v2&template=embedded`}
79
+ origin="http://localhost:5174"
80
+ style={{ width: "100%", height: "100%", border: "none" }}
81
+ />
82
+ ```
83
+
84
+ ## Basic React Native Setup
85
+
86
+ ```tsx
87
+ import { WebViewScreen } from "wovvmap-webview-bridge";
88
+
89
+ export function MapWebView({ mapId }: { mapId: string }) {
90
+ return (
91
+ <WebViewScreen
92
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
93
+ onload={() => console.log("Viewer loaded")}
94
+ />
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## Optional Manual Iframe Setup
100
+
101
+ Use `attachIframeBridge` when you already render your own iframe.
102
+
103
+ ```tsx
104
+ import { useEffect, useRef } from "react";
105
+ import { attachIframeBridge } from "wovvmap-webview-bridge/web";
48
106
 
49
- export default function App() {
107
+ export function CustomIframe({ url }: { url: string }) {
50
108
  const iframeRef = useRef<HTMLIFrameElement>(null);
51
109
 
52
110
  useEffect(() => {
53
111
  if (!iframeRef.current) return;
54
112
 
55
- const cleanup = attachIframeBridge({
113
+ return attachIframeBridge({
56
114
  iframe: iframeRef.current,
57
- origin: "https://your-map-app-url.com",
58
- onLoad: () => sendStartPointToBridge("A1"),
115
+ origin: "https://viewer.example.com",
116
+ onLoad: () => console.log("loaded"),
59
117
  });
118
+ }, []);
60
119
 
61
- registerBridgeHandler("isSceneClick", (payload) => {
62
- console.log("Scene clicked", payload);
63
- });
120
+ return <iframe ref={iframeRef} src={url} title="Wovv Map" />;
121
+ }
122
+ ```
64
123
 
65
- return cleanup;
66
- }, []);
124
+ ## Recommended Import Surface
125
+
126
+ Most apps should import only from `"wovvmap-webview-bridge/web"` or `"wovvmap-webview-bridge"`.
127
+
128
+ ```ts
129
+ import {
130
+ WebIframeScreen,
131
+ DirectionBridgeService,
132
+ SelectionBridgeService,
133
+ FloorBridgeService,
134
+ StepNavigationBridgeService,
135
+ ZoomBridgeService,
136
+ ViewerBridgeService,
137
+ useBridgeStorage,
138
+ useViewerStore,
139
+ useCategoryStore,
140
+ useSubCategoryStore,
141
+ useAmenityStore,
142
+ useNodePointStore,
143
+ useSelectionStore,
144
+ useDirectionStore,
145
+ useFloorStore,
146
+ useNavigationStore,
147
+ useDirectionsRouteRequestStore,
148
+ registerBridgeHandler,
149
+ } from "wovvmap-webview-bridge/web";
150
+ ```
151
+
152
+ ## Exported Components
153
+
154
+ ### `WebIframeScreen`
155
+
156
+ Web-only component for embedding the WovvMap viewer in an iframe.
157
+
158
+ Props:
159
+
160
+ ```ts
161
+ type WebIframeScreenProps = {
162
+ url: string;
163
+ origin?: string;
164
+ onload?: () => void;
165
+ title?: string;
166
+ style?: React.CSSProperties;
167
+ };
168
+ ```
169
+
170
+ Use this in React web apps.
171
+
172
+ ### `WebViewScreen`
173
+
174
+ React Native component for embedding the WovvMap viewer in `react-native-webview`.
175
+
176
+ Props:
177
+
178
+ ```ts
179
+ type WebViewScreenProps = {
180
+ url: string;
181
+ onload?: () => void;
182
+ };
183
+ ```
184
+
185
+ Use this in React Native apps.
186
+
187
+ ### `attachIframeBridge`
188
+
189
+ Manual bridge attachment for custom iframe implementations.
190
+
191
+ ```ts
192
+ attachIframeBridge({
193
+ iframe,
194
+ origin,
195
+ onLoad,
196
+ });
197
+ ```
198
+
199
+ ## Command Services
200
+
201
+ Use these services from host UI. They update host-side package stores when needed and send the correct bridge event to the embedded viewer.
202
+
203
+ ### `DirectionBridgeService`
204
+
205
+ Controls origin, destination, route generation, path clearing, and origin/destination swap.
206
+
207
+ Methods:
208
+
209
+ ```ts
210
+ DirectionBridgeService.configure(config);
211
+ DirectionBridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
212
+ DirectionBridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
213
+ DirectionBridgeService.clearDestination(clearQuery);
214
+ DirectionBridgeService.clearOrigin();
215
+ DirectionBridgeService.swapOriginDestination();
216
+ DirectionBridgeService.generateDirectionsRoute(callbacks);
217
+ DirectionBridgeService.clearActivePath();
218
+ ```
219
+
220
+ Parameters:
221
+
222
+ - `nodeId`: a node key string. Multiple store locations can be joined with `"<->"`, for example `"2105,1601,3<->3166,1570,3"`.
223
+ - `cameraAnimate`: when `true`, viewer camera animates to the point.
224
+ - `clearSelection`: when `true`, viewer clears active category/subcategory/amenity selection.
225
+ - `clearQuery`: when `true`, your host input adapter clears the visible input text.
226
+
227
+ Basic destination example:
228
+
229
+ ```ts
230
+ DirectionBridgeService.setDestination("2105,1601,3", true, false);
231
+ ```
232
+
233
+ Basic origin example:
234
+
235
+ ```ts
236
+ DirectionBridgeService.setOrigin("4360,2330,5", true, true);
237
+ ```
238
+
239
+ Clear destination:
240
+
241
+ ```ts
242
+ DirectionBridgeService.clearDestination(true);
243
+ ```
244
+
245
+ Generate route:
246
+
247
+ ```ts
248
+ DirectionBridgeService.generateDirectionsRoute({
249
+ onSuccess: (result) => {
250
+ console.log("Route ready", result.navigationResult);
251
+ },
252
+ onError: (result) => {
253
+ console.log("Route failed", result.error);
254
+ },
255
+ });
256
+ ```
257
+
258
+ Swap from/to:
259
+
260
+ ```ts
261
+ DirectionBridgeService.swapOriginDestination();
262
+ ```
263
+
264
+ Clear drawn path:
265
+
266
+ ```ts
267
+ DirectionBridgeService.clearActivePath();
268
+ ```
269
+
270
+ #### Direction Input Adapter
271
+
272
+ The package does not own your input UI state. If your app has local "From" and "To" input text, connect it once with `configure`.
273
+
274
+ ```ts
275
+ import {
276
+ DirectionBridgeService,
277
+ SelectionBridgeService,
278
+ useNodePointStore,
279
+ } from "wovvmap-webview-bridge/web";
280
+ import { useDirectionInputStore } from "./store/useDirectionInputStore";
281
+
282
+ DirectionBridgeService.configure({
283
+ directionInputAdapter: {
284
+ setDestinationState: (query, selectedId) => {
285
+ useDirectionInputStore.getState().setDestinationState(query, selectedId);
286
+ },
287
+ setOriginState: (query, selectedId) => {
288
+ useDirectionInputStore.getState().setOriginState(query, selectedId);
289
+ },
290
+ clearDestinationState: (clearQuery) => {
291
+ useDirectionInputStore.getState().clearDestinationState(clearQuery);
292
+ },
293
+ clearOriginState: () => {
294
+ useDirectionInputStore.getState().clearOriginState();
295
+ },
296
+ setActiveDirectionField: (field) => {
297
+ useDirectionInputStore.getState().setActiveDirectionField(field);
298
+ },
299
+ getOriginQuery: () => useDirectionInputStore.getState().originQuery,
300
+ getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
301
+ },
302
+ resolveLocationName: (nodeId) => {
303
+ const nodeKey = nodeId.split("<->")[0];
304
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
305
+ return node?.assets?.locationName?.text || nodeKey;
306
+ },
307
+ onClearSelection: () => {
308
+ SelectionBridgeService.clearSelection();
309
+ },
310
+ });
311
+ ```
312
+
313
+ Why this exists:
314
+
315
+ - different companies can build different input UIs
316
+ - the SDK still updates local input text when `setOrigin`, `setDestination`, or `swapOriginDestination` is called
317
+ - SDK code does not import app-specific stores
318
+
319
+ If you do not configure an adapter, direction commands still work. Only local input text will not auto-update.
320
+
321
+ ### `SelectionBridgeService`
322
+
323
+ Controls category, subcategory, amenity, offers, and clear selection.
324
+
325
+ Methods:
326
+
327
+ ```ts
328
+ SelectionBridgeService.selectCategory(category);
329
+ SelectionBridgeService.selectSubCategory(subCategory);
330
+ SelectionBridgeService.selectAmenity(amenity);
331
+ SelectionBridgeService.toggleOffers();
332
+ SelectionBridgeService.clearSelection();
333
+ SelectionBridgeService.syncHighlightsWithState();
334
+ ```
335
+
336
+ Category example:
337
+
338
+ ```tsx
339
+ import { SelectionBridgeService, useCategoryStore } from "wovvmap-webview-bridge/web";
340
+
341
+ function CategoryButton({ code }: { code: string }) {
342
+ const category = useCategoryStore((s) => s.categories[code]);
343
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
344
+
345
+ if (!category) return null;
346
+
347
+ const isActive =
348
+ activeSelection.type === "Category" &&
349
+ activeSelection.id === category.code.toString();
67
350
 
68
351
  return (
69
- <iframe
70
- ref={iframeRef}
71
- src="https://your-map-app-url.com"
72
- style={{ width: "100%", height: "100%", border: 0 }}
73
- title="Wovv Map"
74
- />
352
+ <button
353
+ className={isActive ? "active" : ""}
354
+ onClick={() => SelectionBridgeService.selectCategory(category)}
355
+ >
356
+ {category.name}
357
+ </button>
75
358
  );
76
359
  }
77
360
  ```
78
361
 
79
- ### 1) Render the WebView
362
+ Subcategory example:
80
363
 
81
364
  ```tsx
82
- import React from "react";
83
- import { WebViewScreen } from "wovvmap-webview-bridge";
365
+ SelectionBridgeService.selectSubCategory(subCategory);
366
+ ```
367
+
368
+ Amenity example:
369
+
370
+ ```tsx
371
+ SelectionBridgeService.selectAmenity(amenity);
372
+ ```
373
+
374
+ Amenity selection also sets the destination with the amenity node keys:
375
+
376
+ ```ts
377
+ DirectionBridgeService.setDestination(amenity.nodePointsKey.join("<->"), true, false);
378
+ ```
379
+
380
+ Offers button example:
381
+
382
+ ```tsx
383
+ import { SelectionBridgeService, useNodePointStore, useSelectionStore } from "wovvmap-webview-bridge/web";
384
+
385
+ export function OffersButton() {
386
+ const offersNodeMap = useNodePointStore((s) => s.offersNodeMap);
387
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
388
+ const hasOffers = (offersNodeMap.all_offers?.nodePointsKey?.length ?? 0) > 0;
84
389
 
85
- export default function App() {
86
- return <WebViewScreen url="https://your-map-app-url.com" />;
390
+ if (!hasOffers) return null;
391
+
392
+ return (
393
+ <button
394
+ className={isOfferSelected ? "active" : ""}
395
+ onClick={() => SelectionBridgeService.toggleOffers()}
396
+ >
397
+ Offers
398
+ </button>
399
+ );
87
400
  }
88
401
  ```
89
402
 
90
- ### 2) Send events to the WebView (RN -> Web)
403
+ Important selection states:
404
+
405
+ - Category active: `useSelectionStore((s) => s.activeSelection.type === "Category")`
406
+ - Selected category id: `useSelectionStore((s) => s.activeSelection.id)`
407
+ - Active category object: `useSelectionStore((s) => s.activeCategory)`
408
+ - Subcategory active: `activeSelection.type === "SubCategory"`
409
+ - Amenity active: `activeSelection.type === "Amenity"`
410
+ - Offers active: `useSelectionStore((s) => s.isOfferSelected)`
411
+
412
+ ### `FloorBridgeService`
413
+
414
+ Controls active floor.
415
+
416
+ ```ts
417
+ FloorBridgeService.changeFloor(floorIndex, animateCamera);
418
+ ```
419
+
420
+ Floor selector example:
91
421
 
92
422
  ```tsx
93
- import {
94
- sendStartPointToBridge,
95
- sendEndPointToBridge,
96
- sendActiveFloorToBridge,
97
- sendPathNextBtnClick,
98
- sendPathPreBtnClick,
99
- sendPathFinishBtnClick,
100
- sendSelectCategory,
101
- sendZoomIn,
102
- sendZoomOut,
103
- sendClearStartAndEndPoint,
104
- sendPathFilter,
105
- sendGetDirectionToBridge,
106
- sendNavigateToBridge,
107
- sendMapThemeToBridge,
108
- sendEditableViewToBridge,
109
- sendSensorDataToBridge,
110
- } from "wovvmap-webview-bridge";
423
+ import { FloorBridgeService, useFloorStore } from "wovvmap-webview-bridge/web";
424
+
425
+ export function FloorSelector() {
426
+ const floors = useFloorStore((s) => s.floors);
427
+ const activeFloor = useFloorStore((s) => s.activeFloor);
111
428
 
112
- sendStartPointToBridge("A1");
113
- sendEndPointToBridge("B5");
114
- sendActiveFloorToBridge(2);
115
- sendPathNextBtnClick();
116
- sendPathPreBtnClick();
117
- sendPathFinishBtnClick();
118
- sendSelectCategory(["Food", "Fashion"]);
119
- sendZoomIn();
120
- sendZoomOut();
121
- sendClearStartAndEndPoint();
122
- sendPathFilter();
123
- sendGetDirectionToBridge();
124
- sendNavigateToBridge("map-id-123");
125
- sendMapThemeToBridge({ "theme-path-color": "#00AAFF" });
126
- sendEditableViewToBridge(mapDataExportObj);
127
- sendSensorDataToBridge({ timestamp: Date.now(), accelerometerSignal: {x: 0, y: 0, z: 0} });
429
+ return (
430
+ <select
431
+ value={activeFloor ?? ""}
432
+ onChange={(event) => FloorBridgeService.changeFloor(Number(event.target.value), true)}
433
+ >
434
+ {floors.map((floor) => (
435
+ <option key={floor.index ?? floor.FloorNumber} value={floor.index}>
436
+ {floor.FloorName}
437
+ </option>
438
+ ))}
439
+ </select>
440
+ );
441
+ }
128
442
  ```
129
443
 
130
- ### 3) Handle events from the WebView (Web -> RN)
444
+ ### `StepNavigationBridgeService`
445
+
446
+ Controls current step in step-by-step navigation.
447
+
448
+ ```ts
449
+ StepNavigationBridgeService.goToStep(index);
450
+ StepNavigationBridgeService.nextStep();
451
+ StepNavigationBridgeService.prevStep();
452
+ StepNavigationBridgeService.finishStep();
453
+ ```
131
454
 
132
- You can register handlers for click events. All other events are stored in the Zustand store.
455
+ Step list example:
133
456
 
134
457
  ```tsx
135
- import { registerBridgeHandler } from "wovvmap-webview-bridge";
458
+ import { StepNavigationBridgeService, useNavigationStore } from "wovvmap-webview-bridge/web";
136
459
 
137
- registerBridgeHandler("isSceneClick", (payload) => {
138
- console.log("Scene clicked", payload);
139
- });
460
+ export function StepList() {
461
+ const result = useNavigationStore((s) => s.navigationResult);
462
+ const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
140
463
 
141
- registerBridgeHandler("isShapeClick", (payload) => {
142
- console.log("Shape clicked", payload.value);
143
- });
464
+ if (!result) return null;
465
+
466
+ return (
467
+ <div>
468
+ {result.steps.map((step, index) => (
469
+ <button
470
+ key={step.id}
471
+ className={currentStepIndex === index ? "active" : ""}
472
+ onClick={() => StepNavigationBridgeService.goToStep(index)}
473
+ >
474
+ {step.instruction}
475
+ </button>
476
+ ))}
477
+ </div>
478
+ );
479
+ }
144
480
  ```
145
481
 
146
- ### 4) Read synced state from the store
147
-
148
- ```tsx
149
- import { useBridgeStorage } from "wovvmap-webview-bridge";
150
-
151
- const state = useBridgeStorage((s) => ({
152
- isBridgeLoaded: s.isBridgeLoaded,
153
- isMapLoaded: s.isMapLoaded,
154
- searchablePoints: s.searchablePoints,
155
- amenities: s.amenities,
156
- allOffers: s.allOffers,
157
- activeFloor: s.activeFloor,
158
- stepByStepList: s.stepByStepList,
159
- pathSummary: s.pathSummary,
160
- categories: s.categories,
161
- subCategories: s.subCategories,
162
- }));
163
- ```
164
-
165
- Store fields:
166
- - isBridgeLoaded
167
- - isMapLoaded
168
- - searchablePoints
169
- - amenities
170
- - allOffers
171
- - activeFloor
172
- - elevator, escalator
173
- - floorImages
174
- - stepByStepList
175
- - pathSummary
176
- - nextPreState
177
- - pointsByKey
178
- - categories
179
- - subCategories
180
- - cameraControllerState
181
-
182
- ## Bridge message contracts
183
-
184
- ### IncomingMessage (Web -> RN)
185
- Keys and payloads:
186
- - pong: boolean
187
- - isConnection: boolean
188
- - mapLoaded: boolean
189
- - _searchablePoints: NodePoint[]
190
- - _allAmenities: AmenityWithNodePoint[]
191
- - _allOffers: string[]
192
- - _activeFloor: number
193
- - FloorImg: FloorImage[]
194
- - categories: Record<ExternalId, Category>
195
- - subCategories: Record<ExternalId, SubCategory>
196
- - isSceneClick: no value
197
- - isShapeClick: NodePoint
198
- - stepByStepList: StepByStepResult
199
- - pathNextPreState: NavState
200
- - cameraControllerState: { cameraPosition: CameraPosition; controlsPosition: ControlsPosition }
201
-
202
- ### OutgoingMessage (RN -> Web)
203
- Keys and payloads:
204
- - applyCSS: { selector: string; style: Partial<CSSStyleDeclaration> }
205
- - ping: boolean
206
- - setEndPoint: string
207
- - setStartPoint: string
208
- - setActiveFloor: number
209
- - pathNextBtnClick: no value
210
- - pathPreBtnClick: no value
211
- - pathFinishBtnClick: no value
212
- - setSelectCategory: string | string[] | null
213
- - zoomIn: no value
214
- - zoomOut: no value
215
- - clearStartAndEndPoint: no value
216
- - setPathFilter: filterPath
217
- - getDirection: no value
218
- - setMapTheme: Theme | null
219
- - navigateTo: string
220
- - editableView: MapApiResponse
221
- - pathHighlightByStepIndex: number
222
- - sensorData: SensorDataPayload
223
-
224
- ## Exported API
482
+ Next/previous example:
225
483
 
226
- ### Components
227
- - WebViewScreen
228
- - WebIframeScreen (web only)
229
-
230
- ### Bridge helpers
231
- - sendStartPointToBridge
232
- - sendEndPointToBridge
233
- - sendActiveFloorToBridge
234
- - sendPathNextBtnClick
235
- - sendPathPreBtnClick
236
- - sendPathFinishBtnClick
237
- - sendSelectCategory
238
- - sendZoomIn
239
- - sendZoomOut
240
- - sendClearStartAndEndPoint
241
- - sendGetDirectionToBridge
242
- - sendPathFilter
243
- - sendNavigateToBridge
244
- - sendMapThemeToBridge
245
- - sendEditableViewToBridge
246
- - sendSensorDataToBridge
484
+ ```tsx
485
+ <button onClick={() => StepNavigationBridgeService.prevStep()}>Previous</button>
486
+ <button onClick={() => StepNavigationBridgeService.nextStep()}>Next</button>
487
+ <button onClick={() => StepNavigationBridgeService.finishStep()}>Finish</button>
488
+ ```
247
489
 
248
- ### Handlers
249
- - registerBridgeHandler
490
+ ### `ZoomBridgeService`
250
491
 
251
- ### Store
252
- - useBridgeStorage (Zustand)
492
+ Controls viewer zoom.
253
493
 
254
- ### Types
255
- - IncomingMessage, OutgoingMessage
256
- - NodePoint
257
- - AmenityWithNodePoint
258
- - Category, SubCategory, ExternalId
259
- - Environment, DayHours, WeeklyHours
260
- - NodePointOffer, BrandInfo
261
- - MapExportContext, GeometryFloor, GeometryNodePoint, GeometryLayer
262
- - NodeMetadata, AssetPayloads
263
- - FloorImage
264
- - StepInstruction, PathSummary, StepByStepResult
265
- - NavState
266
- - filterPath
267
- - Theme
268
- - CameraPosition, ControlsPosition
269
- - MapApiResponse, MapDataExport
270
-
271
- ## File structure
272
-
273
- ```
274
- src/
275
- index.ts
276
- webviewBridge/
277
- WebViewScreen.tsx
278
- BridgeService.ts
279
- BridgeStorage.ts
280
- WebViewBridgeRef.ts
281
- handlers/
282
- WebBridgeHandlers.ts
283
- types/
284
- types.ts
285
- ```
286
-
287
- ## License
288
-
289
- MIT
494
+ ```ts
495
+ ZoomBridgeService.zoomIn();
496
+ ZoomBridgeService.zoomOut();
497
+ ```
498
+
499
+ Example:
500
+
501
+ ```tsx
502
+ <button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
503
+ <button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
504
+ ```
505
+
506
+ ### `ViewerBridgeService`
507
+
508
+ Controls viewer mode, camera view, and text rendering.
509
+
510
+ ```ts
511
+ ViewerBridgeService.setViewMode("2D", true);
512
+ ViewerBridgeService.setViewMode("3D", true);
513
+ ViewerBridgeService.setCameraView("street");
514
+ ViewerBridgeService.setCameraView("sky");
515
+ ViewerBridgeService.setTextType("2D");
516
+ ViewerBridgeService.setTextType("3D");
517
+ ViewerBridgeService.setShow2DIcon(true);
518
+ ViewerBridgeService.setShow2DIcon(false);
519
+ ```
520
+
521
+ Toolbar example:
522
+
523
+ ```tsx
524
+ import { ViewerBridgeService, useViewerStore } from "wovvmap-webview-bridge/web";
525
+
526
+ export function ViewerToolbar() {
527
+ const viewMode = useViewerStore((s) => s.viewMode);
528
+ const cameraView = useViewerStore((s) => s.cameraView);
529
+ const textType = useViewerStore((s) => s.textType);
530
+ const show2DIcon = useViewerStore((s) => s.show2DIcon);
531
+
532
+ return (
533
+ <div>
534
+ <button
535
+ className={viewMode === "2D" ? "active" : ""}
536
+ onClick={() => ViewerBridgeService.setViewMode("2D", true)}
537
+ >
538
+ 2D View
539
+ </button>
540
+ <button
541
+ className={viewMode === "3D" ? "active" : ""}
542
+ onClick={() => ViewerBridgeService.setViewMode("3D", true)}
543
+ >
544
+ 3D View
545
+ </button>
546
+ <button
547
+ className={cameraView === "street" ? "active" : ""}
548
+ onClick={() => ViewerBridgeService.setCameraView("street")}
549
+ >
550
+ Street
551
+ </button>
552
+ <button
553
+ className={cameraView === "sky" ? "active" : ""}
554
+ onClick={() => ViewerBridgeService.setCameraView("sky")}
555
+ >
556
+ Sky
557
+ </button>
558
+ <button
559
+ className={textType === "2D" ? "active" : ""}
560
+ onClick={() => ViewerBridgeService.setTextType("2D")}
561
+ >
562
+ 2D Text
563
+ </button>
564
+ <button
565
+ className={textType === "3D" ? "active" : ""}
566
+ onClick={() => ViewerBridgeService.setTextType("3D")}
567
+ >
568
+ 3D Text
569
+ </button>
570
+ <button
571
+ className={show2DIcon ? "active" : ""}
572
+ onClick={() => ViewerBridgeService.setShow2DIcon(!show2DIcon)}
573
+ >
574
+ 2D Icon
575
+ </button>
576
+ </div>
577
+ );
578
+ }
579
+ ```
580
+
581
+ ### `BridgeService`
582
+
583
+ Low-level event sender. Use command services above for normal UI code.
584
+
585
+ Available low-level methods:
586
+
587
+ ```ts
588
+ BridgeService.setViewerConfig(config);
589
+ BridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
590
+ BridgeService.clearDestination();
591
+ BridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
592
+ BridgeService.clearOrigin();
593
+ BridgeService.swapOriginDestination();
594
+ BridgeService.setActiveSelection(type, code);
595
+ BridgeService.clearActiveSelection();
596
+ BridgeService.setOfferSelected(isSelected);
597
+ BridgeService.generateDirectionsRoute(callbacks);
598
+ BridgeService.clearActivePath();
599
+ BridgeService.changeFloor(floorIndex, animateCamera);
600
+ BridgeService.goToStep(index);
601
+ BridgeService.nextStep();
602
+ BridgeService.prevStep();
603
+ BridgeService.finishStep();
604
+ BridgeService.zoomIn();
605
+ BridgeService.zoomOut();
606
+ BridgeService.setViewMode(viewMode, animate);
607
+ BridgeService.setCameraView(cameraView);
608
+ BridgeService.setTextType(textType);
609
+ BridgeService.setShow2DIcon(show2DIcon);
610
+ ```
611
+
612
+ ## Synced Stores
613
+
614
+ All stores are Zustand stores. Use them directly in React components.
615
+
616
+ ### `useBridgeStorage`
617
+
618
+ Bridge and tenant state.
619
+
620
+ Fields:
621
+
622
+ - `isMapLoaded`: viewer sent map loaded state
623
+ - `isBridgeLoaded`: bridge connection event received
624
+ - `imageBaseUrl`: base URL for category, subcategory, amenity, and store images
625
+
626
+ Example:
627
+
628
+ ```tsx
629
+ const isReady = useBridgeStorage((s) => s.isBridgeLoaded && s.isMapLoaded);
630
+ const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
631
+ ```
632
+
633
+ ### `useViewerStore`
634
+
635
+ Viewer configuration and loading state.
636
+
637
+ Fields:
638
+
639
+ - `apiVersion`: viewer API version, for example `"v2"`
640
+ - `viewMode`: `"2D"` or `"3D"`
641
+ - `cameraView`: `"street"` or `"sky"`
642
+ - `textType`: `"2D"` or `"3D"`
643
+ - `show2DIcon`: boolean
644
+ - `isMapParsed`: true after viewer parsed map data
645
+ - `isLoading`: viewer loading flag
646
+
647
+ Example:
648
+
649
+ ```tsx
650
+ const viewMode = useViewerStore((s) => s.viewMode);
651
+ const cameraView = useViewerStore((s) => s.cameraView);
652
+ ```
653
+
654
+ ### `useCategoryStore`
655
+
656
+ Category catalog.
657
+
658
+ Fields:
659
+
660
+ - `categories`: `Record<string, Category>`
661
+
662
+ Example:
663
+
664
+ ```tsx
665
+ const categories = useCategoryStore((s) => Object.values(s.categories));
666
+ ```
667
+
668
+ Category active state:
669
+
670
+ ```tsx
671
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
672
+ const isActiveCategory =
673
+ activeSelection.type === "Category" &&
674
+ activeSelection.id === category.code.toString();
675
+ ```
676
+
677
+ ### `useSubCategoryStore`
678
+
679
+ Subcategory catalog.
680
+
681
+ Fields:
682
+
683
+ - `subCategories`: `Record<string, SubCategory>`
684
+
685
+ Example:
686
+
687
+ ```tsx
688
+ const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
689
+ ```
690
+
691
+ Subcategory active state:
692
+
693
+ ```tsx
694
+ const isActiveSubCategory =
695
+ activeSelection.type === "SubCategory" &&
696
+ activeSelection.id === subCategory.code.toString();
697
+ ```
698
+
699
+ ### `useAmenityStore`
700
+
701
+ Amenity catalog.
702
+
703
+ Fields:
704
+
705
+ - `amenities`: `Record<string, Amenity>`
706
+
707
+ Example:
708
+
709
+ ```tsx
710
+ const amenities = useAmenityStore((s) => Object.values(s.amenities));
711
+ ```
712
+
713
+ Amenity active state:
714
+
715
+ ```tsx
716
+ const isActiveAmenity =
717
+ activeSelection.type === "Amenity" &&
718
+ activeSelection.id === amenity.id.toString();
719
+ ```
720
+
721
+ ### `useNodePointStore`
722
+
723
+ Searchable stores/locations and node maps.
724
+
725
+ Fields:
726
+
727
+ - `searchableNodePointMap`: `NodePoint[]`
728
+ - `locationGroupedNodeMap`: `Record<string, string[]>`
729
+ - `keyNodeMap`: `Record<string, NodePoint>`
730
+ - `offersNodeMap`: `OffersNodeMap`
731
+
732
+ Actions/helpers:
733
+
734
+ - `getNodePointByKey(key)`
735
+ - `clearSearchableNodePoints()`
736
+
737
+ Example: search source data:
738
+
739
+ ```tsx
740
+ const nodePoints = useNodePointStore((s) => s.searchableNodePointMap);
741
+ ```
742
+
743
+ Example: get store name:
744
+
745
+ ```ts
746
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
747
+ const name = node?.assets?.locationName?.text || node?.key || "";
748
+ ```
749
+
750
+ Example: group multi-location store:
751
+
752
+ ```ts
753
+ const groupKeys = useNodePointStore.getState().locationGroupedNodeMap["Aldo"] || [];
754
+ DirectionBridgeService.setDestination(groupKeys.join("<->"), true, false);
755
+ ```
756
+
757
+ Example: offers data:
758
+
759
+ ```tsx
760
+ const allOffers = useNodePointStore((s) => s.offersNodeMap.all_offers);
761
+ const hasOffers = (allOffers?.nodePointsKey?.length ?? 0) > 0;
762
+ ```
763
+
764
+ ### `useSelectionStore`
765
+
766
+ Active category/subcategory/amenity and offers state.
767
+
768
+ Fields:
769
+
770
+ - `activeSelection`: `{ type: "Category" | "SubCategory" | "Amenity" | null; id: string | null }`
771
+ - `activeCategory`: `Category | null`
772
+ - `isOfferSelected`: boolean
773
+
774
+ Examples:
775
+
776
+ ```tsx
777
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
778
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
779
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
780
+ ```
781
+
782
+ Offer button active:
783
+
784
+ ```tsx
785
+ const isActive = useSelectionStore((s) => s.isOfferSelected);
786
+ ```
787
+
788
+ Category tab active:
789
+
790
+ ```tsx
791
+ const isActive =
792
+ activeSelection.type === "Category" &&
793
+ activeSelection.id === category.code.toString();
794
+ ```
795
+
796
+ Subcategory tab active:
797
+
798
+ ```tsx
799
+ const isActive =
800
+ activeSelection.type === "SubCategory" &&
801
+ activeSelection.id === subCategory.code.toString();
802
+ ```
803
+
804
+ Amenity active:
805
+
806
+ ```tsx
807
+ const isActive =
808
+ activeSelection.type === "Amenity" &&
809
+ activeSelection.id === amenity.id.toString();
810
+ ```
811
+
812
+ ### `useDirectionStore`
813
+
814
+ Origin/destination and pathfinding state mirrored from the viewer.
815
+
816
+ Fields:
817
+
818
+ - `selectedOriginPointId`: selected start node id or joined node ids
819
+ - `selectedDestinationPointId`: selected end node id or joined node ids
820
+ - `pathfindingError`: route error string or `null`
821
+ - `routingPreferences`: `{ elevator?: boolean; escalator?: boolean; stair?: boolean }`
822
+
823
+ Examples:
824
+
825
+ ```tsx
826
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
827
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
828
+ const pathfindingError = useDirectionStore((s) => s.pathfindingError);
829
+ const routingPreferences = useDirectionStore((s) => s.routingPreferences);
830
+ ```
831
+
832
+ Show route error:
833
+
834
+ ```tsx
835
+ {pathfindingError && <div>No Routes Available: {pathfindingError}</div>}
836
+ ```
837
+
838
+ Enable get directions button:
839
+
840
+ ```tsx
841
+ const canGenerateRoute = Boolean(origin && destination);
842
+ ```
843
+
844
+ Update routing preference from host:
845
+
846
+ ```tsx
847
+ useDirectionStore.getState().setRoutingPreferences({
848
+ elevator: true,
849
+ escalator: false,
850
+ });
851
+ ```
852
+
853
+ ### `useDirectionsRouteRequestStore`
854
+
855
+ Request state for `DirectionBridgeService.generateDirectionsRoute`.
856
+
857
+ Fields:
858
+
859
+ - `status`: `"idle" | "loading" | "success" | "error"`
860
+ - `result`: `DirectionsRouteResult | null`
861
+ - `error`: string or `null`
862
+ - `isLoading`: boolean
863
+
864
+ Example:
865
+
866
+ ```tsx
867
+ const routeRequest = useDirectionsRouteRequestStore();
868
+
869
+ if (routeRequest.isLoading) {
870
+ return <div>Generating route...</div>;
871
+ }
872
+
873
+ if (routeRequest.status === "error") {
874
+ return <div>{routeRequest.error}</div>;
875
+ }
876
+ ```
877
+
878
+ Navigate to step-by-step only when route succeeds:
879
+
880
+ ```tsx
881
+ DirectionBridgeService.generateDirectionsRoute({
882
+ onSuccess: () => {
883
+ navigateToStepByStep();
884
+ },
885
+ onError: (result) => {
886
+ console.log(result.error);
887
+ },
888
+ });
889
+ ```
890
+
891
+ ### `useFloorStore`
892
+
893
+ Floor list and current floor.
894
+
895
+ Fields:
896
+
897
+ - `floors`: `FloorImage[]`
898
+ - `activeFloor`: number or `null`
899
+
900
+ Example:
901
+
902
+ ```tsx
903
+ const floors = useFloorStore((s) => s.floors);
904
+ const activeFloor = useFloorStore((s) => s.activeFloor);
905
+ ```
906
+
907
+ ### `useNavigationStore`
908
+
909
+ Step-by-step navigation state.
910
+
911
+ Fields:
912
+
913
+ - `activePath`: `NodePoint[] | null`
914
+ - `navigationResult`: `NavigationResult | null`
915
+ - `simulationMode`: `"none" | "full" | "current"`
916
+ - `currentStepIndex`: number
917
+
918
+ Example:
919
+
920
+ ```tsx
921
+ const navigationResult = useNavigationStore((s) => s.navigationResult);
922
+ const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
923
+ ```
924
+
925
+ Step data:
926
+
927
+ ```tsx
928
+ navigationResult?.steps.map((step) => (
929
+ <div key={step.id}>
930
+ {step.instruction} - {step.distanceText}
931
+ </div>
932
+ ));
933
+ ```
934
+
935
+ ## Viewer Click Events
936
+
937
+ Use `registerBridgeHandler` for viewer interaction events that should trigger host UI behavior.
938
+
939
+ Currently supported handler:
940
+
941
+ - `isShapeClick`: fires when a map shape/store is clicked in the viewer. Value is the clicked node id.
942
+
943
+ Example:
944
+
945
+ ```tsx
946
+ import { registerBridgeHandler } from "wovvmap-webview-bridge/web";
947
+
948
+ useEffect(() => {
949
+ registerBridgeHandler("isShapeClick", (message) => {
950
+ console.log("Clicked node id", message.value);
951
+ openStoreDetail(message.value);
952
+ });
953
+ }, []);
954
+ ```
955
+
956
+ ## Full Kiosk Host Example
957
+
958
+ This example shows the main pieces together.
959
+
960
+ ```tsx
961
+ import { useEffect, useRef } from "react";
962
+ import {
963
+ WebIframeScreen,
964
+ DirectionBridgeService,
965
+ SelectionBridgeService,
966
+ ViewerBridgeService,
967
+ ZoomBridgeService,
968
+ useCategoryStore,
969
+ useDirectionStore,
970
+ useNodePointStore,
971
+ registerBridgeHandler,
972
+ } from "wovvmap-webview-bridge/web";
973
+ import { useDirectionInputStore } from "./useDirectionInputStore";
974
+
975
+ export function KioskMap({ mapId }: { mapId: string | null }) {
976
+ const categories = useCategoryStore((s) => Object.values(s.categories));
977
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
978
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
979
+ const configuredRef = useRef(false);
980
+
981
+ useEffect(() => {
982
+ if (configuredRef.current) return;
983
+ configuredRef.current = true;
984
+
985
+ DirectionBridgeService.configure({
986
+ directionInputAdapter: {
987
+ setDestinationState: (query, selectedId) =>
988
+ useDirectionInputStore.getState().setDestinationState(query, selectedId),
989
+ setOriginState: (query, selectedId) =>
990
+ useDirectionInputStore.getState().setOriginState(query, selectedId),
991
+ clearDestinationState: (clearQuery) =>
992
+ useDirectionInputStore.getState().clearDestinationState(clearQuery),
993
+ clearOriginState: () =>
994
+ useDirectionInputStore.getState().clearOriginState(),
995
+ setActiveDirectionField: (field) =>
996
+ useDirectionInputStore.getState().setActiveDirectionField(field),
997
+ getOriginQuery: () => useDirectionInputStore.getState().originQuery,
998
+ getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
999
+ },
1000
+ resolveLocationName: (nodeId) => {
1001
+ const nodeKey = nodeId.split("<->")[0];
1002
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
1003
+ return node?.assets?.locationName?.text || nodeKey;
1004
+ },
1005
+ onClearSelection: () => SelectionBridgeService.clearSelection(),
1006
+ });
1007
+ }, []);
1008
+
1009
+ useEffect(() => {
1010
+ registerBridgeHandler("isShapeClick", (message) => {
1011
+ DirectionBridgeService.setDestination(message.value, true, false);
1012
+ });
1013
+ }, []);
1014
+
1015
+ if (!mapId) return null;
1016
+
1017
+ return (
1018
+ <div>
1019
+ <WebIframeScreen
1020
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
1021
+ origin="https://viewer.example.com"
1022
+ />
1023
+
1024
+ <div>
1025
+ {categories.map((category) => (
1026
+ <button
1027
+ key={category.code}
1028
+ onClick={() => SelectionBridgeService.selectCategory(category)}
1029
+ >
1030
+ {category.name}
1031
+ </button>
1032
+ ))}
1033
+ </div>
1034
+
1035
+ <button onClick={() => ViewerBridgeService.setViewMode("2D", true)}>
1036
+ 2D View
1037
+ </button>
1038
+ <button onClick={() => ViewerBridgeService.setViewMode("3D", true)}>
1039
+ 3D View
1040
+ </button>
1041
+ <button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
1042
+ <button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
1043
+
1044
+ <button
1045
+ disabled={!origin || !destination}
1046
+ onClick={() =>
1047
+ DirectionBridgeService.generateDirectionsRoute({
1048
+ onSuccess: () => console.log("route ready"),
1049
+ })
1050
+ }
1051
+ >
1052
+ Get Directions
1053
+ </button>
1054
+ </div>
1055
+ );
1056
+ }
1057
+ ```
1058
+
1059
+ ## Data Flow
1060
+
1061
+ 1. Host renders `WebIframeScreen` or `WebViewScreen`.
1062
+ 2. Viewer initializes and sends grouped state events.
1063
+ 3. Package handlers apply those events into Zustand stores.
1064
+ 4. Host UI reads stores and renders categories, amenities, floors, offers, directions, and navigation.
1065
+ 5. Host UI calls `*BridgeService` methods.
1066
+ 6. Package sends typed bridge event to viewer.
1067
+ 7. Viewer updates its internal state/scene and sends fresh grouped state back.
1068
+
1069
+ ## Incoming Viewer Events
1070
+
1071
+ These are sent by the viewer to the host package and applied into stores automatically.
1072
+
1073
+ | Key | Store updated | Value |
1074
+ | --- | --- | --- |
1075
+ | `bridgeState` | `useBridgeStorage.isMapLoaded` | `{ isMapLoaded: boolean }` |
1076
+ | `isConnection` | `useBridgeStorage.isBridgeLoaded` | `boolean` |
1077
+ | `tenantState` | `useBridgeStorage.imageBaseUrl` | `{ imageBaseUrl }` |
1078
+ | `viewerState` | `useViewerStore` | `ViewerBridgeState` |
1079
+ | `catalogState` | `useCategoryStore`, `useSubCategoryStore`, `useAmenityStore` | categories, subCategories, amenities |
1080
+ | `floorState` | `useFloorStore` | activeFloor, floors |
1081
+ | `nodePointState` | `useNodePointStore` | searchableNodePointMap, locationGroupedNodeMap, keyNodeMap, offersNodeMap |
1082
+ | `selectionState` | `useSelectionStore` | activeSelection, activeCategory, isOfferSelected |
1083
+ | `directionState` | `useDirectionStore` | selectedOriginPointId, selectedDestinationPointId, pathfindingError, routingPreferences |
1084
+ | `navigationState` | `useNavigationStore` | activePath, navigationResult, simulationMode, currentStepIndex |
1085
+ | `directionsRouteResult` | `useDirectionsRouteRequestStore` | status, navigationResult, error |
1086
+ | `isShapeClick` | event handler | clicked node id |
1087
+
1088
+ ## Outgoing Host Commands
1089
+
1090
+ These are sent by command services to the viewer.
1091
+
1092
+ | Service | Message key |
1093
+ | --- | --- |
1094
+ | `DirectionBridgeService.setDestination` | `setDestination` |
1095
+ | `DirectionBridgeService.clearDestination` | `clearDestination` |
1096
+ | `DirectionBridgeService.setOrigin` | `setOrigin` |
1097
+ | `DirectionBridgeService.clearOrigin` | `clearOrigin` |
1098
+ | `DirectionBridgeService.swapOriginDestination` | `swapOriginDestination` |
1099
+ | `DirectionBridgeService.generateDirectionsRoute` | `generateDirectionsRoute` |
1100
+ | `DirectionBridgeService.clearActivePath` | `clearActivePath` |
1101
+ | `SelectionBridgeService.selectCategory` | `setActiveSelection` |
1102
+ | `SelectionBridgeService.selectSubCategory` | `setActiveSelection` |
1103
+ | `SelectionBridgeService.selectAmenity` | `setActiveSelection` plus destination |
1104
+ | `SelectionBridgeService.toggleOffers` | `setOfferSelected` |
1105
+ | `SelectionBridgeService.clearSelection` | `clearActiveSelection` and `setOfferSelected(false)` |
1106
+ | `FloorBridgeService.changeFloor` | `changeFloor` |
1107
+ | `StepNavigationBridgeService.goToStep` | `goToStep` |
1108
+ | `StepNavigationBridgeService.nextStep` | `nextStep` |
1109
+ | `StepNavigationBridgeService.prevStep` | `prevStep` |
1110
+ | `StepNavigationBridgeService.finishStep` | `finishStep` |
1111
+ | `ZoomBridgeService.zoomIn` | `zoomIn` |
1112
+ | `ZoomBridgeService.zoomOut` | `zoomOut` |
1113
+ | `ViewerBridgeService.setViewMode` | `setViewMode` |
1114
+ | `ViewerBridgeService.setCameraView` | `setCameraView` |
1115
+ | `ViewerBridgeService.setTextType` | `setTextType` |
1116
+ | `ViewerBridgeService.setShow2DIcon` | `setShow2DIcon` |
1117
+
1118
+ ## Type Exports
1119
+
1120
+ The package exports these core types:
1121
+
1122
+ ```ts
1123
+ type IncomingMessage;
1124
+ type OutgoingMessage;
1125
+ type NodePoint;
1126
+ type FloorImage;
1127
+ type Amenity;
1128
+ type Category;
1129
+ type SubCategory;
1130
+ type ViewerBridgeState;
1131
+ type ViewMode;
1132
+ type CameraView;
1133
+ type TextType;
1134
+ type NavigationBridgeState;
1135
+ type DirectionsRouteResult;
1136
+ type NavigationResult;
1137
+ type NavigationStep;
1138
+ type NavigationSummary;
1139
+ type SimulationMode;
1140
+ type DirectionBridgeServiceConfig;
1141
+ type DirectionInputAdapter;
1142
+ type ViewerConfig;
1143
+ ```
1144
+
1145
+ ## Important Type Shapes
1146
+
1147
+ ### `Category`
1148
+
1149
+ ```ts
1150
+ type Category = {
1151
+ code: string | number;
1152
+ name: string;
1153
+ image?: string;
1154
+ subCategoryCodes: string[] | number[];
1155
+ nodePointsKey?: string[];
1156
+ nodePoints?: NodePoint[];
1157
+ };
1158
+ ```
1159
+
1160
+ ### `SubCategory`
1161
+
1162
+ ```ts
1163
+ type SubCategory = {
1164
+ code: string | number;
1165
+ name: string;
1166
+ image?: string;
1167
+ description?: string;
1168
+ categoryCode: string | number;
1169
+ nodePointsKey?: string[];
1170
+ nodePoints?: NodePoint[];
1171
+ };
1172
+ ```
1173
+
1174
+ ### `Amenity`
1175
+
1176
+ ```ts
1177
+ type Amenity = {
1178
+ id: string | number;
1179
+ status: number;
1180
+ fieldCode: string | number;
1181
+ name: string;
1182
+ image: string;
1183
+ wayfinding_id?: string | number | null;
1184
+ nodePointsKey?: string[];
1185
+ nodePoints?: NodePoint[];
1186
+ };
1187
+ ```
1188
+
1189
+ ### `NodePoint`
1190
+
1191
+ Important fields:
1192
+
1193
+ ```ts
1194
+ type NodePoint = {
1195
+ key: string;
1196
+ floorIndex: number;
1197
+ floorName: string;
1198
+ assets: {
1199
+ locationName: { text: string } | null;
1200
+ pointImages: unknown | null;
1201
+ logo: string | null;
1202
+ brandLogo: string | null;
1203
+ brandImages: string[];
1204
+ };
1205
+ categoryCode?: string | number;
1206
+ subCategoryCode?: string | number;
1207
+ offers: NodePointOffer[];
1208
+ up: boolean;
1209
+ down: boolean;
1210
+ };
1211
+ ```
1212
+
1213
+ ### `DirectionsRouteResult`
1214
+
1215
+ ```ts
1216
+ type DirectionsRouteResult = {
1217
+ status: "success" | "error";
1218
+ navigationResult: NavigationResult | null;
1219
+ error: string | null;
1220
+ };
1221
+ ```
1222
+
1223
+ ### `NavigationStep`
1224
+
1225
+ ```ts
1226
+ type NavigationStep = {
1227
+ id: string;
1228
+ instruction: string;
1229
+ action:
1230
+ | "straight"
1231
+ | "turn_left"
1232
+ | "turn_right"
1233
+ | "take_elevator"
1234
+ | "take_escalator"
1235
+ | "take_elevator_up"
1236
+ | "take_elevator_down"
1237
+ | "take_escalator_up"
1238
+ | "take_escalator_down"
1239
+ | "exit_elevator"
1240
+ | "exit_escalator"
1241
+ | "start"
1242
+ | "arrive";
1243
+ distanceText: string;
1244
+ timeText: string;
1245
+ floorIndex: number;
1246
+ };
1247
+ ```
1248
+
1249
+ ## Common UI Recipes
1250
+
1251
+ ### Category Footer
1252
+
1253
+ ```tsx
1254
+ const categories = useCategoryStore((s) => Object.values(s.categories));
1255
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1256
+
1257
+ return categories.map((category) => {
1258
+ const active =
1259
+ activeSelection.type === "Category" &&
1260
+ activeSelection.id === category.code.toString();
1261
+
1262
+ return (
1263
+ <button
1264
+ key={category.code}
1265
+ className={active ? "active" : ""}
1266
+ onClick={() => SelectionBridgeService.selectCategory(category)}
1267
+ >
1268
+ {category.name}
1269
+ </button>
1270
+ );
1271
+ });
1272
+ ```
1273
+
1274
+ ### Subcategory Footer
1275
+
1276
+ ```tsx
1277
+ const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
1278
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
1279
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1280
+
1281
+ const visibleSubCategories = subCategories.filter((subCategory) =>
1282
+ activeCategory?.subCategoryCodes.includes(subCategory.code)
1283
+ );
1284
+
1285
+ return visibleSubCategories.map((subCategory) => {
1286
+ const active =
1287
+ activeSelection.type === "SubCategory" &&
1288
+ activeSelection.id === subCategory.code.toString();
1289
+
1290
+ return (
1291
+ <button
1292
+ key={subCategory.code}
1293
+ className={active ? "active" : ""}
1294
+ onClick={() => SelectionBridgeService.selectSubCategory(subCategory)}
1295
+ >
1296
+ {subCategory.name}
1297
+ </button>
1298
+ );
1299
+ });
1300
+ ```
1301
+
1302
+ ### Amenity List
1303
+
1304
+ ```tsx
1305
+ const amenities = useAmenityStore((s) => Object.values(s.amenities));
1306
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1307
+
1308
+ return amenities.map((amenity) => {
1309
+ const active =
1310
+ activeSelection.type === "Amenity" &&
1311
+ activeSelection.id === amenity.id.toString();
1312
+
1313
+ return (
1314
+ <button
1315
+ key={amenity.id}
1316
+ className={active ? "active" : ""}
1317
+ onClick={() => SelectionBridgeService.selectAmenity(amenity)}
1318
+ >
1319
+ {amenity.name}
1320
+ </button>
1321
+ );
1322
+ });
1323
+ ```
1324
+
1325
+ ### Store Search Selection
1326
+
1327
+ ```tsx
1328
+ function selectStore(nodeKey: string, field: "from" | "to") {
1329
+ const group = useNodePointStore.getState().locationGroupedNodeMap[nodeKey] || [nodeKey];
1330
+ const joinedNodeIds = group.join("<->");
1331
+
1332
+ if (field === "from") {
1333
+ DirectionBridgeService.setOrigin(joinedNodeIds, true, false);
1334
+ } else {
1335
+ DirectionBridgeService.setDestination(joinedNodeIds, true, false);
1336
+ }
1337
+ }
1338
+ ```
1339
+
1340
+ ### Default Location
1341
+
1342
+ Default location should stay in the host app because only the host knows the map id and user preference.
1343
+
1344
+ ```tsx
1345
+ const defaultLocationKey = getDefaultLocationForMap(mapId);
1346
+
1347
+ if (defaultLocationKey) {
1348
+ DirectionBridgeService.setOrigin(defaultLocationKey, false, false);
1349
+ }
1350
+ ```
1351
+
1352
+ To show default location badge:
1353
+
1354
+ ```tsx
1355
+ const isDefaultLocation = selectedOriginPointId === defaultLocationKey;
1356
+ ```
1357
+
1358
+ ### Get Directions Button
1359
+
1360
+ ```tsx
1361
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
1362
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
1363
+ const isLoading = useDirectionsRouteRequestStore((s) => s.isLoading);
1364
+
1365
+ return (
1366
+ <button
1367
+ disabled={!origin || !destination || isLoading}
1368
+ onClick={() =>
1369
+ DirectionBridgeService.generateDirectionsRoute({
1370
+ onSuccess: () => navigateToStepByStep(),
1371
+ })
1372
+ }
1373
+ >
1374
+ {isLoading ? "Loading..." : "Get Directions"}
1375
+ </button>
1376
+ );
1377
+ ```
1378
+
1379
+ ### Pathfinding Error
1380
+
1381
+ ```tsx
1382
+ const pathfindingError = useDirectionStore((s) => s.pathfindingError);
1383
+
1384
+ return pathfindingError ? (
1385
+ <div>
1386
+ <strong>No Routes Available</strong>
1387
+ <span>{pathfindingError}</span>
1388
+ </div>
1389
+ ) : null;
1390
+ ```
1391
+
1392
+ ### Step-by-Step Panel
1393
+
1394
+ ```tsx
1395
+ const navigationResult = useNavigationStore((s) => s.navigationResult);
1396
+
1397
+ if (!navigationResult) return null;
1398
+
1399
+ return (
1400
+ <div>
1401
+ <h3>Total Distance {navigationResult.summary.totalDistance}</h3>
1402
+ <p>Arrive in {navigationResult.summary.totalApproxTime}</p>
1403
+ {navigationResult.steps.map((step, index) => (
1404
+ <button key={step.id} onClick={() => StepNavigationBridgeService.goToStep(index)}>
1405
+ {step.instruction}
1406
+ </button>
1407
+ ))}
1408
+ </div>
1409
+ );
1410
+ ```
1411
+
1412
+ ## Image URLs
1413
+
1414
+ Image base URL comes from tenant state:
1415
+
1416
+ ```tsx
1417
+ const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
1418
+ ```
1419
+
1420
+ Category image:
1421
+
1422
+ ```tsx
1423
+ const src = category.image && imageBaseUrl ? `${imageBaseUrl}${category.image}` : "";
1424
+ ```
1425
+
1426
+ Amenity image:
1427
+
1428
+ ```tsx
1429
+ const src = amenity.image && imageBaseUrl ? `${imageBaseUrl}${amenity.image}` : "";
1430
+ ```
1431
+
1432
+ ## What Should Stay In Host App
1433
+
1434
+ Keep these in the host app:
1435
+
1436
+ - `mapId`
1437
+ - default location per map id
1438
+ - local direction input query store if your UI has text inputs
1439
+ - host-specific flow history
1440
+ - custom UI layout
1441
+ - business-specific filtering that is not part of bridge protocol
1442
+
1443
+ Use the package for:
1444
+
1445
+ - iframe/WebView bridge setup
1446
+ - reading viewer-synced stores
1447
+ - sending viewer commands
1448
+ - route result lifecycle
1449
+ - selection/offers/floor/zoom/viewer controls
1450
+
1451
+ ## Troubleshooting
1452
+
1453
+ ### Stores are empty after refresh
1454
+
1455
+ Check:
1456
+
1457
+ - iframe/WebView URL is correct
1458
+ - viewer is using embedded template and initializes bridge after map parse
1459
+ - `useBridgeStorage((s) => s.isBridgeLoaded)` becomes `true`
1460
+ - `useBridgeStorage((s) => s.isMapLoaded)` becomes `true`
1461
+ - host is not rendering UI before `mapId` exists
1462
+
1463
+ ### Category or amenity images do not show
1464
+
1465
+ Use `imageBaseUrl` from `useBridgeStorage`.
1466
+
1467
+ Correct:
1468
+
1469
+ ```tsx
1470
+ const src = imageBaseUrl && item.image ? `${imageBaseUrl}${item.image}` : "";
1471
+ ```
1472
+
1473
+ Wrong:
1474
+
1475
+ ```tsx
1476
+ const src = imageBaseUrl || "" + item.image;
1477
+ ```
1478
+
1479
+ The wrong version returns only `imageBaseUrl` when it exists.
1480
+
1481
+ ### Direction input text is not updating
1482
+
1483
+ Call `DirectionBridgeService.configure` and provide `directionInputAdapter`.
1484
+
1485
+ Without adapter:
1486
+
1487
+ - bridge command still works
1488
+ - viewer origin/destination still changes
1489
+ - local input text does not change automatically
1490
+
1491
+ ### Route success needs UI navigation
1492
+
1493
+ Use `generateDirectionsRoute` callbacks or `useDirectionsRouteRequestStore`.
1494
+
1495
+ ```ts
1496
+ DirectionBridgeService.generateDirectionsRoute({
1497
+ onSuccess: () => navigateToStepByStep(),
1498
+ onError: (result) => showError(result.error),
1499
+ });
1500
+ ```
1501
+
1502
+ ### Offers button active state
1503
+
1504
+ Use:
1505
+
1506
+ ```tsx
1507
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
1508
+ ```
1509
+
1510
+ Do not infer offer active state from category/subcategory.
1511
+
1512
+ ### Category active state
1513
+
1514
+ Use:
1515
+
1516
+ ```tsx
1517
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1518
+ const isActive =
1519
+ activeSelection.type === "Category" &&
1520
+ activeSelection.id === category.code.toString();
1521
+ ```
1522
+
1523
+ ### Subcategory unselect should keep category visible
1524
+
1525
+ Keep `activeCategory` from `useSelectionStore`.
1526
+
1527
+ ```tsx
1528
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
1529
+ ```
1530
+
1531
+ Use `activeCategory` to render subcategory list, not only `activeSelection`.
1532
+
1533
+ ### Text/view controls click but do not change
1534
+
1535
+ Make sure your control overlay is above iframe/canvas layers.
1536
+
1537
+ ```tsx
1538
+ <div className="absolute z-[1000] pointer-events-auto">
1539
+ ...
1540
+ </div>
1541
+ ```
1542
+
1543
+ ### Do not duplicate bridge services in host app
1544
+
1545
+ Do not recreate old local services like:
1546
+
1547
+ - `TemplateDirectionService`
1548
+ - `TemplateSelectionService`
1549
+ - `TemplateFloorService`
1550
+ - `TemplateStepNavigationService`
1551
+ - `TemplateZoomService`
1552
+ - `TemplateViewerService`
1553
+
1554
+ Use package exports instead:
1555
+
1556
+ ```ts
1557
+ import {
1558
+ DirectionBridgeService,
1559
+ SelectionBridgeService,
1560
+ FloorBridgeService,
1561
+ StepNavigationBridgeService,
1562
+ ZoomBridgeService,
1563
+ ViewerBridgeService,
1564
+ } from "wovvmap-webview-bridge/web";
1565
+ ```
1566
+
1567
+ ## Export Checklist
1568
+
1569
+ ### Components
1570
+
1571
+ - `WebIframeScreen`
1572
+ - `WebViewScreen`
1573
+ - `attachIframeBridge`
1574
+
1575
+ ### Services
1576
+
1577
+ - `BridgeService`
1578
+ - `DirectionBridgeService`
1579
+ - `SelectionBridgeService`
1580
+ - `FloorBridgeService`
1581
+ - `StepNavigationBridgeService`
1582
+ - `ZoomBridgeService`
1583
+ - `ViewerBridgeService`
1584
+
1585
+ ### Stores
1586
+
1587
+ - `useBridgeStorage`
1588
+ - `useViewerStore`
1589
+ - `useCategoryStore`
1590
+ - `useSubCategoryStore`
1591
+ - `useAmenityStore`
1592
+ - `useNodePointStore`
1593
+ - `useSelectionStore`
1594
+ - `useDirectionStore`
1595
+ - `useFloorStore`
1596
+ - `useNavigationStore`
1597
+ - `useDirectionsRouteRequestStore`
1598
+
1599
+ ### Handlers
1600
+
1601
+ - `registerBridgeHandler`
1602
+
1603
+ ### Types
1604
+
1605
+ - `IncomingMessage`
1606
+ - `OutgoingMessage`
1607
+ - `NodePoint`
1608
+ - `FloorImage`
1609
+ - `Amenity`
1610
+ - `Category`
1611
+ - `SubCategory`
1612
+ - `ViewerBridgeState`
1613
+ - `ViewMode`
1614
+ - `CameraView`
1615
+ - `TextType`
1616
+ - `NavigationBridgeState`
1617
+ - `DirectionsRouteResult`
1618
+ - `NavigationResult`
1619
+ - `NavigationStep`
1620
+ - `NavigationSummary`
1621
+ - `SimulationMode`
1622
+ - `DirectionBridgeServiceConfig`
1623
+ - `DirectionInputAdapter`
1624
+ - `ViewerConfig`