@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.
- package/dist/DuonMallSelector.d.ts +1 -1
- package/dist/DuonMallSelector.js +2 -0
- package/dist/DuonMapView.d.ts +6 -2
- package/dist/DuonMapView.js +22 -4
- package/dist/DuonSitumMapView.d.ts +15 -0
- package/dist/DuonSitumMapView.js +155 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/package.json +6 -2
- package/src/DuonMallSelector.tsx +2 -1
- package/src/DuonMapView.tsx +40 -4
- package/src/DuonSitumMapView.tsx +236 -0
- package/src/index.ts +13 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ViewStyle } from "react-native";
|
|
3
|
-
import type
|
|
3
|
+
import { type DuonMall } from "@dtechph/wayfinding-core";
|
|
4
4
|
export interface DuonMallSelectorProps {
|
|
5
5
|
malls: DuonMall[];
|
|
6
6
|
selectedMall?: DuonMall | null;
|
package/dist/DuonMallSelector.js
CHANGED
|
@@ -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: [
|
package/dist/DuonMapView.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/DuonMapView.js
CHANGED
|
@@ -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 (
|
|
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:
|
|
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) }, `${
|
|
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.
|
|
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.
|
|
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
|
},
|
package/src/DuonMallSelector.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
View,
|
|
10
10
|
type ViewStyle,
|
|
11
11
|
} from "react-native";
|
|
12
|
-
import type
|
|
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
|
}}
|
package/src/DuonMapView.tsx
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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={`${
|
|
51
|
-
source={{ uri:
|
|
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";
|