npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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 zustand

For React Native hosts:

npm install wovvmap-webview-bridge zustand react-native-webview

Basic 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=1

Recommended 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:

  1. Web SDK channel URL and embedded viewer load.
  2. Mall-name header after real map data is ready.
  3. Default location selection.
  4. Search store results.
  5. Store detail card.
  6. Category and subcategory browsing.
  7. Amenities and offers.
  8. Origin/destination directions.
  9. pathVisible=true active route.
  10. Step-by-step guidance.
  11. Reverse directions / swap endpoints.
  12. Floor navigation.
  13. 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: when true, viewer camera animates to the point.
  • clearSelection: when true, viewer clears active category/subcategory/amenity selection.
  • clearQuery: when true, 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, or swapOriginDestination is 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 state
  • isBridgeLoaded: bridge connection event received
  • imageBaseUrl: 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, or null before map data is ready
  • apiVersion: viewer API version, for example "v2"
  • viewMode: "2D" or "3D"
  • cameraView: "street" or "sky"
  • textType: "2D" or "3D"
  • show2DIcon: boolean
  • isMapParsed: true after viewer parsed map data
  • isLoading: 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 | null
  • isOfferSelected: 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 ids
  • selectedDestinationPointId: selected end node id or joined node ids
  • pathfindingError: route error string or null
  • routingPreferences: { 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 | null
  • error: string or null
  • isLoading: 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 or null

Example:

const floors = useFloorStore((s) => s.floors);
const activeFloor = useFloorStore((s) => s.activeFloor);

useNavigationStore

Step-by-step navigation state.

Fields:

  • activePath: NodePoint[] | null
  • navigationResult: NavigationResult | null
  • simulationMode: "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

  1. Host renders WebIframeScreen or WebViewScreen.
  2. Viewer initializes and sends grouped state events.
  3. Package handlers apply those events into Zustand stores.
  4. Host UI reads stores and renders categories, amenities, floors, offers, directions, and navigation.
  5. Host UI calls *BridgeService methods.
  6. Package sends typed bridge event to viewer.
  7. 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) becomes true
  • useBridgeStorage((s) => s.isMapLoaded) becomes true
  • host is not rendering UI before mapId exists

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:

  • TemplateDirectionService
  • TemplateSelectionService
  • TemplateFloorService
  • TemplateStepNavigationService
  • TemplateZoomService
  • TemplateViewerService

Use package exports instead:

import {
  DirectionBridgeService,
  SelectionBridgeService,
  FloorBridgeService,
  StepNavigationBridgeService,
  ZoomBridgeService,
  ViewerBridgeService,
} from "wovvmap-webview-bridge/web";

Export Checklist

Components

  • WebIframeScreen
  • WebViewScreen
  • attachIframeBridge

Services

  • BridgeService
  • DirectionBridgeService
  • SelectionBridgeService
  • FloorBridgeService
  • StepNavigationBridgeService
  • ZoomBridgeService
  • ViewerBridgeService

Stores

  • useBridgeStorage
  • useViewerStore
  • useCategoryStore
  • useSubCategoryStore
  • useAmenityStore
  • useNodePointStore
  • useSelectionStore
  • useDirectionStore
  • useFloorStore
  • useNavigationStore
  • useDirectionsRouteRequestStore

Handlers

  • registerBridgeHandler

Types

  • IncomingMessage
  • OutgoingMessage
  • NodePoint
  • FloorImage
  • Amenity
  • Category
  • SubCategory
  • ViewerBridgeState
  • ViewMode
  • CameraView
  • TextType
  • NavigationBridgeState
  • DirectionsRouteResult
  • NavigationResult
  • NavigationStep
  • NavigationSummary
  • SimulationMode
  • DirectionBridgeServiceConfig
  • DirectionInputAdapter
  • ViewerConfig