wovvmap-webview-bridge
v1.1.0
Published
`wovvmap-webview-bridge` is the host-side SDK for embedding a WovvMap viewer inside a web iframe or a React Native WebView.
Readme
wovvmap-webview-bridge
wovvmap-webview-bridge is the host-side SDK for embedding a WovvMap viewer inside a web iframe or a React Native WebView.
It gives the host app:
- iframe/WebView components
- typed bridge command services
- synced Zustand stores for viewer data
- event handlers for viewer interactions
- TypeScript types for categories, stores, directions, floors, navigation, and viewer configuration
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.
Entry Points
Use the web entry point in React web apps:
import {
WebIframeScreen,
DirectionBridgeService,
useCategoryStore,
} from "wovvmap-webview-bridge/web";Use the root entry point in React Native apps:
import {
WebViewScreen,
DirectionBridgeService,
useCategoryStore,
} from "wovvmap-webview-bridge";Installation
For web iframe hosts:
npm install wovvmap-webview-bridge zustandFor React Native hosts:
npm install wovvmap-webview-bridge zustand react-native-webviewBasic Web Iframe Setup
The host app owns the map id. The package does not store mapId because each host decides which map to open.
The embedded viewer URL must use template=embedded. If the host app has its own URL state, keep the host template separate, for example /websdk?template=kiosk, and pass template=embedded only to the iframe/WebView URL.
import { WebIframeScreen } from "wovvmap-webview-bridge/web";
export function MapHost({ mapId }: { mapId: string | null }) {
if (!mapId) {
return null;
}
return (
<WebIframeScreen
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
origin="https://viewer.example.com"
onload={() => console.log("Viewer iframe loaded")}
style={{ width: "100%", height: "100%", border: "none" }}
/>
);
}For local development this can look like:
<WebIframeScreen
url={`http://localhost:5174/viewer/${mapId}?v=v2&template=embedded`}
origin="http://localhost:5174"
style={{ width: "100%", height: "100%", border: "none" }}
/>Recommended Ready Gate
For kiosk demos and production embeds, keep the host chrome hidden until the embedded viewer has sent enough state to render the real mall UI. This avoids showing placeholder titles such as "Web SDK Demo" before the mall name arrives.
import {
WebIframeScreen,
useBridgeStorage,
useFloorStore,
useNodePointStore,
useViewerStore,
} from "wovvmap-webview-bridge/web";
export function KioskHost({ mapId }: { mapId: string }) {
const mallName = useViewerStore((s) => s.mallName);
const isMapParsed = useViewerStore((s) => s.isMapParsed);
const isLoading = useViewerStore((s) => s.isLoading);
const isBridgeLoaded = useBridgeStorage((s) => s.isBridgeLoaded);
const isMapLoaded = useBridgeStorage((s) => s.isMapLoaded);
const floorCount = useFloorStore((s) => s.floors.length);
const nodeCount = useNodePointStore((s) => s.searchableNodePointMap.length);
const isReady =
Boolean(mallName) &&
isBridgeLoaded &&
isMapLoaded &&
isMapParsed &&
!isLoading &&
floorCount > 0 &&
nodeCount > 0;
return (
<div className="host-shell">
{isReady && <Header title={`Welcome to ${mallName}`} />}
<WebIframeScreen
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
origin="https://viewer.example.com"
/>
{isReady && <Footer />}
{!isReady && <LoadingOverlay />}
</div>
);
}Demo Recording Mode
The sample web host can expose a demo URL such as:
http://localhost:5173/websdk?demo=1&resetDefault=1Recommended demo parameters:
demo=1: shows labeled walkthrough cards and runs a guided feature sequence.resetDefault=1: clears the saved default location so the default-location selection flow can be demonstrated.mapId=<id>: optionally chooses the mall map to open.template=kiosk: host template for the SDK wrapper.
The embedded viewer URL generated by the host must still use template=embedded; this parameter belongs only inside the iframe/WebView URL.
Suggested video sequence:
- Web SDK channel URL and embedded viewer load.
- Mall-name header after real map data is ready.
- Default location selection.
- Search store results.
- Store detail card.
- Category and subcategory browsing.
- Amenities and offers.
- Origin/destination directions.
pathVisible=trueactive route.- Step-by-step guidance.
- Reverse directions / swap endpoints.
- Floor navigation.
- Zoom, 2D/3D, street/top view, and text controls.
Basic React Native Setup
import { WebViewScreen } from "wovvmap-webview-bridge";
export function MapWebView({ mapId }: { mapId: string }) {
return (
<WebViewScreen
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
onload={() => console.log("Viewer loaded")}
/>
);
}Optional Manual Iframe Setup
Use attachIframeBridge when you already render your own iframe.
import { useEffect, useRef } from "react";
import { attachIframeBridge } from "wovvmap-webview-bridge/web";
export function CustomIframe({ url }: { url: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
if (!iframeRef.current) return;
return attachIframeBridge({
iframe: iframeRef.current,
origin: "https://viewer.example.com",
onLoad: () => console.log("loaded"),
});
}, []);
return <iframe ref={iframeRef} src={url} title="Wovv Map" />;
}Recommended Import Surface
Most apps should import only from "wovvmap-webview-bridge/web" or "wovvmap-webview-bridge".
import {
WebIframeScreen,
DirectionBridgeService,
SelectionBridgeService,
FloorBridgeService,
StepNavigationBridgeService,
ZoomBridgeService,
ViewerBridgeService,
useBridgeStorage,
useViewerStore,
useCategoryStore,
useSubCategoryStore,
useAmenityStore,
useNodePointStore,
useSelectionStore,
useDirectionStore,
useFloorStore,
useNavigationStore,
useDirectionsRouteRequestStore,
registerBridgeHandler,
} from "wovvmap-webview-bridge/web";Exported Components
WebIframeScreen
Web-only component for embedding the WovvMap viewer in an iframe.
Props:
type WebIframeScreenProps = {
url: string;
origin?: string;
onload?: () => void;
title?: string;
style?: React.CSSProperties;
};Use this in React web apps.
WebViewScreen
React Native component for embedding the WovvMap viewer in react-native-webview.
Props:
type WebViewScreenProps = {
url: string;
onload?: () => void;
};Use this in React Native apps.
attachIframeBridge
Manual bridge attachment for custom iframe implementations.
attachIframeBridge({
iframe,
origin,
onLoad,
});Command Services
Use these services from host UI. They update host-side package stores when needed and send the correct bridge event to the embedded viewer.
DirectionBridgeService
Controls origin, destination, route generation, path clearing, and origin/destination swap.
Methods:
DirectionBridgeService.configure(config);
DirectionBridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
DirectionBridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
DirectionBridgeService.clearDestination(clearQuery);
DirectionBridgeService.clearOrigin();
DirectionBridgeService.swapOriginDestination();
DirectionBridgeService.generateDirectionsRoute(callbacks);
DirectionBridgeService.clearActivePath();Parameters:
nodeId: a node key string. Multiple store locations can be joined with"<->", for example"2105,1601,3<->3166,1570,3".cameraAnimate: whentrue, viewer camera animates to the point.clearSelection: whentrue, viewer clears active category/subcategory/amenity selection.clearQuery: whentrue, your host input adapter clears the visible input text.
Basic destination example:
DirectionBridgeService.setDestination("2105,1601,3", true, false);Basic origin example:
DirectionBridgeService.setOrigin("4360,2330,5", true, true);Clear destination:
DirectionBridgeService.clearDestination(true);Generate route:
DirectionBridgeService.generateDirectionsRoute({
onSuccess: (result) => {
console.log("Route ready", result.navigationResult);
},
onError: (result) => {
console.log("Route failed", result.error);
},
});Swap from/to:
DirectionBridgeService.swapOriginDestination();Clear drawn path:
DirectionBridgeService.clearActivePath();Direction Input Adapter
The package does not own your input UI state. If your app has local "From" and "To" input text, connect it once with configure.
import {
DirectionBridgeService,
SelectionBridgeService,
useNodePointStore,
} from "wovvmap-webview-bridge/web";
import { useDirectionInputStore } from "./store/useDirectionInputStore";
DirectionBridgeService.configure({
directionInputAdapter: {
setDestinationState: (query, selectedId) => {
useDirectionInputStore.getState().setDestinationState(query, selectedId);
},
setOriginState: (query, selectedId) => {
useDirectionInputStore.getState().setOriginState(query, selectedId);
},
clearDestinationState: (clearQuery) => {
useDirectionInputStore.getState().clearDestinationState(clearQuery);
},
clearOriginState: () => {
useDirectionInputStore.getState().clearOriginState();
},
setActiveDirectionField: (field) => {
useDirectionInputStore.getState().setActiveDirectionField(field);
},
getOriginQuery: () => useDirectionInputStore.getState().originQuery,
getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
},
resolveLocationName: (nodeId) => {
const nodeKey = nodeId.split("<->")[0];
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
return node?.assets?.locationName?.text || nodeKey;
},
onClearSelection: () => {
SelectionBridgeService.clearSelection();
},
});Why this exists:
- different companies can build different input UIs
- the SDK still updates local input text when
setOrigin,setDestination, orswapOriginDestinationis called - SDK code does not import app-specific stores
If you do not configure an adapter, direction commands still work. Only local input text will not auto-update.
SelectionBridgeService
Controls category, subcategory, amenity, offers, and clear selection.
Methods:
SelectionBridgeService.selectCategory(category);
SelectionBridgeService.selectSubCategory(subCategory);
SelectionBridgeService.selectAmenity(amenity);
SelectionBridgeService.toggleOffers();
SelectionBridgeService.clearSelection();
SelectionBridgeService.syncHighlightsWithState();Category example:
import { SelectionBridgeService, useCategoryStore } from "wovvmap-webview-bridge/web";
function CategoryButton({ code }: { code: string }) {
const category = useCategoryStore((s) => s.categories[code]);
const activeSelection = useSelectionStore((s) => s.activeSelection);
if (!category) return null;
const isActive =
activeSelection.type === "Category" &&
activeSelection.id === category.code.toString();
return (
<button
className={isActive ? "active" : ""}
onClick={() => SelectionBridgeService.selectCategory(category)}
>
{category.name}
</button>
);
}Subcategory example:
SelectionBridgeService.selectSubCategory(subCategory);Amenity example:
SelectionBridgeService.selectAmenity(amenity);Amenity selection also sets the destination with the amenity node keys:
DirectionBridgeService.setDestination(amenity.nodePointsKey.join("<->"), true, false);Offers button example:
import { SelectionBridgeService, useNodePointStore, useSelectionStore } from "wovvmap-webview-bridge/web";
export function OffersButton() {
const offersNodeMap = useNodePointStore((s) => s.offersNodeMap);
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
const hasOffers = (offersNodeMap.all_offers?.nodePointsKey?.length ?? 0) > 0;
if (!hasOffers) return null;
return (
<button
className={isOfferSelected ? "active" : ""}
onClick={() => SelectionBridgeService.toggleOffers()}
>
Offers
</button>
);
}Important selection states:
- Category active:
useSelectionStore((s) => s.activeSelection.type === "Category") - Selected category id:
useSelectionStore((s) => s.activeSelection.id) - Active category object:
useSelectionStore((s) => s.activeCategory) - Subcategory active:
activeSelection.type === "SubCategory" - Amenity active:
activeSelection.type === "Amenity" - Offers active:
useSelectionStore((s) => s.isOfferSelected)
FloorBridgeService
Controls active floor.
FloorBridgeService.changeFloor(floorIndex, animateCamera);Floor selector example:
import { FloorBridgeService, useFloorStore } from "wovvmap-webview-bridge/web";
export function FloorSelector() {
const floors = useFloorStore((s) => s.floors);
const activeFloor = useFloorStore((s) => s.activeFloor);
return (
<select
value={activeFloor ?? ""}
onChange={(event) => FloorBridgeService.changeFloor(Number(event.target.value), true)}
>
{floors.map((floor) => (
<option key={floor.index ?? floor.FloorNumber} value={floor.index}>
{floor.FloorName}
</option>
))}
</select>
);
}StepNavigationBridgeService
Controls current step in step-by-step navigation.
StepNavigationBridgeService.goToStep(index);
StepNavigationBridgeService.nextStep();
StepNavigationBridgeService.prevStep();
StepNavigationBridgeService.finishStep();Step list example:
import { StepNavigationBridgeService, useNavigationStore } from "wovvmap-webview-bridge/web";
export function StepList() {
const result = useNavigationStore((s) => s.navigationResult);
const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
if (!result) return null;
return (
<div>
{result.steps.map((step, index) => (
<button
key={step.id}
className={currentStepIndex === index ? "active" : ""}
onClick={() => StepNavigationBridgeService.goToStep(index)}
>
{step.instruction}
</button>
))}
</div>
);
}Next/previous example:
<button onClick={() => StepNavigationBridgeService.prevStep()}>Previous</button>
<button onClick={() => StepNavigationBridgeService.nextStep()}>Next</button>
<button onClick={() => StepNavigationBridgeService.finishStep()}>Finish</button>ZoomBridgeService
Controls viewer zoom.
ZoomBridgeService.zoomIn();
ZoomBridgeService.zoomOut();Example:
<button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
<button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>ViewerBridgeService
Controls viewer mode, camera view, and text rendering.
ViewerBridgeService.setViewMode("2D", true);
ViewerBridgeService.setViewMode("3D", true);
ViewerBridgeService.setCameraView("street");
ViewerBridgeService.setCameraView("sky");
ViewerBridgeService.setTextType("2D");
ViewerBridgeService.setTextType("3D");
ViewerBridgeService.setShow2DIcon(true);
ViewerBridgeService.setShow2DIcon(false);Toolbar example:
import { ViewerBridgeService, useViewerStore } from "wovvmap-webview-bridge/web";
export function ViewerToolbar() {
const viewMode = useViewerStore((s) => s.viewMode);
const cameraView = useViewerStore((s) => s.cameraView);
const textType = useViewerStore((s) => s.textType);
const show2DIcon = useViewerStore((s) => s.show2DIcon);
return (
<div>
<button
className={viewMode === "2D" ? "active" : ""}
onClick={() => ViewerBridgeService.setViewMode("2D", true)}
>
2D View
</button>
<button
className={viewMode === "3D" ? "active" : ""}
onClick={() => ViewerBridgeService.setViewMode("3D", true)}
>
3D View
</button>
<button
className={cameraView === "street" ? "active" : ""}
onClick={() => ViewerBridgeService.setCameraView("street")}
>
Street
</button>
<button
className={cameraView === "sky" ? "active" : ""}
onClick={() => ViewerBridgeService.setCameraView("sky")}
>
Sky
</button>
<button
className={textType === "2D" ? "active" : ""}
onClick={() => ViewerBridgeService.setTextType("2D")}
>
2D Text
</button>
<button
className={textType === "3D" ? "active" : ""}
onClick={() => ViewerBridgeService.setTextType("3D")}
>
3D Text
</button>
<button
className={show2DIcon ? "active" : ""}
onClick={() => ViewerBridgeService.setShow2DIcon(!show2DIcon)}
>
2D Icon
</button>
</div>
);
}BridgeService
Low-level event sender. Use command services above for normal UI code.
Available low-level methods:
BridgeService.setViewerConfig(config);
BridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
BridgeService.clearDestination();
BridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
BridgeService.clearOrigin();
BridgeService.swapOriginDestination();
BridgeService.setActiveSelection(type, code);
BridgeService.clearActiveSelection();
BridgeService.setOfferSelected(isSelected);
BridgeService.generateDirectionsRoute(callbacks);
BridgeService.clearActivePath();
BridgeService.changeFloor(floorIndex, animateCamera);
BridgeService.goToStep(index);
BridgeService.nextStep();
BridgeService.prevStep();
BridgeService.finishStep();
BridgeService.zoomIn();
BridgeService.zoomOut();
BridgeService.setViewMode(viewMode, animate);
BridgeService.setCameraView(cameraView);
BridgeService.setTextType(textType);
BridgeService.setShow2DIcon(show2DIcon);Synced Stores
All stores are Zustand stores. Use them directly in React components.
useBridgeStorage
Bridge and tenant state.
Fields:
isMapLoaded: viewer sent map loaded stateisBridgeLoaded: bridge connection event receivedimageBaseUrl: base URL for category, subcategory, amenity, and store images
Example:
const isReady = useBridgeStorage((s) => s.isBridgeLoaded && s.isMapLoaded);
const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);useViewerStore
Viewer configuration and loading state.
Fields:
mallName: mall name from the embedded viewer, ornullbefore map data is readyapiVersion: viewer API version, for example"v2"viewMode:"2D"or"3D"cameraView:"street"or"sky"textType:"2D"or"3D"show2DIcon: booleanisMapParsed: true after viewer parsed map dataisLoading: viewer loading flag
Example:
const mallName = useViewerStore((s) => s.mallName);
const viewMode = useViewerStore((s) => s.viewMode);
const cameraView = useViewerStore((s) => s.cameraView);useCategoryStore
Category catalog.
Fields:
categories:Record<string, Category>
Example:
const categories = useCategoryStore((s) => Object.values(s.categories));Category active state:
const activeSelection = useSelectionStore((s) => s.activeSelection);
const isActiveCategory =
activeSelection.type === "Category" &&
activeSelection.id === category.code.toString();useSubCategoryStore
Subcategory catalog.
Fields:
subCategories:Record<string, SubCategory>
Example:
const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));Subcategory active state:
const isActiveSubCategory =
activeSelection.type === "SubCategory" &&
activeSelection.id === subCategory.code.toString();useAmenityStore
Amenity catalog.
Fields:
amenities:Record<string, Amenity>
Example:
const amenities = useAmenityStore((s) => Object.values(s.amenities));Amenity active state:
const isActiveAmenity =
activeSelection.type === "Amenity" &&
activeSelection.id === amenity.id.toString();useNodePointStore
Searchable stores/locations and node maps.
Fields:
searchableNodePointMap:NodePoint[]locationGroupedNodeMap:Record<string, string[]>keyNodeMap:Record<string, NodePoint>offersNodeMap:OffersNodeMap
Actions/helpers:
getNodePointByKey(key)clearSearchableNodePoints()
Example: search source data:
const nodePoints = useNodePointStore((s) => s.searchableNodePointMap);Example: get store name:
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
const name = node?.assets?.locationName?.text || node?.key || "";Example: group multi-location store:
const groupKeys = useNodePointStore.getState().locationGroupedNodeMap["Aldo"] || [];
DirectionBridgeService.setDestination(groupKeys.join("<->"), true, false);Example: offers data:
const allOffers = useNodePointStore((s) => s.offersNodeMap.all_offers);
const hasOffers = (allOffers?.nodePointsKey?.length ?? 0) > 0;useSelectionStore
Active category/subcategory/amenity and offers state.
Fields:
activeSelection:{ type: "Category" | "SubCategory" | "Amenity" | null; id: string | null }activeCategory:Category | nullisOfferSelected: boolean
Examples:
const activeSelection = useSelectionStore((s) => s.activeSelection);
const activeCategory = useSelectionStore((s) => s.activeCategory);
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);Offer button active:
const isActive = useSelectionStore((s) => s.isOfferSelected);Category tab active:
const isActive =
activeSelection.type === "Category" &&
activeSelection.id === category.code.toString();Subcategory tab active:
const isActive =
activeSelection.type === "SubCategory" &&
activeSelection.id === subCategory.code.toString();Amenity active:
const isActive =
activeSelection.type === "Amenity" &&
activeSelection.id === amenity.id.toString();useDirectionStore
Origin/destination and pathfinding state mirrored from the viewer.
Fields:
selectedOriginPointId: selected start node id or joined node idsselectedDestinationPointId: selected end node id or joined node idspathfindingError: route error string ornullroutingPreferences:{ elevator?: boolean; escalator?: boolean; stair?: boolean }
Examples:
const origin = useDirectionStore((s) => s.selectedOriginPointId);
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
const pathfindingError = useDirectionStore((s) => s.pathfindingError);
const routingPreferences = useDirectionStore((s) => s.routingPreferences);Show route error:
{pathfindingError && <div>No Routes Available: {pathfindingError}</div>}Enable get directions button:
const canGenerateRoute = Boolean(origin && destination);Update routing preference from host:
useDirectionStore.getState().setRoutingPreferences({
elevator: true,
escalator: false,
});useDirectionsRouteRequestStore
Request state for DirectionBridgeService.generateDirectionsRoute.
Fields:
status:"idle" | "loading" | "success" | "error"result:DirectionsRouteResult | nullerror: string ornullisLoading: boolean
Example:
const routeRequest = useDirectionsRouteRequestStore();
if (routeRequest.isLoading) {
return <div>Generating route...</div>;
}
if (routeRequest.status === "error") {
return <div>{routeRequest.error}</div>;
}Navigate to step-by-step only when route succeeds:
DirectionBridgeService.generateDirectionsRoute({
onSuccess: () => {
navigateToStepByStep();
},
onError: (result) => {
console.log(result.error);
},
});useFloorStore
Floor list and current floor.
Fields:
floors:FloorImage[]activeFloor: number ornull
Example:
const floors = useFloorStore((s) => s.floors);
const activeFloor = useFloorStore((s) => s.activeFloor);useNavigationStore
Step-by-step navigation state.
Fields:
activePath:NodePoint[] | nullnavigationResult:NavigationResult | nullsimulationMode:"none" | "full" | "current"currentStepIndex: number
Example:
const navigationResult = useNavigationStore((s) => s.navigationResult);
const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);Step data:
navigationResult?.steps.map((step) => (
<div key={step.id}>
{step.instruction} - {step.distanceText}
</div>
));Viewer Click Events
Use registerBridgeHandler for viewer interaction events that should trigger host UI behavior.
Currently supported handler:
isShapeClick: fires when a map shape/store is clicked in the viewer. Value is the clicked node id.
Example:
import { registerBridgeHandler } from "wovvmap-webview-bridge/web";
useEffect(() => {
registerBridgeHandler("isShapeClick", (message) => {
console.log("Clicked node id", message.value);
openStoreDetail(message.value);
});
}, []);Full Kiosk Host Example
This example shows the main pieces together.
import { useEffect, useRef } from "react";
import {
WebIframeScreen,
DirectionBridgeService,
SelectionBridgeService,
ViewerBridgeService,
ZoomBridgeService,
useCategoryStore,
useDirectionStore,
useNodePointStore,
registerBridgeHandler,
} from "wovvmap-webview-bridge/web";
import { useDirectionInputStore } from "./useDirectionInputStore";
export function KioskMap({ mapId }: { mapId: string | null }) {
const categories = useCategoryStore((s) => Object.values(s.categories));
const origin = useDirectionStore((s) => s.selectedOriginPointId);
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
const configuredRef = useRef(false);
useEffect(() => {
if (configuredRef.current) return;
configuredRef.current = true;
DirectionBridgeService.configure({
directionInputAdapter: {
setDestinationState: (query, selectedId) =>
useDirectionInputStore.getState().setDestinationState(query, selectedId),
setOriginState: (query, selectedId) =>
useDirectionInputStore.getState().setOriginState(query, selectedId),
clearDestinationState: (clearQuery) =>
useDirectionInputStore.getState().clearDestinationState(clearQuery),
clearOriginState: () =>
useDirectionInputStore.getState().clearOriginState(),
setActiveDirectionField: (field) =>
useDirectionInputStore.getState().setActiveDirectionField(field),
getOriginQuery: () => useDirectionInputStore.getState().originQuery,
getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
},
resolveLocationName: (nodeId) => {
const nodeKey = nodeId.split("<->")[0];
const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
return node?.assets?.locationName?.text || nodeKey;
},
onClearSelection: () => SelectionBridgeService.clearSelection(),
});
}, []);
useEffect(() => {
registerBridgeHandler("isShapeClick", (message) => {
DirectionBridgeService.setDestination(message.value, true, false);
});
}, []);
if (!mapId) return null;
return (
<div>
<WebIframeScreen
url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
origin="https://viewer.example.com"
/>
<div>
{categories.map((category) => (
<button
key={category.code}
onClick={() => SelectionBridgeService.selectCategory(category)}
>
{category.name}
</button>
))}
</div>
<button onClick={() => ViewerBridgeService.setViewMode("2D", true)}>
2D View
</button>
<button onClick={() => ViewerBridgeService.setViewMode("3D", true)}>
3D View
</button>
<button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
<button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
<button
disabled={!origin || !destination}
onClick={() =>
DirectionBridgeService.generateDirectionsRoute({
onSuccess: () => console.log("route ready"),
})
}
>
Get Directions
</button>
</div>
);
}Data Flow
- Host renders
WebIframeScreenorWebViewScreen. - Viewer initializes and sends grouped state events.
- Package handlers apply those events into Zustand stores.
- Host UI reads stores and renders categories, amenities, floors, offers, directions, and navigation.
- Host UI calls
*BridgeServicemethods. - Package sends typed bridge event to viewer.
- Viewer updates its internal state/scene and sends fresh grouped state back.
Incoming Viewer Events
These are sent by the viewer to the host package and applied into stores automatically.
| Key | Store updated | Value |
| --- | --- | --- |
| bridgeState | useBridgeStorage.isMapLoaded | { isMapLoaded: boolean } |
| isConnection | useBridgeStorage.isBridgeLoaded | boolean |
| tenantState | useBridgeStorage.imageBaseUrl | { imageBaseUrl } |
| viewerState | useViewerStore | mallName, API version, view mode, camera/text settings, parsed/loading state |
| catalogState | useCategoryStore, useSubCategoryStore, useAmenityStore | categories, subCategories, amenities |
| floorState | useFloorStore | activeFloor, floors |
| nodePointState | useNodePointStore | searchableNodePointMap, locationGroupedNodeMap, keyNodeMap, offersNodeMap |
| selectionState | useSelectionStore | activeSelection, activeCategory, isOfferSelected |
| directionState | useDirectionStore | selectedOriginPointId, selectedDestinationPointId, pathfindingError, routingPreferences |
| navigationState | useNavigationStore | activePath, navigationResult, simulationMode, currentStepIndex |
| directionsRouteResult | useDirectionsRouteRequestStore | status, navigationResult, error |
| isShapeClick | event handler | clicked node id |
Outgoing Host Commands
These are sent by command services to the viewer.
| Service | Message key |
| --- | --- |
| DirectionBridgeService.setDestination | setDestination |
| DirectionBridgeService.clearDestination | clearDestination |
| DirectionBridgeService.setOrigin | setOrigin |
| DirectionBridgeService.clearOrigin | clearOrigin |
| DirectionBridgeService.swapOriginDestination | swapOriginDestination |
| DirectionBridgeService.generateDirectionsRoute | generateDirectionsRoute |
| DirectionBridgeService.clearActivePath | clearActivePath |
| SelectionBridgeService.selectCategory | setActiveSelection |
| SelectionBridgeService.selectSubCategory | setActiveSelection |
| SelectionBridgeService.selectAmenity | setActiveSelection plus destination |
| SelectionBridgeService.toggleOffers | setOfferSelected |
| SelectionBridgeService.clearSelection | clearActiveSelection and setOfferSelected(false) |
| FloorBridgeService.changeFloor | changeFloor |
| StepNavigationBridgeService.goToStep | goToStep |
| StepNavigationBridgeService.nextStep | nextStep |
| StepNavigationBridgeService.prevStep | prevStep |
| StepNavigationBridgeService.finishStep | finishStep |
| ZoomBridgeService.zoomIn | zoomIn |
| ZoomBridgeService.zoomOut | zoomOut |
| ViewerBridgeService.setViewMode | setViewMode |
| ViewerBridgeService.setCameraView | setCameraView |
| ViewerBridgeService.setTextType | setTextType |
| ViewerBridgeService.setShow2DIcon | setShow2DIcon |
Type Exports
The package exports these core types:
type IncomingMessage;
type OutgoingMessage;
type NodePoint;
type FloorImage;
type Amenity;
type Category;
type SubCategory;
type ViewerBridgeState;
type ViewMode;
type CameraView;
type TextType;
type NavigationBridgeState;
type DirectionsRouteResult;
type NavigationResult;
type NavigationStep;
type NavigationSummary;
type SimulationMode;
type DirectionBridgeServiceConfig;
type DirectionInputAdapter;
type ViewerConfig;Important Type Shapes
Category
type Category = {
code: string | number;
name: string;
image?: string;
subCategoryCodes: string[] | number[];
nodePointsKey?: string[];
nodePoints?: NodePoint[];
};SubCategory
type SubCategory = {
code: string | number;
name: string;
image?: string;
description?: string;
categoryCode: string | number;
nodePointsKey?: string[];
nodePoints?: NodePoint[];
};Amenity
type Amenity = {
id: string | number;
status: number;
fieldCode: string | number;
name: string;
image: string;
wayfinding_id?: string | number | null;
nodePointsKey?: string[];
nodePoints?: NodePoint[];
};NodePoint
Important fields:
type NodePoint = {
key: string;
floorIndex: number;
floorName: string;
assets: {
locationName: { text: string } | null;
pointImages: unknown | null;
logo: string | null;
brandLogo: string | null;
brandImages: string[];
};
categoryCode?: string | number;
subCategoryCode?: string | number;
offers: NodePointOffer[];
up: boolean;
down: boolean;
};DirectionsRouteResult
type DirectionsRouteResult = {
status: "success" | "error";
navigationResult: NavigationResult | null;
error: string | null;
};NavigationStep
type NavigationStep = {
id: string;
instruction: string;
action:
| "straight"
| "turn_left"
| "turn_right"
| "take_elevator"
| "take_escalator"
| "take_elevator_up"
| "take_elevator_down"
| "take_escalator_up"
| "take_escalator_down"
| "exit_elevator"
| "exit_escalator"
| "start"
| "arrive";
distanceText: string;
timeText: string;
floorIndex: number;
};Common UI Recipes
Category Footer
const categories = useCategoryStore((s) => Object.values(s.categories));
const activeSelection = useSelectionStore((s) => s.activeSelection);
return categories.map((category) => {
const active =
activeSelection.type === "Category" &&
activeSelection.id === category.code.toString();
return (
<button
key={category.code}
className={active ? "active" : ""}
onClick={() => SelectionBridgeService.selectCategory(category)}
>
{category.name}
</button>
);
});Subcategory Footer
const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
const activeCategory = useSelectionStore((s) => s.activeCategory);
const activeSelection = useSelectionStore((s) => s.activeSelection);
const visibleSubCategories = subCategories.filter((subCategory) =>
activeCategory?.subCategoryCodes.includes(subCategory.code)
);
return visibleSubCategories.map((subCategory) => {
const active =
activeSelection.type === "SubCategory" &&
activeSelection.id === subCategory.code.toString();
return (
<button
key={subCategory.code}
className={active ? "active" : ""}
onClick={() => SelectionBridgeService.selectSubCategory(subCategory)}
>
{subCategory.name}
</button>
);
});Amenity List
const amenities = useAmenityStore((s) => Object.values(s.amenities));
const activeSelection = useSelectionStore((s) => s.activeSelection);
return amenities.map((amenity) => {
const active =
activeSelection.type === "Amenity" &&
activeSelection.id === amenity.id.toString();
return (
<button
key={amenity.id}
className={active ? "active" : ""}
onClick={() => SelectionBridgeService.selectAmenity(amenity)}
>
{amenity.name}
</button>
);
});Store Search Selection
function selectStore(nodeKey: string, field: "from" | "to") {
const group = useNodePointStore.getState().locationGroupedNodeMap[nodeKey] || [nodeKey];
const joinedNodeIds = group.join("<->");
if (field === "from") {
DirectionBridgeService.setOrigin(joinedNodeIds, true, false);
} else {
DirectionBridgeService.setDestination(joinedNodeIds, true, false);
}
}Default Location
Default location should stay in the host app because only the host knows the map id and user preference.
const defaultLocationKey = getDefaultLocationForMap(mapId);
if (defaultLocationKey) {
DirectionBridgeService.setOrigin(defaultLocationKey, false, false);
}To show default location badge:
const isDefaultLocation = selectedOriginPointId === defaultLocationKey;Get Directions Button
const origin = useDirectionStore((s) => s.selectedOriginPointId);
const destination = useDirectionStore((s) => s.selectedDestinationPointId);
const isLoading = useDirectionsRouteRequestStore((s) => s.isLoading);
return (
<button
disabled={!origin || !destination || isLoading}
onClick={() =>
DirectionBridgeService.generateDirectionsRoute({
onSuccess: () => navigateToStepByStep(),
})
}
>
{isLoading ? "Loading..." : "Get Directions"}
</button>
);Pathfinding Error
const pathfindingError = useDirectionStore((s) => s.pathfindingError);
return pathfindingError ? (
<div>
<strong>No Routes Available</strong>
<span>{pathfindingError}</span>
</div>
) : null;Step-by-Step Panel
const navigationResult = useNavigationStore((s) => s.navigationResult);
if (!navigationResult) return null;
return (
<div>
<h3>Total Distance {navigationResult.summary.totalDistance}</h3>
<p>Arrive in {navigationResult.summary.totalApproxTime}</p>
{navigationResult.steps.map((step, index) => (
<button key={step.id} onClick={() => StepNavigationBridgeService.goToStep(index)}>
{step.instruction}
</button>
))}
</div>
);Image URLs
Image base URL comes from tenant state:
const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);Category image:
const src = category.image && imageBaseUrl ? `${imageBaseUrl}${category.image}` : "";Amenity image:
const src = amenity.image && imageBaseUrl ? `${imageBaseUrl}${amenity.image}` : "";What Should Stay In Host App
Keep these in the host app:
mapId- default location per map id
- local direction input query store if your UI has text inputs
- host-specific flow history
- custom UI layout
- business-specific filtering that is not part of bridge protocol
Use the package for:
- iframe/WebView bridge setup
- reading viewer-synced stores
- sending viewer commands
- route result lifecycle
- selection/offers/floor/zoom/viewer controls
Troubleshooting
Stores are empty after refresh
Check:
- iframe/WebView URL is correct
- viewer is using embedded template and initializes bridge after map parse
useBridgeStorage((s) => s.isBridgeLoaded)becomestrueuseBridgeStorage((s) => s.isMapLoaded)becomestrue- host is not rendering UI before
mapIdexists
Category or amenity images do not show
Use imageBaseUrl from useBridgeStorage.
Correct:
const src = imageBaseUrl && item.image ? `${imageBaseUrl}${item.image}` : "";Wrong:
const src = imageBaseUrl || "" + item.image;The wrong version returns only imageBaseUrl when it exists.
Direction input text is not updating
Call DirectionBridgeService.configure and provide directionInputAdapter.
Without adapter:
- bridge command still works
- viewer origin/destination still changes
- local input text does not change automatically
Route success needs UI navigation
Use generateDirectionsRoute callbacks or useDirectionsRouteRequestStore.
DirectionBridgeService.generateDirectionsRoute({
onSuccess: () => navigateToStepByStep(),
onError: (result) => showError(result.error),
});Offers button active state
Use:
const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);Do not infer offer active state from category/subcategory.
Category active state
Use:
const activeSelection = useSelectionStore((s) => s.activeSelection);
const isActive =
activeSelection.type === "Category" &&
activeSelection.id === category.code.toString();Subcategory unselect should keep category visible
Keep activeCategory from useSelectionStore.
const activeCategory = useSelectionStore((s) => s.activeCategory);Use activeCategory to render subcategory list, not only activeSelection.
Text/view controls click but do not change
Make sure your control overlay is above iframe/canvas layers.
<div className="absolute z-[1000] pointer-events-auto">
...
</div>Do not duplicate bridge services in host app
Do not recreate old local services like:
TemplateDirectionServiceTemplateSelectionServiceTemplateFloorServiceTemplateStepNavigationServiceTemplateZoomServiceTemplateViewerService
Use package exports instead:
import {
DirectionBridgeService,
SelectionBridgeService,
FloorBridgeService,
StepNavigationBridgeService,
ZoomBridgeService,
ViewerBridgeService,
} from "wovvmap-webview-bridge/web";Export Checklist
Components
WebIframeScreenWebViewScreenattachIframeBridge
Services
BridgeServiceDirectionBridgeServiceSelectionBridgeServiceFloorBridgeServiceStepNavigationBridgeServiceZoomBridgeServiceViewerBridgeService
Stores
useBridgeStorageuseViewerStoreuseCategoryStoreuseSubCategoryStoreuseAmenityStoreuseNodePointStoreuseSelectionStoreuseDirectionStoreuseFloorStoreuseNavigationStoreuseDirectionsRouteRequestStore
Handlers
registerBridgeHandler
Types
IncomingMessageOutgoingMessageNodePointFloorImageAmenityCategorySubCategoryViewerBridgeStateViewModeCameraViewTextTypeNavigationBridgeStateDirectionsRouteResultNavigationResultNavigationStepNavigationSummarySimulationModeDirectionBridgeServiceConfigDirectionInputAdapterViewerConfig
