@dtechph/wayfinding-react-native 0.1.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/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # `@dtechph/wayfinding-react-native`
2
+
3
+ React Native / Expo components for the Duon Wayfinding SDK (`DuonMapView`, `DuonMallSelector`).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @dtechph/wayfinding-react-native react-native-webview
9
+ ```
10
+
11
+ Requires GitHub Packages auth — see the [root README](../../README.md).
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import {
17
+ DuonWayfinding,
18
+ DuonMallSelector,
19
+ DuonMapView,
20
+ } from "@dtechph/wayfinding-react-native";
21
+
22
+ DuonWayfinding.initialize({
23
+ apiBaseUrl: "https://your-backend.example.com",
24
+ apiKey: "duon_sk_live_...",
25
+ });
26
+
27
+ const malls = await DuonWayfinding.fetchMalls();
28
+ const url = DuonWayfinding.getViewerUrl(malls[0]);
29
+
30
+ // <DuonMallSelector malls={malls} onSelect={...} />
31
+ // <DuonMapView url={url} />
32
+ ```
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import { type ViewStyle } from "react-native";
3
+ import type { DuonMall } from "@dtechph/wayfinding-core";
4
+ export interface DuonMallSelectorProps {
5
+ malls: DuonMall[];
6
+ selectedMall?: DuonMall | null;
7
+ onSelect: (mall: DuonMall) => void;
8
+ loading?: boolean;
9
+ error?: string | null;
10
+ onRetry?: () => void;
11
+ style?: ViewStyle;
12
+ }
13
+ export declare function DuonMallSelector({ malls, selectedMall, onSelect, loading, error, onRetry, style, }: DuonMallSelectorProps): React.JSX.Element;
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { ActivityIndicator, FlatList, Modal, Pressable, StyleSheet, Text, View, } from "react-native";
4
+ function mallLabel(mall) {
5
+ return `${mall.name} (${mall.mapType.toUpperCase()})`;
6
+ }
7
+ export function DuonMallSelector({ malls, selectedMall, onSelect, loading = false, error = null, onRetry, style, }) {
8
+ const [open, setOpen] = useState(false);
9
+ const selected = useMemo(() => {
10
+ if (selectedMall)
11
+ return selectedMall;
12
+ return malls[0] ?? null;
13
+ }, [malls, selectedMall]);
14
+ if (loading) {
15
+ return (_jsxs(View, { style: [styles.container, styles.center, style], children: [_jsx(ActivityIndicator, { color: "#4f46e5" }), _jsx(Text, { style: styles.hint, children: "Loading malls\u2026" })] }));
16
+ }
17
+ if (error) {
18
+ return (_jsxs(View, { style: [styles.container, styles.center, style], children: [_jsx(Text, { style: styles.error, children: error }), onRetry && (_jsx(Pressable, { style: styles.retryButton, onPress: onRetry, children: _jsx(Text, { style: styles.retryText, children: "Retry" }) }))] }));
19
+ }
20
+ if (malls.length === 0) {
21
+ return (_jsx(View, { style: [styles.container, styles.center, style], children: _jsx(Text, { style: styles.hint, children: "No malls available" }) }));
22
+ }
23
+ 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
+ const isSelected = item.buildingId === selected?.buildingId;
25
+ return (_jsx(Pressable, { style: [styles.option, isSelected && styles.optionSelected], onPress: () => {
26
+ onSelect(item);
27
+ setOpen(false);
28
+ }, children: _jsx(Text, { style: [
29
+ styles.optionText,
30
+ isSelected && styles.optionTextSelected,
31
+ ], numberOfLines: 2, children: mallLabel(item) }) }));
32
+ } })] })] }) })] }));
33
+ }
34
+ const styles = StyleSheet.create({
35
+ container: {
36
+ paddingHorizontal: 16,
37
+ paddingVertical: 8,
38
+ },
39
+ center: {
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ minHeight: 56,
43
+ gap: 8,
44
+ },
45
+ label: {
46
+ fontSize: 13,
47
+ fontWeight: "600",
48
+ color: "#374151",
49
+ marginBottom: 6,
50
+ },
51
+ trigger: {
52
+ minHeight: 44,
53
+ borderWidth: 1,
54
+ borderColor: "#e5e7eb",
55
+ borderRadius: 10,
56
+ backgroundColor: "#fff",
57
+ paddingHorizontal: 12,
58
+ flexDirection: "row",
59
+ alignItems: "center",
60
+ justifyContent: "space-between",
61
+ gap: 8,
62
+ },
63
+ triggerText: {
64
+ flex: 1,
65
+ fontSize: 15,
66
+ color: "#111827",
67
+ },
68
+ chevron: {
69
+ fontSize: 16,
70
+ color: "#4f46e5",
71
+ lineHeight: 18,
72
+ },
73
+ backdrop: {
74
+ flex: 1,
75
+ backgroundColor: "rgba(17, 24, 39, 0.45)",
76
+ justifyContent: "center",
77
+ paddingHorizontal: 24,
78
+ },
79
+ sheet: {
80
+ maxHeight: "60%",
81
+ backgroundColor: "#fff",
82
+ borderRadius: 14,
83
+ overflow: "hidden",
84
+ paddingVertical: 8,
85
+ },
86
+ sheetTitle: {
87
+ fontSize: 13,
88
+ fontWeight: "600",
89
+ color: "#6b7280",
90
+ paddingHorizontal: 16,
91
+ paddingTop: 8,
92
+ paddingBottom: 10,
93
+ },
94
+ option: {
95
+ paddingHorizontal: 16,
96
+ paddingVertical: 14,
97
+ },
98
+ optionSelected: {
99
+ backgroundColor: "#eef2ff",
100
+ },
101
+ optionText: {
102
+ fontSize: 15,
103
+ color: "#111827",
104
+ },
105
+ optionTextSelected: {
106
+ color: "#4338ca",
107
+ fontWeight: "600",
108
+ },
109
+ hint: {
110
+ fontSize: 14,
111
+ color: "#6b7280",
112
+ },
113
+ error: {
114
+ fontSize: 14,
115
+ color: "#b91c1c",
116
+ textAlign: "center",
117
+ },
118
+ retryButton: {
119
+ paddingHorizontal: 16,
120
+ paddingVertical: 8,
121
+ backgroundColor: "#4f46e5",
122
+ borderRadius: 8,
123
+ },
124
+ retryText: {
125
+ color: "#fff",
126
+ fontWeight: "600",
127
+ },
128
+ });
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { type ViewStyle } from "react-native";
3
+ export interface DuonMapViewProps {
4
+ url: string;
5
+ style?: ViewStyle;
6
+ onLoadStart?: () => void;
7
+ onLoadEnd?: () => void;
8
+ onError?: (message: string) => void;
9
+ }
10
+ export declare function DuonMapView({ url, style, onLoadStart, onLoadEnd, onError, }: DuonMapViewProps): React.JSX.Element;
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
3
+ import { Pressable, StyleSheet, Text, View, } from "react-native";
4
+ import { WebView } from "react-native-webview";
5
+ import { isAllowedMapNavigationUrl } from "./navigationGuard";
6
+ export function DuonMapView({ url, style, onLoadStart, onLoadEnd, onError, }) {
7
+ const [hasError, setHasError] = useState(false);
8
+ const [reloadKey, setReloadKey] = useState(0);
9
+ const handleRetry = useCallback(() => {
10
+ setHasError(false);
11
+ setReloadKey((k) => k + 1);
12
+ }, []);
13
+ const onNavigationRequest = useCallback((request) => {
14
+ return isAllowedMapNavigationUrl(request.url);
15
+ }, []);
16
+ if (!url) {
17
+ return (_jsx(View, { style: [styles.center, style], children: _jsx(Text, { style: styles.errorText, children: "No map URL provided" }) }));
18
+ }
19
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsx(WebView, { source: { uri: url }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, onLoadStart: () => {
20
+ setHasError(false);
21
+ onLoadStart?.();
22
+ }, onLoadEnd: () => {
23
+ onLoadEnd?.();
24
+ }, onError: () => {
25
+ setHasError(true);
26
+ onError?.("Unable to load map");
27
+ }, onHttpError: () => {
28
+ setHasError(true);
29
+ 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" }) })] }))] }));
31
+ }
32
+ const styles = StyleSheet.create({
33
+ container: {
34
+ flex: 1,
35
+ backgroundColor: "#f3f4f6",
36
+ },
37
+ webview: {
38
+ flex: 1,
39
+ },
40
+ center: {
41
+ flex: 1,
42
+ alignItems: "center",
43
+ justifyContent: "center",
44
+ },
45
+ overlay: {
46
+ position: "absolute",
47
+ top: 0,
48
+ left: 0,
49
+ right: 0,
50
+ bottom: 0,
51
+ alignItems: "center",
52
+ justifyContent: "center",
53
+ backgroundColor: "rgba(255,255,255,0.92)",
54
+ gap: 12,
55
+ },
56
+ errorText: {
57
+ fontSize: 16,
58
+ fontWeight: "600",
59
+ color: "#374151",
60
+ },
61
+ retryButton: {
62
+ paddingHorizontal: 20,
63
+ paddingVertical: 10,
64
+ backgroundColor: "#4f46e5",
65
+ borderRadius: 8,
66
+ },
67
+ retryText: {
68
+ color: "#fff",
69
+ fontWeight: "600",
70
+ },
71
+ });
@@ -0,0 +1,7 @@
1
+ export { DuonMapView } from "./DuonMapView";
2
+ export type { DuonMapViewProps } from "./DuonMapView";
3
+ export { DuonMallSelector } from "./DuonMallSelector";
4
+ export type { DuonMallSelectorProps } from "./DuonMallSelector";
5
+ export { isAllowedMapNavigationUrl } from "./navigationGuard";
6
+ export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "@dtechph/wayfinding-core";
7
+ export type { DuonMall, DuonWayfindingConfig } from "@dtechph/wayfinding-core";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { DuonMapView } from "./DuonMapView";
2
+ export { DuonMallSelector } from "./DuonMallSelector";
3
+ export { isAllowedMapNavigationUrl } from "./navigationGuard";
4
+ export { DuonWayfinding, DuonAuthError, DuonConfigError, DuonForbiddenError, DuonNetworkError, } from "@dtechph/wayfinding-core";
@@ -0,0 +1 @@
1
+ export declare const isAllowedMapNavigationUrl: (url: string) => boolean;
@@ -0,0 +1,19 @@
1
+ const ALLOWED_HOSTS = [
2
+ "kiosk.duon.ph",
3
+ "maps.situm.com",
4
+ "api.situm.com",
5
+ ];
6
+ export const isAllowedMapNavigationUrl = (url) => {
7
+ try {
8
+ const uri = new URL(url);
9
+ if (uri.protocol === "about:")
10
+ return true;
11
+ if (uri.hostname === "kiosk.duon.ph" || uri.hostname.endsWith(".duon.ph")) {
12
+ return true;
13
+ }
14
+ return ALLOWED_HOSTS.some((host) => uri.hostname === host || uri.hostname.endsWith(`.${host}`));
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@dtechph/wayfinding-react-native",
3
+ "version": "0.1.0",
4
+ "description": "Duon Wayfinding SDK — React Native / Expo components",
5
+ "license": "UNLICENSED",
6
+ "author": "dtechph",
7
+ "homepage": "https://github.com/dtechph/DuonSDK/tree/main/packages/react-native#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/dtechph/DuonSDK/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/dtechph/DuonSDK.git",
14
+ "directory": "packages/react-native"
15
+ },
16
+ "keywords": [
17
+ "duon",
18
+ "wayfinding",
19
+ "react-native",
20
+ "expo",
21
+ "indoor-maps",
22
+ "situm",
23
+ "sdk"
24
+ ],
25
+ "main": "dist/index.js",
26
+ "module": "dist/index.js",
27
+ "react-native": "src/index.ts",
28
+ "types": "dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "react-native": "./src/index.ts",
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "README.md"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "publishConfig": {
48
+ "registry": "https://registry.npmjs.org",
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ },
54
+ "dependencies": {
55
+ "@dtechph/wayfinding-core": "^0.1.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@react-native-picker/picker": ">=2",
59
+ "react": ">=18",
60
+ "react-native": ">=0.74",
61
+ "react-native-webview": ">=13"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@react-native-picker/picker": {
65
+ "optional": true
66
+ },
67
+ "react": {
68
+ "optional": true
69
+ },
70
+ "react-native": {
71
+ "optional": true
72
+ },
73
+ "react-native-webview": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "@react-native-picker/picker": "2.11.4",
79
+ "@types/react": "~19.2.2",
80
+ "react": "19.2.3",
81
+ "react-native": "0.86.0",
82
+ "react-native-webview": "13.16.1",
83
+ "typescript": "~5.9.2"
84
+ }
85
+ }
@@ -0,0 +1,231 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ FlatList,
5
+ Modal,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ type ViewStyle,
11
+ } from "react-native";
12
+ import type { DuonMall } from "@dtechph/wayfinding-core";
13
+
14
+ export interface DuonMallSelectorProps {
15
+ malls: DuonMall[];
16
+ selectedMall?: DuonMall | null;
17
+ onSelect: (mall: DuonMall) => void;
18
+ loading?: boolean;
19
+ error?: string | null;
20
+ onRetry?: () => void;
21
+ style?: ViewStyle;
22
+ }
23
+
24
+ function mallLabel(mall: DuonMall): string {
25
+ return `${mall.name} (${mall.mapType.toUpperCase()})`;
26
+ }
27
+
28
+ export function DuonMallSelector({
29
+ malls,
30
+ selectedMall,
31
+ onSelect,
32
+ loading = false,
33
+ error = null,
34
+ onRetry,
35
+ style,
36
+ }: DuonMallSelectorProps) {
37
+ const [open, setOpen] = useState(false);
38
+
39
+ const selected = useMemo(() => {
40
+ if (selectedMall) return selectedMall;
41
+ return malls[0] ?? null;
42
+ }, [malls, selectedMall]);
43
+
44
+ if (loading) {
45
+ return (
46
+ <View style={[styles.container, styles.center, style]}>
47
+ <ActivityIndicator color="#4f46e5" />
48
+ <Text style={styles.hint}>Loading malls…</Text>
49
+ </View>
50
+ );
51
+ }
52
+
53
+ if (error) {
54
+ return (
55
+ <View style={[styles.container, styles.center, style]}>
56
+ <Text style={styles.error}>{error}</Text>
57
+ {onRetry && (
58
+ <Pressable style={styles.retryButton} onPress={onRetry}>
59
+ <Text style={styles.retryText}>Retry</Text>
60
+ </Pressable>
61
+ )}
62
+ </View>
63
+ );
64
+ }
65
+
66
+ if (malls.length === 0) {
67
+ return (
68
+ <View style={[styles.container, styles.center, style]}>
69
+ <Text style={styles.hint}>No malls available</Text>
70
+ </View>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <View style={[styles.container, style]}>
76
+ <Text style={styles.label}>Select mall</Text>
77
+ <Pressable
78
+ accessibilityRole="button"
79
+ accessibilityState={{ expanded: open }}
80
+ style={styles.trigger}
81
+ onPress={() => setOpen(true)}
82
+ >
83
+ <Text style={styles.triggerText} numberOfLines={1}>
84
+ {selected ? mallLabel(selected) : "Select a mall"}
85
+ </Text>
86
+ <Text style={styles.chevron}>▾</Text>
87
+ </Pressable>
88
+
89
+ <Modal
90
+ visible={open}
91
+ transparent
92
+ animationType="fade"
93
+ onRequestClose={() => setOpen(false)}
94
+ >
95
+ <View style={styles.backdrop}>
96
+ <Pressable
97
+ style={StyleSheet.absoluteFill}
98
+ onPress={() => setOpen(false)}
99
+ accessibilityLabel="Dismiss mall selector"
100
+ />
101
+ <View style={styles.sheet}>
102
+ <Text style={styles.sheetTitle}>Select mall</Text>
103
+ <FlatList
104
+ data={malls}
105
+ keyExtractor={(mall) => `${mall.buildingId}-${mall.name}`}
106
+ keyboardShouldPersistTaps="handled"
107
+ renderItem={({ item }) => {
108
+ const isSelected = item.buildingId === selected?.buildingId;
109
+ return (
110
+ <Pressable
111
+ style={[styles.option, isSelected && styles.optionSelected]}
112
+ onPress={() => {
113
+ onSelect(item);
114
+ setOpen(false);
115
+ }}
116
+ >
117
+ <Text
118
+ style={[
119
+ styles.optionText,
120
+ isSelected && styles.optionTextSelected,
121
+ ]}
122
+ numberOfLines={2}
123
+ >
124
+ {mallLabel(item)}
125
+ </Text>
126
+ </Pressable>
127
+ );
128
+ }}
129
+ />
130
+ </View>
131
+ </View>
132
+ </Modal>
133
+ </View>
134
+ );
135
+ }
136
+
137
+ const styles = StyleSheet.create({
138
+ container: {
139
+ paddingHorizontal: 16,
140
+ paddingVertical: 8,
141
+ },
142
+ center: {
143
+ alignItems: "center",
144
+ justifyContent: "center",
145
+ minHeight: 56,
146
+ gap: 8,
147
+ },
148
+ label: {
149
+ fontSize: 13,
150
+ fontWeight: "600",
151
+ color: "#374151",
152
+ marginBottom: 6,
153
+ },
154
+ trigger: {
155
+ minHeight: 44,
156
+ borderWidth: 1,
157
+ borderColor: "#e5e7eb",
158
+ borderRadius: 10,
159
+ backgroundColor: "#fff",
160
+ paddingHorizontal: 12,
161
+ flexDirection: "row",
162
+ alignItems: "center",
163
+ justifyContent: "space-between",
164
+ gap: 8,
165
+ },
166
+ triggerText: {
167
+ flex: 1,
168
+ fontSize: 15,
169
+ color: "#111827",
170
+ },
171
+ chevron: {
172
+ fontSize: 16,
173
+ color: "#4f46e5",
174
+ lineHeight: 18,
175
+ },
176
+ backdrop: {
177
+ flex: 1,
178
+ backgroundColor: "rgba(17, 24, 39, 0.45)",
179
+ justifyContent: "center",
180
+ paddingHorizontal: 24,
181
+ },
182
+ sheet: {
183
+ maxHeight: "60%",
184
+ backgroundColor: "#fff",
185
+ borderRadius: 14,
186
+ overflow: "hidden",
187
+ paddingVertical: 8,
188
+ },
189
+ sheetTitle: {
190
+ fontSize: 13,
191
+ fontWeight: "600",
192
+ color: "#6b7280",
193
+ paddingHorizontal: 16,
194
+ paddingTop: 8,
195
+ paddingBottom: 10,
196
+ },
197
+ option: {
198
+ paddingHorizontal: 16,
199
+ paddingVertical: 14,
200
+ },
201
+ optionSelected: {
202
+ backgroundColor: "#eef2ff",
203
+ },
204
+ optionText: {
205
+ fontSize: 15,
206
+ color: "#111827",
207
+ },
208
+ optionTextSelected: {
209
+ color: "#4338ca",
210
+ fontWeight: "600",
211
+ },
212
+ hint: {
213
+ fontSize: 14,
214
+ color: "#6b7280",
215
+ },
216
+ error: {
217
+ fontSize: 14,
218
+ color: "#b91c1c",
219
+ textAlign: "center",
220
+ },
221
+ retryButton: {
222
+ paddingHorizontal: 16,
223
+ paddingVertical: 8,
224
+ backgroundColor: "#4f46e5",
225
+ borderRadius: 8,
226
+ },
227
+ retryText: {
228
+ color: "#fff",
229
+ fontWeight: "600",
230
+ },
231
+ });
@@ -0,0 +1,123 @@
1
+ import React, { useCallback, useState } from "react";
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ type ViewStyle,
8
+ } from "react-native";
9
+ import { WebView, type WebViewNavigation } from "react-native-webview";
10
+ import { isAllowedMapNavigationUrl } from "./navigationGuard";
11
+
12
+ export interface DuonMapViewProps {
13
+ url: string;
14
+ style?: ViewStyle;
15
+ onLoadStart?: () => void;
16
+ onLoadEnd?: () => void;
17
+ onError?: (message: string) => void;
18
+ }
19
+
20
+ export function DuonMapView({
21
+ url,
22
+ style,
23
+ onLoadStart,
24
+ onLoadEnd,
25
+ onError,
26
+ }: DuonMapViewProps) {
27
+ const [hasError, setHasError] = useState(false);
28
+ const [reloadKey, setReloadKey] = useState(0);
29
+
30
+ const handleRetry = useCallback(() => {
31
+ setHasError(false);
32
+ setReloadKey((k) => k + 1);
33
+ }, []);
34
+
35
+ const onNavigationRequest = useCallback((request: WebViewNavigation) => {
36
+ return isAllowedMapNavigationUrl(request.url);
37
+ }, []);
38
+
39
+ if (!url) {
40
+ return (
41
+ <View style={[styles.center, style]}>
42
+ <Text style={styles.errorText}>No map URL provided</Text>
43
+ </View>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <View style={[styles.container, style]}>
49
+ <WebView
50
+ key={`${url}-${reloadKey}`}
51
+ source={{ uri: url }}
52
+ style={styles.webview}
53
+ javaScriptEnabled
54
+ domStorageEnabled
55
+ onLoadStart={() => {
56
+ setHasError(false);
57
+ onLoadStart?.();
58
+ }}
59
+ onLoadEnd={() => {
60
+ onLoadEnd?.();
61
+ }}
62
+ onError={() => {
63
+ setHasError(true);
64
+ onError?.("Unable to load map");
65
+ }}
66
+ onHttpError={() => {
67
+ setHasError(true);
68
+ onError?.("Map HTTP error");
69
+ }}
70
+ onShouldStartLoadWithRequest={(request) => onNavigationRequest(request)}
71
+ />
72
+ {hasError && (
73
+ <View style={styles.overlay}>
74
+ <Text style={styles.errorText}>Unable to load map</Text>
75
+ <Pressable style={styles.retryButton} onPress={handleRetry}>
76
+ <Text style={styles.retryText}>Retry</Text>
77
+ </Pressable>
78
+ </View>
79
+ )}
80
+ </View>
81
+ );
82
+ }
83
+
84
+ const styles = StyleSheet.create({
85
+ container: {
86
+ flex: 1,
87
+ backgroundColor: "#f3f4f6",
88
+ },
89
+ webview: {
90
+ flex: 1,
91
+ },
92
+ center: {
93
+ flex: 1,
94
+ alignItems: "center",
95
+ justifyContent: "center",
96
+ },
97
+ overlay: {
98
+ position: "absolute",
99
+ top: 0,
100
+ left: 0,
101
+ right: 0,
102
+ bottom: 0,
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ backgroundColor: "rgba(255,255,255,0.92)",
106
+ gap: 12,
107
+ },
108
+ errorText: {
109
+ fontSize: 16,
110
+ fontWeight: "600",
111
+ color: "#374151",
112
+ },
113
+ retryButton: {
114
+ paddingHorizontal: 20,
115
+ paddingVertical: 10,
116
+ backgroundColor: "#4f46e5",
117
+ borderRadius: 8,
118
+ },
119
+ retryText: {
120
+ color: "#fff",
121
+ fontWeight: "600",
122
+ },
123
+ });
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { DuonMapView } from "./DuonMapView";
2
+ export type { DuonMapViewProps } from "./DuonMapView";
3
+
4
+ export { DuonMallSelector } from "./DuonMallSelector";
5
+ export type { DuonMallSelectorProps } from "./DuonMallSelector";
6
+
7
+ export { isAllowedMapNavigationUrl } from "./navigationGuard";
8
+
9
+ export {
10
+ DuonWayfinding,
11
+ DuonAuthError,
12
+ DuonConfigError,
13
+ DuonForbiddenError,
14
+ DuonNetworkError,
15
+ } from "@dtechph/wayfinding-core";
16
+
17
+ export type { DuonMall, DuonWayfindingConfig } from "@dtechph/wayfinding-core";
@@ -0,0 +1,18 @@
1
+ const ALLOWED_HOSTS = [
2
+ "kiosk.duon.ph",
3
+ "maps.situm.com",
4
+ "api.situm.com",
5
+ ];
6
+
7
+ export const isAllowedMapNavigationUrl = (url: string): boolean => {
8
+ try {
9
+ const uri = new URL(url);
10
+ if (uri.protocol === "about:") return true;
11
+ if (uri.hostname === "kiosk.duon.ph" || uri.hostname.endsWith(".duon.ph")) {
12
+ return true;
13
+ }
14
+ return ALLOWED_HOSTS.some((host) => uri.hostname === host || uri.hostname.endsWith(`.${host}`));
15
+ } catch {
16
+ return false;
17
+ }
18
+ };