@dtechph/wayfinding-react-native 0.1.0 → 0.2.0

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.
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { type ViewStyle } from "react-native";
3
- import type { DuonMall } from "@dtechph/wayfinding-core";
3
+ import { type DuonMall } from "@dtechph/wayfinding-core";
4
4
  export interface DuonMallSelectorProps {
5
5
  malls: DuonMall[];
6
6
  selectedMall?: DuonMall | null;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
3
  import { ActivityIndicator, FlatList, Modal, Pressable, StyleSheet, Text, View, } from "react-native";
4
+ import { setActiveMall } from "@dtechph/wayfinding-core";
4
5
  function mallLabel(mall) {
5
6
  return `${mall.name} (${mall.mapType.toUpperCase()})`;
6
7
  }
@@ -23,6 +24,7 @@ export function DuonMallSelector({ malls, selectedMall, onSelect, loading = fals
23
24
  return (_jsxs(View, { style: [styles.container, style], children: [_jsx(Text, { style: styles.label, children: "Select mall" }), _jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { expanded: open }, style: styles.trigger, onPress: () => setOpen(true), children: [_jsx(Text, { style: styles.triggerText, numberOfLines: 1, children: selected ? mallLabel(selected) : "Select a mall" }), _jsx(Text, { style: styles.chevron, children: "\u25BE" })] }), _jsx(Modal, { visible: open, transparent: true, animationType: "fade", onRequestClose: () => setOpen(false), children: _jsxs(View, { style: styles.backdrop, children: [_jsx(Pressable, { style: StyleSheet.absoluteFill, onPress: () => setOpen(false), accessibilityLabel: "Dismiss mall selector" }), _jsxs(View, { style: styles.sheet, children: [_jsx(Text, { style: styles.sheetTitle, children: "Select mall" }), _jsx(FlatList, { data: malls, keyExtractor: (mall) => `${mall.buildingId}-${mall.name}`, keyboardShouldPersistTaps: "handled", renderItem: ({ item }) => {
24
25
  const isSelected = item.buildingId === selected?.buildingId;
25
26
  return (_jsx(Pressable, { style: [styles.option, isSelected && styles.optionSelected], onPress: () => {
27
+ setActiveMall(item);
26
28
  onSelect(item);
27
29
  setOpen(false);
28
30
  }, children: _jsx(Text, { style: [
@@ -1,10 +1,14 @@
1
1
  import React from "react";
2
2
  import { type ViewStyle } from "react-native";
3
+ import { type DuonMall } from "@dtechph/wayfinding-core";
3
4
  export interface DuonMapViewProps {
4
- url: string;
5
+ /** Prefer passing mall — Situm malls use native map + auto analytics. */
6
+ mall?: DuonMall | null;
7
+ /** Legacy: kiosk / embed URL. Used when mall is kiosk or mall omitted. */
8
+ url?: string;
5
9
  style?: ViewStyle;
6
10
  onLoadStart?: () => void;
7
11
  onLoadEnd?: () => void;
8
12
  onError?: (message: string) => void;
9
13
  }
10
- export declare function DuonMapView({ url, style, onLoadStart, onLoadEnd, onError, }: DuonMapViewProps): React.JSX.Element;
14
+ export declare function DuonMapView({ mall, url, style, onLoadStart, onLoadEnd, onError, }: DuonMapViewProps): React.JSX.Element;
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useState } from "react";
3
3
  import { Pressable, StyleSheet, Text, View, } from "react-native";
4
4
  import { WebView } from "react-native-webview";
5
+ import { trackMapError, trackMapLoad, } from "@dtechph/wayfinding-core";
6
+ import { DuonSitumMapView } from "./DuonSitumMapView";
5
7
  import { isAllowedMapNavigationUrl } from "./navigationGuard";
6
- export function DuonMapView({ url, style, onLoadStart, onLoadEnd, onError, }) {
8
+ export function DuonMapView({ mall, url, style, onLoadStart, onLoadEnd, onError, }) {
7
9
  const [hasError, setHasError] = useState(false);
8
10
  const [reloadKey, setReloadKey] = useState(0);
9
11
  const handleRetry = useCallback(() => {
@@ -13,21 +15,37 @@ export function DuonMapView({ url, style, onLoadStart, onLoadEnd, onError, }) {
13
15
  const onNavigationRequest = useCallback((request) => {
14
16
  return isAllowedMapNavigationUrl(request.url);
15
17
  }, []);
16
- if (!url) {
18
+ if (mall?.mapType === "situm" && mall.situmApiKey) {
19
+ return (_jsx(DuonSitumMapView, { mall: mall, style: style, onLoadStart: onLoadStart, onLoadEnd: onLoadEnd, onError: onError }));
20
+ }
21
+ const resolvedUrl = url || mall?.viewerUrl || "";
22
+ if (!resolvedUrl) {
17
23
  return (_jsx(View, { style: [styles.center, style], children: _jsx(Text, { style: styles.errorText, children: "No map URL provided" }) }));
18
24
  }
19
- return (_jsxs(View, { style: [styles.container, style], children: [_jsx(WebView, { source: { uri: url }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, onLoadStart: () => {
25
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsx(WebView, { source: { uri: resolvedUrl }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, onLoadStart: () => {
20
26
  setHasError(false);
21
27
  onLoadStart?.();
22
28
  }, onLoadEnd: () => {
29
+ trackMapLoad({
30
+ buildingId: mall?.buildingId,
31
+ mapType: mall?.mapType || "kiosk",
32
+ });
23
33
  onLoadEnd?.();
24
34
  }, onError: () => {
25
35
  setHasError(true);
36
+ trackMapError({
37
+ message: "Unable to load map",
38
+ buildingId: mall?.buildingId,
39
+ });
26
40
  onError?.("Unable to load map");
27
41
  }, onHttpError: () => {
28
42
  setHasError(true);
43
+ trackMapError({
44
+ message: "Map HTTP error",
45
+ buildingId: mall?.buildingId,
46
+ });
29
47
  onError?.("Map HTTP error");
30
- }, onShouldStartLoadWithRequest: (request) => onNavigationRequest(request) }, `${url}-${reloadKey}`), hasError && (_jsxs(View, { style: styles.overlay, children: [_jsx(Text, { style: styles.errorText, children: "Unable to load map" }), _jsx(Pressable, { style: styles.retryButton, onPress: handleRetry, children: _jsx(Text, { style: styles.retryText, children: "Retry" }) })] }))] }));
48
+ }, onShouldStartLoadWithRequest: (request) => onNavigationRequest(request) }, `${resolvedUrl}-${reloadKey}`), hasError && (_jsxs(View, { style: styles.overlay, children: [_jsx(Text, { style: styles.errorText, children: "Unable to load map" }), _jsx(Pressable, { style: styles.retryButton, onPress: handleRetry, children: _jsx(Text, { style: styles.retryText, children: "Retry" }) })] }))] }));
31
49
  }
32
50
  const styles = StyleSheet.create({
33
51
  container: {
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { type ViewStyle } from "react-native";
3
+ import { type DuonMall } from "@dtechph/wayfinding-core";
4
+ export interface DuonSitumMapViewProps {
5
+ mall: DuonMall;
6
+ style?: ViewStyle;
7
+ onLoadStart?: () => void;
8
+ onLoadEnd?: () => void;
9
+ onError?: (message: string) => void;
10
+ }
11
+ /**
12
+ * Native Situm map with automatic analytics (location, geofence, POI, search).
13
+ * Requires host dependency: `@situm/react-native`.
14
+ */
15
+ export declare function DuonSitumMapView({ mall, style, onLoadStart, onLoadEnd, onError, }: DuonSitumMapViewProps): React.JSX.Element;
@@ -0,0 +1,155 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { StyleSheet, Text, View } from "react-native";
4
+ import { trackGeofenceEnter, trackGeofenceExit, trackLocation, trackMapError, trackMapLoad, trackPoiSelect, trackSearch, } from "@dtechph/wayfinding-core";
5
+ function loadSitum() {
6
+ try {
7
+ // Optional peer — resolved at runtime in host apps that install @situm/react-native
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ return require("@situm/react-native");
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ /**
16
+ * Native Situm map with automatic analytics (location, geofence, POI, search).
17
+ * Requires host dependency: `@situm/react-native`.
18
+ */
19
+ export function DuonSitumMapView({ mall, style, onLoadStart, onLoadEnd, onError, }) {
20
+ const situm = loadSitum();
21
+ const buildingId = mall.situmBuildingId || mall.buildingId;
22
+ const apiKey = mall.situmApiKey;
23
+ const startedRef = useRef(false);
24
+ useEffect(() => {
25
+ if (!situm || !apiKey)
26
+ return;
27
+ const plugin = situm.default;
28
+ if (!plugin)
29
+ return;
30
+ onLoadStart?.();
31
+ try {
32
+ plugin.init?.();
33
+ plugin.enableUserHelper?.();
34
+ }
35
+ catch {
36
+ // init may already have run
37
+ }
38
+ plugin.onLocationUpdate?.((location) => {
39
+ const cartesian = (location.cartesianCoordinate ||
40
+ location.position ||
41
+ {});
42
+ const coord = (location.coordinate || {});
43
+ trackLocation({
44
+ x: Number(cartesian.x ?? location.x),
45
+ y: Number(cartesian.y ?? location.y),
46
+ lat: Number(coord.latitude ?? location.latitude),
47
+ lng: Number(coord.longitude ?? location.longitude),
48
+ accuracy: Number(location.accuracy),
49
+ floorId: String(location.floorIdentifier ?? location.floorId ?? "") || undefined,
50
+ buildingId: String(location.buildingIdentifier ?? buildingId),
51
+ });
52
+ });
53
+ plugin.onEnterGeofences?.((result) => {
54
+ for (const g of result.geofences || []) {
55
+ trackGeofenceEnter({
56
+ geofenceId: String(g.identifier ?? g.id ?? ""),
57
+ geofenceName: g.name != null ? String(g.name) : undefined,
58
+ buildingId: String(g.buildingIdentifier ?? buildingId),
59
+ floorId: g.floorIdentifier != null ? String(g.floorIdentifier) : undefined,
60
+ code: g.code != null ? String(g.code) : undefined,
61
+ });
62
+ }
63
+ });
64
+ plugin.onExitGeofences?.((result) => {
65
+ for (const g of result.geofences || []) {
66
+ trackGeofenceExit({
67
+ geofenceId: String(g.identifier ?? g.id ?? ""),
68
+ geofenceName: g.name != null ? String(g.name) : undefined,
69
+ buildingId: String(g.buildingIdentifier ?? buildingId),
70
+ floorId: g.floorIdentifier != null ? String(g.floorIdentifier) : undefined,
71
+ code: g.code != null ? String(g.code) : undefined,
72
+ });
73
+ }
74
+ });
75
+ if (!startedRef.current) {
76
+ startedRef.current = true;
77
+ try {
78
+ plugin.requestLocationUpdates?.({});
79
+ }
80
+ catch {
81
+ // host may manage positioning
82
+ }
83
+ }
84
+ return () => {
85
+ try {
86
+ plugin.removeLocationUpdates?.();
87
+ }
88
+ catch {
89
+ // ignore
90
+ }
91
+ startedRef.current = false;
92
+ };
93
+ }, [situm, apiKey, buildingId, onLoadStart]);
94
+ if (!apiKey) {
95
+ return (_jsx(View, { style: [styles.center, style], children: _jsx(Text, { style: styles.errorText, children: "Situm credentials missing for this mall" }) }));
96
+ }
97
+ if (!situm?.MapView) {
98
+ return (_jsx(View, { style: [styles.center, style], children: _jsx(Text, { style: styles.errorText, children: "Install @situm/react-native to enable native Situm maps" }) }));
99
+ }
100
+ const MapView = situm.MapView;
101
+ const Provider = situm.SitumProvider;
102
+ const map = (_jsx(MapView, { style: StyleSheet.flatten([styles.map, style]), configuration: {
103
+ buildingIdentifier: buildingId,
104
+ situmApiKey: apiKey,
105
+ }, onLoad: () => {
106
+ trackMapLoad({ buildingId, mapType: "situm" });
107
+ onLoadEnd?.();
108
+ }, onPoiSelected: (poi) => {
109
+ trackPoiSelect({
110
+ poiId: String(poi.identifier ?? ""),
111
+ poiName: poi.name,
112
+ buildingId,
113
+ floorId: poi.floorIdentifier,
114
+ });
115
+ // Treat POI selection as a search result interaction
116
+ if (poi.name) {
117
+ trackSearch({
118
+ query: poi.name,
119
+ resultType: "poi",
120
+ resultId: String(poi.identifier ?? ""),
121
+ resultName: poi.name,
122
+ buildingId,
123
+ floorId: poi.floorIdentifier,
124
+ });
125
+ }
126
+ } }));
127
+ try {
128
+ if (Provider) {
129
+ return _jsx(Provider, { apiKey: apiKey, children: map });
130
+ }
131
+ }
132
+ catch (err) {
133
+ trackMapError({
134
+ message: err instanceof Error ? err.message : "Situm map error",
135
+ buildingId,
136
+ });
137
+ onError?.(err instanceof Error ? err.message : "Situm map error");
138
+ }
139
+ return map;
140
+ }
141
+ const styles = StyleSheet.create({
142
+ map: { flex: 1 },
143
+ center: {
144
+ flex: 1,
145
+ alignItems: "center",
146
+ justifyContent: "center",
147
+ padding: 16,
148
+ },
149
+ errorText: {
150
+ fontSize: 15,
151
+ fontWeight: "600",
152
+ color: "#374151",
153
+ textAlign: "center",
154
+ },
155
+ });
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { DuonMapView } from "./DuonMapView";
2
2
  export type { DuonMapViewProps } from "./DuonMapView";
3
+ export { DuonSitumMapView } from "./DuonSitumMapView";
4
+ export type { DuonSitumMapViewProps } from "./DuonSitumMapView";
3
5
  export { DuonMallSelector } from "./DuonMallSelector";
4
6
  export type { DuonMallSelectorProps } from "./DuonMallSelector";
5
7
  export { isAllowedMapNavigationUrl } from "./navigationGuard";
6
- export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "@dtechph/wayfinding-core";
8
+ export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, initialize, fetchMalls, getViewerUrl, setActiveMall, flush, trackSearch, trackLocation, trackGeofenceEnter, trackGeofenceExit, trackPoiSelect, } from "@dtechph/wayfinding-core";
7
9
  export type { DuonMall, DuonWayfindingConfig } from "@dtechph/wayfinding-core";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { DuonMapView } from "./DuonMapView";
2
+ export { DuonSitumMapView } from "./DuonSitumMapView";
2
3
  export { DuonMallSelector } from "./DuonMallSelector";
3
4
  export { isAllowedMapNavigationUrl } from "./navigationGuard";
4
- export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "@dtechph/wayfinding-core";
5
+ export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, initialize, fetchMalls, getViewerUrl, setActiveMall, flush, trackSearch, trackLocation, trackGeofenceEnter, trackGeofenceExit, trackPoiSelect, } from "@dtechph/wayfinding-core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtechph/wayfinding-react-native",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Duon Wayfinding SDK — React Native / Expo components",
5
5
  "license": "UNLICENSED",
6
6
  "author": "dtechph",
@@ -52,10 +52,11 @@
52
52
  "node": ">=18"
53
53
  },
54
54
  "dependencies": {
55
- "@dtechph/wayfinding-core": "^0.1.0"
55
+ "@dtechph/wayfinding-core": "^0.2.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@react-native-picker/picker": ">=2",
59
+ "@situm/react-native": ">=3.15.0",
59
60
  "react": ">=18",
60
61
  "react-native": ">=0.74",
61
62
  "react-native-webview": ">=13"
@@ -64,6 +65,9 @@
64
65
  "@react-native-picker/picker": {
65
66
  "optional": true
66
67
  },
68
+ "@situm/react-native": {
69
+ "optional": true
70
+ },
67
71
  "react": {
68
72
  "optional": true
69
73
  },
@@ -9,7 +9,7 @@ import {
9
9
  View,
10
10
  type ViewStyle,
11
11
  } from "react-native";
12
- import type { DuonMall } from "@dtechph/wayfinding-core";
12
+ import { setActiveMall, type DuonMall } from "@dtechph/wayfinding-core";
13
13
 
14
14
  export interface DuonMallSelectorProps {
15
15
  malls: DuonMall[];
@@ -110,6 +110,7 @@ export function DuonMallSelector({
110
110
  <Pressable
111
111
  style={[styles.option, isSelected && styles.optionSelected]}
112
112
  onPress={() => {
113
+ setActiveMall(item);
113
114
  onSelect(item);
114
115
  setOpen(false);
115
116
  }}
@@ -7,10 +7,19 @@ import {
7
7
  type ViewStyle,
8
8
  } from "react-native";
9
9
  import { WebView, type WebViewNavigation } from "react-native-webview";
10
+ import {
11
+ trackMapError,
12
+ trackMapLoad,
13
+ type DuonMall,
14
+ } from "@dtechph/wayfinding-core";
15
+ import { DuonSitumMapView } from "./DuonSitumMapView";
10
16
  import { isAllowedMapNavigationUrl } from "./navigationGuard";
11
17
 
12
18
  export interface DuonMapViewProps {
13
- url: string;
19
+ /** Prefer passing mall — Situm malls use native map + auto analytics. */
20
+ mall?: DuonMall | null;
21
+ /** Legacy: kiosk / embed URL. Used when mall is kiosk or mall omitted. */
22
+ url?: string;
14
23
  style?: ViewStyle;
15
24
  onLoadStart?: () => void;
16
25
  onLoadEnd?: () => void;
@@ -18,6 +27,7 @@ export interface DuonMapViewProps {
18
27
  }
19
28
 
20
29
  export function DuonMapView({
30
+ mall,
21
31
  url,
22
32
  style,
23
33
  onLoadStart,
@@ -36,7 +46,21 @@ export function DuonMapView({
36
46
  return isAllowedMapNavigationUrl(request.url);
37
47
  }, []);
38
48
 
39
- if (!url) {
49
+ if (mall?.mapType === "situm" && mall.situmApiKey) {
50
+ return (
51
+ <DuonSitumMapView
52
+ mall={mall}
53
+ style={style}
54
+ onLoadStart={onLoadStart}
55
+ onLoadEnd={onLoadEnd}
56
+ onError={onError}
57
+ />
58
+ );
59
+ }
60
+
61
+ const resolvedUrl = url || mall?.viewerUrl || "";
62
+
63
+ if (!resolvedUrl) {
40
64
  return (
41
65
  <View style={[styles.center, style]}>
42
66
  <Text style={styles.errorText}>No map URL provided</Text>
@@ -47,8 +71,8 @@ export function DuonMapView({
47
71
  return (
48
72
  <View style={[styles.container, style]}>
49
73
  <WebView
50
- key={`${url}-${reloadKey}`}
51
- source={{ uri: url }}
74
+ key={`${resolvedUrl}-${reloadKey}`}
75
+ source={{ uri: resolvedUrl }}
52
76
  style={styles.webview}
53
77
  javaScriptEnabled
54
78
  domStorageEnabled
@@ -57,14 +81,26 @@ export function DuonMapView({
57
81
  onLoadStart?.();
58
82
  }}
59
83
  onLoadEnd={() => {
84
+ trackMapLoad({
85
+ buildingId: mall?.buildingId,
86
+ mapType: mall?.mapType || "kiosk",
87
+ });
60
88
  onLoadEnd?.();
61
89
  }}
62
90
  onError={() => {
63
91
  setHasError(true);
92
+ trackMapError({
93
+ message: "Unable to load map",
94
+ buildingId: mall?.buildingId,
95
+ });
64
96
  onError?.("Unable to load map");
65
97
  }}
66
98
  onHttpError={() => {
67
99
  setHasError(true);
100
+ trackMapError({
101
+ message: "Map HTTP error",
102
+ buildingId: mall?.buildingId,
103
+ });
68
104
  onError?.("Map HTTP error");
69
105
  }}
70
106
  onShouldStartLoadWithRequest={(request) => onNavigationRequest(request)}
@@ -0,0 +1,236 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { StyleSheet, Text, View, type ViewStyle } from "react-native";
3
+ import {
4
+ trackGeofenceEnter,
5
+ trackGeofenceExit,
6
+ trackLocation,
7
+ trackMapError,
8
+ trackMapLoad,
9
+ trackPoiSelect,
10
+ trackSearch,
11
+ type DuonMall,
12
+ } from "@dtechph/wayfinding-core";
13
+
14
+ export interface DuonSitumMapViewProps {
15
+ mall: DuonMall;
16
+ style?: ViewStyle;
17
+ onLoadStart?: () => void;
18
+ onLoadEnd?: () => void;
19
+ onError?: (message: string) => void;
20
+ }
21
+
22
+ type SitumModule = {
23
+ default?: {
24
+ init?: () => void;
25
+ requestLocationUpdates?: (opts?: object) => void;
26
+ removeLocationUpdates?: () => void;
27
+ onLocationUpdate?: (cb: (location: Record<string, unknown>) => void) => void;
28
+ onEnterGeofences?: (cb: (result: { geofences?: Array<Record<string, unknown>> }) => void) => void;
29
+ onExitGeofences?: (cb: (result: { geofences?: Array<Record<string, unknown>> }) => void) => void;
30
+ enableUserHelper?: () => void;
31
+ };
32
+ MapView?: React.ComponentType<{
33
+ style?: ViewStyle;
34
+ configuration: {
35
+ buildingIdentifier: string;
36
+ situmApiKey: string;
37
+ };
38
+ onLoad?: () => void;
39
+ onPoiSelected?: (poi: { identifier?: string; name?: string; floorIdentifier?: string }) => void;
40
+ onFloorChanged?: (floor: { identifier?: string }) => void;
41
+ }>;
42
+ SitumProvider?: React.ComponentType<{
43
+ apiKey: string;
44
+ children: React.ReactNode;
45
+ }>;
46
+ };
47
+
48
+ function loadSitum(): SitumModule | null {
49
+ try {
50
+ // Optional peer — resolved at runtime in host apps that install @situm/react-native
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ return require("@situm/react-native") as SitumModule;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Native Situm map with automatic analytics (location, geofence, POI, search).
60
+ * Requires host dependency: `@situm/react-native`.
61
+ */
62
+ export function DuonSitumMapView({
63
+ mall,
64
+ style,
65
+ onLoadStart,
66
+ onLoadEnd,
67
+ onError,
68
+ }: DuonSitumMapViewProps) {
69
+ const situm = loadSitum();
70
+ const buildingId = mall.situmBuildingId || mall.buildingId;
71
+ const apiKey = mall.situmApiKey;
72
+ const startedRef = useRef(false);
73
+
74
+ useEffect(() => {
75
+ if (!situm || !apiKey) return;
76
+ const plugin = situm.default;
77
+ if (!plugin) return;
78
+
79
+ onLoadStart?.();
80
+ try {
81
+ plugin.init?.();
82
+ plugin.enableUserHelper?.();
83
+ } catch {
84
+ // init may already have run
85
+ }
86
+
87
+ plugin.onLocationUpdate?.((location) => {
88
+ const cartesian = (location.cartesianCoordinate ||
89
+ location.position ||
90
+ {}) as Record<string, unknown>;
91
+ const coord = (location.coordinate || {}) as Record<string, unknown>;
92
+ trackLocation({
93
+ x: Number(cartesian.x ?? location.x),
94
+ y: Number(cartesian.y ?? location.y),
95
+ lat: Number(coord.latitude ?? location.latitude),
96
+ lng: Number(coord.longitude ?? location.longitude),
97
+ accuracy: Number(location.accuracy),
98
+ floorId: String(
99
+ location.floorIdentifier ?? location.floorId ?? ""
100
+ ) || undefined,
101
+ buildingId: String(
102
+ location.buildingIdentifier ?? buildingId
103
+ ),
104
+ });
105
+ });
106
+
107
+ plugin.onEnterGeofences?.((result) => {
108
+ for (const g of result.geofences || []) {
109
+ trackGeofenceEnter({
110
+ geofenceId: String(g.identifier ?? g.id ?? ""),
111
+ geofenceName: g.name != null ? String(g.name) : undefined,
112
+ buildingId: String(g.buildingIdentifier ?? buildingId),
113
+ floorId:
114
+ g.floorIdentifier != null ? String(g.floorIdentifier) : undefined,
115
+ code: g.code != null ? String(g.code) : undefined,
116
+ });
117
+ }
118
+ });
119
+
120
+ plugin.onExitGeofences?.((result) => {
121
+ for (const g of result.geofences || []) {
122
+ trackGeofenceExit({
123
+ geofenceId: String(g.identifier ?? g.id ?? ""),
124
+ geofenceName: g.name != null ? String(g.name) : undefined,
125
+ buildingId: String(g.buildingIdentifier ?? buildingId),
126
+ floorId:
127
+ g.floorIdentifier != null ? String(g.floorIdentifier) : undefined,
128
+ code: g.code != null ? String(g.code) : undefined,
129
+ });
130
+ }
131
+ });
132
+
133
+ if (!startedRef.current) {
134
+ startedRef.current = true;
135
+ try {
136
+ plugin.requestLocationUpdates?.({});
137
+ } catch {
138
+ // host may manage positioning
139
+ }
140
+ }
141
+
142
+ return () => {
143
+ try {
144
+ plugin.removeLocationUpdates?.();
145
+ } catch {
146
+ // ignore
147
+ }
148
+ startedRef.current = false;
149
+ };
150
+ }, [situm, apiKey, buildingId, onLoadStart]);
151
+
152
+ if (!apiKey) {
153
+ return (
154
+ <View style={[styles.center, style]}>
155
+ <Text style={styles.errorText}>
156
+ Situm credentials missing for this mall
157
+ </Text>
158
+ </View>
159
+ );
160
+ }
161
+
162
+ if (!situm?.MapView) {
163
+ return (
164
+ <View style={[styles.center, style]}>
165
+ <Text style={styles.errorText}>
166
+ Install @situm/react-native to enable native Situm maps
167
+ </Text>
168
+ </View>
169
+ );
170
+ }
171
+
172
+ const MapView = situm.MapView;
173
+ const Provider = situm.SitumProvider;
174
+ const map = (
175
+ <MapView
176
+ style={StyleSheet.flatten([styles.map, style])}
177
+ configuration={{
178
+ buildingIdentifier: buildingId,
179
+ situmApiKey: apiKey,
180
+ }}
181
+ onLoad={() => {
182
+ trackMapLoad({ buildingId, mapType: "situm" });
183
+ onLoadEnd?.();
184
+ }}
185
+ onPoiSelected={(poi) => {
186
+ trackPoiSelect({
187
+ poiId: String(poi.identifier ?? ""),
188
+ poiName: poi.name,
189
+ buildingId,
190
+ floorId: poi.floorIdentifier,
191
+ });
192
+ // Treat POI selection as a search result interaction
193
+ if (poi.name) {
194
+ trackSearch({
195
+ query: poi.name,
196
+ resultType: "poi",
197
+ resultId: String(poi.identifier ?? ""),
198
+ resultName: poi.name,
199
+ buildingId,
200
+ floorId: poi.floorIdentifier,
201
+ });
202
+ }
203
+ }}
204
+ />
205
+ );
206
+
207
+ try {
208
+ if (Provider) {
209
+ return <Provider apiKey={apiKey}>{map}</Provider>;
210
+ }
211
+ } catch (err) {
212
+ trackMapError({
213
+ message: err instanceof Error ? err.message : "Situm map error",
214
+ buildingId,
215
+ });
216
+ onError?.(err instanceof Error ? err.message : "Situm map error");
217
+ }
218
+
219
+ return map;
220
+ }
221
+
222
+ const styles = StyleSheet.create({
223
+ map: { flex: 1 },
224
+ center: {
225
+ flex: 1,
226
+ alignItems: "center",
227
+ justifyContent: "center",
228
+ padding: 16,
229
+ },
230
+ errorText: {
231
+ fontSize: 15,
232
+ fontWeight: "600",
233
+ color: "#374151",
234
+ textAlign: "center",
235
+ },
236
+ });
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { DuonMapView } from "./DuonMapView";
2
2
  export type { DuonMapViewProps } from "./DuonMapView";
3
3
 
4
+ export { DuonSitumMapView } from "./DuonSitumMapView";
5
+ export type { DuonSitumMapViewProps } from "./DuonSitumMapView";
6
+
4
7
  export { DuonMallSelector } from "./DuonMallSelector";
5
8
  export type { DuonMallSelectorProps } from "./DuonMallSelector";
6
9
 
@@ -12,6 +15,16 @@ export {
12
15
  DuonConfigError,
13
16
  DuonForbiddenError,
14
17
  DuonNetworkError,
18
+ initialize,
19
+ fetchMalls,
20
+ getViewerUrl,
21
+ setActiveMall,
22
+ flush,
23
+ trackSearch,
24
+ trackLocation,
25
+ trackGeofenceEnter,
26
+ trackGeofenceExit,
27
+ trackPoiSelect,
15
28
  } from "@dtechph/wayfinding-core";
16
29
 
17
30
  export type { DuonMall, DuonWayfindingConfig } from "@dtechph/wayfinding-core";