@atomiqlab/react-native-mapbox-navigation 1.1.1 → 1.1.2
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 +368 -0
- package/SECURITY.md +36 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationActivity.kt +43 -0
- package/app.plugin.js +64 -4
- package/docs/TROUBLESHOOTING.md +22 -0
- package/ios/MapboxNavigationModule.swift +35 -1
- package/package.json +4 -2
- package/scripts/verify-release.mjs +24 -9
- package/src/MapboxNavigation.types.ts +47 -17
- package/src/index.tsx +113 -25
package/README.md
CHANGED
|
@@ -50,6 +50,16 @@ Regenerate native projects:
|
|
|
50
50
|
npx expo prebuild --clean
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Token validation behavior
|
|
54
|
+
|
|
55
|
+
The config plugin now fails fast during prebuild/build when tokens are missing or malformed:
|
|
56
|
+
|
|
57
|
+
- Missing/invalid public token (`pk...`)
|
|
58
|
+
- Missing/invalid downloads token (`sk...`)
|
|
59
|
+
|
|
60
|
+
This prevents silent runtime failures and surfaces setup issues early.
|
|
61
|
+
|
|
62
|
+
|
|
53
63
|
## Quick Start
|
|
54
64
|
|
|
55
65
|
```ts
|
|
@@ -107,6 +117,353 @@ import { MapboxNavigationView } from "@atomiqlab/react-native-mapbox-navigation"
|
|
|
107
117
|
/>;
|
|
108
118
|
```
|
|
109
119
|
|
|
120
|
+
```tsx
|
|
121
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
122
|
+
import {
|
|
123
|
+
addArriveListener,
|
|
124
|
+
addBannerInstructionListener,
|
|
125
|
+
addCancelNavigationListener,
|
|
126
|
+
addErrorListener,
|
|
127
|
+
addLocationChangeListener,
|
|
128
|
+
addRouteProgressChangeListener,
|
|
129
|
+
getNavigationSettings,
|
|
130
|
+
isNavigating,
|
|
131
|
+
MapboxNavigationView,
|
|
132
|
+
setDistanceUnit,
|
|
133
|
+
setLanguage,
|
|
134
|
+
setMuted,
|
|
135
|
+
setVoiceVolume,
|
|
136
|
+
startNavigation,
|
|
137
|
+
stopNavigation,
|
|
138
|
+
type Waypoint,
|
|
139
|
+
} from "@atomiqlab/react-native-mapbox-navigation";
|
|
140
|
+
import {
|
|
141
|
+
Platform,
|
|
142
|
+
Pressable,
|
|
143
|
+
SafeAreaView,
|
|
144
|
+
ScrollView,
|
|
145
|
+
StyleSheet,
|
|
146
|
+
Text,
|
|
147
|
+
TextInput,
|
|
148
|
+
View,
|
|
149
|
+
} from "react-native";
|
|
150
|
+
|
|
151
|
+
const START: Waypoint = {
|
|
152
|
+
latitude: 37.7749,
|
|
153
|
+
longitude: -122.4194,
|
|
154
|
+
name: "San Francisco",
|
|
155
|
+
};
|
|
156
|
+
const DEST: Waypoint = {
|
|
157
|
+
latitude: 37.7847,
|
|
158
|
+
longitude: -122.4073,
|
|
159
|
+
name: "Downtown",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export default function Index() {
|
|
163
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
164
|
+
const [navigating, setNavigating] = useState(false);
|
|
165
|
+
const [showEmbedded, setShowEmbedded] = useState(false);
|
|
166
|
+
|
|
167
|
+
const [mute, setMuteState] = useState(false);
|
|
168
|
+
const [unit, setUnit] = useState<"metric" | "imperial">("metric");
|
|
169
|
+
const [language, setLanguageState] = useState("en");
|
|
170
|
+
const [volumeInput, setVolumeInput] = useState("1");
|
|
171
|
+
|
|
172
|
+
const pushLog = (line: string) =>
|
|
173
|
+
setLogs((prev) =>
|
|
174
|
+
[`${new Date().toLocaleTimeString()} ${line}`, ...prev].slice(0, 40),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const s1 = addLocationChangeListener((e) =>
|
|
179
|
+
pushLog(`location: ${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`),
|
|
180
|
+
);
|
|
181
|
+
const s2 = addRouteProgressChangeListener((e) =>
|
|
182
|
+
pushLog(`progress: ${(e.fractionTraveled * 100).toFixed(1)}%`),
|
|
183
|
+
);
|
|
184
|
+
const s3 = addBannerInstructionListener((e) =>
|
|
185
|
+
pushLog(`banner: ${e.primaryText}`),
|
|
186
|
+
);
|
|
187
|
+
const s4 = addArriveListener((e) => {
|
|
188
|
+
pushLog(`arrive: ${e.name ?? "destination"}`);
|
|
189
|
+
setNavigating(false);
|
|
190
|
+
});
|
|
191
|
+
const s5 = addCancelNavigationListener(() => {
|
|
192
|
+
pushLog("cancelled");
|
|
193
|
+
setNavigating(false);
|
|
194
|
+
});
|
|
195
|
+
const s6 = addErrorListener((e) =>
|
|
196
|
+
pushLog(`error: ${e.message ?? e.code}`),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
(async () => {
|
|
200
|
+
try {
|
|
201
|
+
setNavigating(await isNavigating());
|
|
202
|
+
pushLog(`isNavigating() loaded`);
|
|
203
|
+
} catch (e: any) {
|
|
204
|
+
pushLog(`isNavigating failed: ${e?.message ?? "unknown"}`);
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
return () => {
|
|
209
|
+
s1.remove();
|
|
210
|
+
s2.remove();
|
|
211
|
+
s3.remove();
|
|
212
|
+
s4.remove();
|
|
213
|
+
s5.remove();
|
|
214
|
+
s6.remove();
|
|
215
|
+
};
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
const canRun = useMemo(
|
|
219
|
+
() => Platform.OS === "ios" || Platform.OS === "android",
|
|
220
|
+
[],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!canRun) {
|
|
224
|
+
return (
|
|
225
|
+
<SafeAreaView style={styles.center}>
|
|
226
|
+
<Text style={styles.title}>Run on iOS/Android only</Text>
|
|
227
|
+
</SafeAreaView>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<SafeAreaView style={styles.container}>
|
|
233
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
234
|
+
<Text style={styles.title}>Mapbox Navigation Full API Test</Text>
|
|
235
|
+
<Text style={styles.status}>
|
|
236
|
+
Status: {navigating ? "Navigating" : "Idle"}
|
|
237
|
+
</Text>
|
|
238
|
+
|
|
239
|
+
<View style={styles.row}>
|
|
240
|
+
<Pressable
|
|
241
|
+
style={styles.btn}
|
|
242
|
+
onPress={async () => {
|
|
243
|
+
try {
|
|
244
|
+
await startNavigation({
|
|
245
|
+
startOrigin: START,
|
|
246
|
+
destination: DEST,
|
|
247
|
+
shouldSimulateRoute: true,
|
|
248
|
+
routeAlternatives: true,
|
|
249
|
+
cameraMode: "following",
|
|
250
|
+
uiTheme: "system",
|
|
251
|
+
mute,
|
|
252
|
+
voiceVolume: Math.max(
|
|
253
|
+
0,
|
|
254
|
+
Math.min(Number(volumeInput) || 1, 1),
|
|
255
|
+
),
|
|
256
|
+
distanceUnit: unit,
|
|
257
|
+
language,
|
|
258
|
+
});
|
|
259
|
+
setNavigating(true);
|
|
260
|
+
pushLog("startNavigation() ok");
|
|
261
|
+
} catch (e: any) {
|
|
262
|
+
pushLog(`startNavigation failed: ${e?.message ?? "unknown"}`);
|
|
263
|
+
}
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<Text style={styles.btnText}>startNavigation</Text>
|
|
267
|
+
</Pressable>
|
|
268
|
+
|
|
269
|
+
<Pressable
|
|
270
|
+
style={styles.btn}
|
|
271
|
+
onPress={async () => {
|
|
272
|
+
try {
|
|
273
|
+
await stopNavigation();
|
|
274
|
+
setNavigating(false);
|
|
275
|
+
pushLog("stopNavigation() ok");
|
|
276
|
+
} catch (e: any) {
|
|
277
|
+
pushLog(`stopNavigation failed: ${e?.message ?? "unknown"}`);
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
<Text style={styles.btnText}>stopNavigation</Text>
|
|
282
|
+
</Pressable>
|
|
283
|
+
</View>
|
|
284
|
+
|
|
285
|
+
<View style={styles.row}>
|
|
286
|
+
<Pressable
|
|
287
|
+
style={styles.btn}
|
|
288
|
+
onPress={async () =>
|
|
289
|
+
pushLog(`isNavigating(): ${await isNavigating()}`)
|
|
290
|
+
}
|
|
291
|
+
>
|
|
292
|
+
<Text style={styles.btnText}>isNavigating</Text>
|
|
293
|
+
</Pressable>
|
|
294
|
+
|
|
295
|
+
<Pressable
|
|
296
|
+
style={styles.btn}
|
|
297
|
+
onPress={async () =>
|
|
298
|
+
pushLog(
|
|
299
|
+
`getNavigationSettings(): ${JSON.stringify(await getNavigationSettings())}`,
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
<Text style={styles.btnText}>getNavigationSettings</Text>
|
|
304
|
+
</Pressable>
|
|
305
|
+
</View>
|
|
306
|
+
|
|
307
|
+
<View style={styles.card}>
|
|
308
|
+
<Text style={styles.label}>mute / setMuted</Text>
|
|
309
|
+
<View style={styles.row}>
|
|
310
|
+
<Pressable
|
|
311
|
+
style={styles.btn}
|
|
312
|
+
onPress={async () => {
|
|
313
|
+
const next = !mute;
|
|
314
|
+
setMuteState(next);
|
|
315
|
+
await setMuted(next);
|
|
316
|
+
pushLog(`setMuted(${next})`);
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<Text style={styles.btnText}>{mute ? "Unmute" : "Mute"}</Text>
|
|
320
|
+
</Pressable>
|
|
321
|
+
</View>
|
|
322
|
+
|
|
323
|
+
<Text style={styles.label}>setVoiceVolume (0..1)</Text>
|
|
324
|
+
<View style={styles.row}>
|
|
325
|
+
<TextInput
|
|
326
|
+
style={styles.input}
|
|
327
|
+
value={volumeInput}
|
|
328
|
+
onChangeText={setVolumeInput}
|
|
329
|
+
keyboardType="decimal-pad"
|
|
330
|
+
/>
|
|
331
|
+
<Pressable
|
|
332
|
+
style={styles.btn}
|
|
333
|
+
onPress={async () => {
|
|
334
|
+
const vol = Math.max(0, Math.min(Number(volumeInput) || 1, 1));
|
|
335
|
+
await setVoiceVolume(vol);
|
|
336
|
+
pushLog(`setVoiceVolume(${vol})`);
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
<Text style={styles.btnText}>Apply</Text>
|
|
340
|
+
</Pressable>
|
|
341
|
+
</View>
|
|
342
|
+
|
|
343
|
+
<Text style={styles.label}>setDistanceUnit</Text>
|
|
344
|
+
<View style={styles.row}>
|
|
345
|
+
<Pressable
|
|
346
|
+
style={styles.btn}
|
|
347
|
+
onPress={async () => {
|
|
348
|
+
setUnit("metric");
|
|
349
|
+
await setDistanceUnit("metric");
|
|
350
|
+
pushLog("setDistanceUnit(metric)");
|
|
351
|
+
}}
|
|
352
|
+
>
|
|
353
|
+
<Text style={styles.btnText}>Metric</Text>
|
|
354
|
+
</Pressable>
|
|
355
|
+
<Pressable
|
|
356
|
+
style={styles.btn}
|
|
357
|
+
onPress={async () => {
|
|
358
|
+
setUnit("imperial");
|
|
359
|
+
await setDistanceUnit("imperial");
|
|
360
|
+
pushLog("setDistanceUnit(imperial)");
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<Text style={styles.btnText}>Imperial</Text>
|
|
364
|
+
</Pressable>
|
|
365
|
+
</View>
|
|
366
|
+
|
|
367
|
+
<Text style={styles.label}>setLanguage</Text>
|
|
368
|
+
<View style={styles.row}>
|
|
369
|
+
<TextInput
|
|
370
|
+
style={styles.input}
|
|
371
|
+
value={language}
|
|
372
|
+
onChangeText={setLanguageState}
|
|
373
|
+
/>
|
|
374
|
+
<Pressable
|
|
375
|
+
style={styles.btn}
|
|
376
|
+
onPress={async () => {
|
|
377
|
+
await setLanguage(language.trim() || "en");
|
|
378
|
+
pushLog(`setLanguage(${language.trim() || "en"})`);
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
<Text style={styles.btnText}>Apply</Text>
|
|
382
|
+
</Pressable>
|
|
383
|
+
</View>
|
|
384
|
+
</View>
|
|
385
|
+
|
|
386
|
+
<Pressable
|
|
387
|
+
style={styles.btn}
|
|
388
|
+
onPress={() => setShowEmbedded((v) => !v)}
|
|
389
|
+
>
|
|
390
|
+
<Text style={styles.btnText}>
|
|
391
|
+
{showEmbedded ? "Hide" : "Show"} Embedded MapboxNavigationView
|
|
392
|
+
</Text>
|
|
393
|
+
</Pressable>
|
|
394
|
+
|
|
395
|
+
{showEmbedded ? (
|
|
396
|
+
<View style={styles.embeddedWrap}>
|
|
397
|
+
<MapboxNavigationView
|
|
398
|
+
style={{ flex: 1 }}
|
|
399
|
+
destination={DEST}
|
|
400
|
+
startOrigin={START}
|
|
401
|
+
shouldSimulateRoute
|
|
402
|
+
cameraMode="overview"
|
|
403
|
+
uiTheme="system"
|
|
404
|
+
onError={(e) => pushLog(`embedded error: ${e.message ?? e.code}`)}
|
|
405
|
+
onBannerInstruction={(e) =>
|
|
406
|
+
pushLog(`embedded banner: ${e.primaryText}`)
|
|
407
|
+
}
|
|
408
|
+
onArrive={(e) =>
|
|
409
|
+
pushLog(`embedded arrive: ${e.name ?? "destination"}`)
|
|
410
|
+
}
|
|
411
|
+
onCancelNavigation={() => pushLog("embedded cancelled")}
|
|
412
|
+
/>
|
|
413
|
+
</View>
|
|
414
|
+
) : null}
|
|
415
|
+
|
|
416
|
+
<View style={styles.card}>
|
|
417
|
+
<Text style={styles.label}>Event Logs</Text>
|
|
418
|
+
{logs.map((l, i) => (
|
|
419
|
+
<Text key={`${l}-${i}`} style={styles.log}>
|
|
420
|
+
{l}
|
|
421
|
+
</Text>
|
|
422
|
+
))}
|
|
423
|
+
</View>
|
|
424
|
+
</ScrollView>
|
|
425
|
+
</SafeAreaView>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const styles = StyleSheet.create({
|
|
430
|
+
container: { flex: 1, backgroundColor: "#0b1020" },
|
|
431
|
+
center: {
|
|
432
|
+
flex: 1,
|
|
433
|
+
justifyContent: "center",
|
|
434
|
+
alignItems: "center",
|
|
435
|
+
backgroundColor: "#0b1020",
|
|
436
|
+
},
|
|
437
|
+
content: { padding: 14, gap: 10, paddingBottom: 28 },
|
|
438
|
+
title: { color: "#fff", fontSize: 22, fontWeight: "800" },
|
|
439
|
+
status: { color: "#8ec5ff", fontSize: 13 },
|
|
440
|
+
row: { flexDirection: "row", gap: 8, alignItems: "center" },
|
|
441
|
+
card: { backgroundColor: "#131a2d", borderRadius: 12, padding: 10, gap: 8 },
|
|
442
|
+
label: { color: "#cde4ff", fontSize: 12, fontWeight: "700" },
|
|
443
|
+
input: {
|
|
444
|
+
flex: 1,
|
|
445
|
+
backgroundColor: "#0e1426",
|
|
446
|
+
borderColor: "#2b3a5d",
|
|
447
|
+
borderWidth: 1,
|
|
448
|
+
borderRadius: 8,
|
|
449
|
+
color: "#fff",
|
|
450
|
+
paddingHorizontal: 10,
|
|
451
|
+
paddingVertical: 8,
|
|
452
|
+
},
|
|
453
|
+
btn: {
|
|
454
|
+
flex: 1,
|
|
455
|
+
backgroundColor: "#2563eb",
|
|
456
|
+
borderRadius: 10,
|
|
457
|
+
paddingVertical: 10,
|
|
458
|
+
paddingHorizontal: 10,
|
|
459
|
+
alignItems: "center",
|
|
460
|
+
},
|
|
461
|
+
btnText: { color: "#fff", fontWeight: "700", fontSize: 12 },
|
|
462
|
+
embeddedWrap: { height: 320, borderRadius: 12, overflow: "hidden" },
|
|
463
|
+
log: { color: "#dbeafe", fontSize: 11, marginBottom: 2 },
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
110
467
|
## API Overview
|
|
111
468
|
|
|
112
469
|
Core functions:
|
|
@@ -139,6 +496,17 @@ Main `NavigationOptions` fields:
|
|
|
139
496
|
|
|
140
497
|
Full types: `src/MapboxNavigation.types.ts`
|
|
141
498
|
|
|
499
|
+
## Common Error Codes
|
|
500
|
+
|
|
501
|
+
- `MAPBOX_TOKEN_INVALID`: invalid/expired token or unauthorized access
|
|
502
|
+
- `MAPBOX_TOKEN_FORBIDDEN`: token lacks required scopes/permissions
|
|
503
|
+
- `MAPBOX_RATE_LIMITED`: Mapbox rate limit reached
|
|
504
|
+
- `ROUTE_FETCH_FAILED`: route request failed with native details
|
|
505
|
+
- `CURRENT_LOCATION_UNAVAILABLE`: unable to resolve device location
|
|
506
|
+
- `INVALID_COORDINATES`: invalid origin/destination coordinates
|
|
507
|
+
|
|
508
|
+
Subscribe via `addErrorListener` or `onError` to surface these to developers during testing and production diagnostics.
|
|
509
|
+
|
|
142
510
|
## Platform Notes
|
|
143
511
|
|
|
144
512
|
- Android: `startOrigin` is optional (current location is supported).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Only the latest published minor version receives security updates.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
- Open a private security advisory on GitHub, or
|
|
10
|
+
- Email the maintainers listed in the repository profile.
|
|
11
|
+
|
|
12
|
+
Include:
|
|
13
|
+
- Package version
|
|
14
|
+
- Platform (iOS/Android)
|
|
15
|
+
- Reproduction steps
|
|
16
|
+
- Impact assessment
|
|
17
|
+
|
|
18
|
+
## Dependency Audit Policy
|
|
19
|
+
|
|
20
|
+
`npm audit` output in Expo/React Native apps can include transitive advisories from upstream tooling.
|
|
21
|
+
|
|
22
|
+
Project policy:
|
|
23
|
+
1. Prioritize vulnerabilities reachable in production runtime dependencies.
|
|
24
|
+
2. Track dev/build-only advisories from Expo/Metro/Jest and resolve when upstream ships patched versions.
|
|
25
|
+
3. Never run `npm audit fix --force` blindly in release branches.
|
|
26
|
+
4. Keep Expo SDK and React Native versions updated to receive upstream security fixes.
|
|
27
|
+
|
|
28
|
+
## Token Security
|
|
29
|
+
|
|
30
|
+
- Never commit Mapbox tokens to git.
|
|
31
|
+
- Use environment variables or CI/CD secrets.
|
|
32
|
+
- Rotate tokens immediately if leaked.
|
|
33
|
+
|
|
34
|
+
Required tokens:
|
|
35
|
+
- `EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN` (`pk...`)
|
|
36
|
+
- `MAPBOX_DOWNLOADS_TOKEN` (`sk...`, `DOWNLOADS:READ`)
|
|
@@ -112,6 +112,15 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
112
112
|
|
|
113
113
|
override fun onRouteFetchFailed(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
114
114
|
Log.e(TAG, "Route fetch failed. reasons=${reasons.size}, options=$routeOptions")
|
|
115
|
+
val (code, message) = mapRouteFetchFailure(reasons)
|
|
116
|
+
MapboxNavigationEventBridge.emit(
|
|
117
|
+
"onError",
|
|
118
|
+
mapOf(
|
|
119
|
+
"code" to code,
|
|
120
|
+
"message" to message
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
showErrorAndStay(message, null)
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
override fun onRouteFetchSuccessful(routes: List<NavigationRoute>) {
|
|
@@ -127,6 +136,13 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
127
136
|
|
|
128
137
|
override fun onRouteFetchCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
|
|
129
138
|
Log.w(TAG, "Route fetch canceled. origin=$routerOrigin, options=$routeOptions")
|
|
139
|
+
MapboxNavigationEventBridge.emit(
|
|
140
|
+
"onError",
|
|
141
|
+
mapOf(
|
|
142
|
+
"code" to "ROUTE_FETCH_CANCELED",
|
|
143
|
+
"message" to "Route fetch canceled (origin: $routerOrigin)."
|
|
144
|
+
)
|
|
145
|
+
)
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -301,6 +317,25 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
301
317
|
}
|
|
302
318
|
}
|
|
303
319
|
|
|
320
|
+
private fun mapRouteFetchFailure(reasons: List<RouterFailure>): Pair<String, String> {
|
|
321
|
+
val details = reasons.joinToString(" | ") { it.message.orEmpty() }.trim()
|
|
322
|
+
if (details.contains("401") || details.contains("unauthorized", ignoreCase = true)) {
|
|
323
|
+
return "MAPBOX_TOKEN_INVALID" to
|
|
324
|
+
"Route fetch failed: unauthorized. Check EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN and token scopes."
|
|
325
|
+
}
|
|
326
|
+
if (details.contains("403") || details.contains("forbidden", ignoreCase = true)) {
|
|
327
|
+
return "MAPBOX_TOKEN_FORBIDDEN" to
|
|
328
|
+
"Route fetch failed: access forbidden. Verify token scopes and account permissions."
|
|
329
|
+
}
|
|
330
|
+
if (details.contains("429") || details.contains("rate", ignoreCase = true)) {
|
|
331
|
+
return "MAPBOX_RATE_LIMITED" to
|
|
332
|
+
"Route fetch failed: rate limited by Mapbox."
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return "ROUTE_FETCH_FAILED" to
|
|
336
|
+
(if (details.isNotEmpty()) "Route fetch failed: $details" else "Route fetch failed for unknown reason.")
|
|
337
|
+
}
|
|
338
|
+
|
|
304
339
|
private fun hasLocationPermission(): Boolean {
|
|
305
340
|
val fineGranted = ContextCompat.checkSelfPermission(
|
|
306
341
|
this,
|
|
@@ -322,6 +357,14 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
322
357
|
Log.e(TAG, message)
|
|
323
358
|
}
|
|
324
359
|
|
|
360
|
+
MapboxNavigationEventBridge.emit(
|
|
361
|
+
"onError",
|
|
362
|
+
mapOf(
|
|
363
|
+
"code" to "NATIVE_ERROR",
|
|
364
|
+
"message" to message
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
325
368
|
val errorView = TextView(this).apply {
|
|
326
369
|
text = message
|
|
327
370
|
gravity = Gravity.CENTER
|
package/app.plugin.js
CHANGED
|
@@ -32,6 +32,64 @@ const REQUIRED_ANDROID_PERMISSIONS = [
|
|
|
32
32
|
const DEFAULT_IOS_LOCATION_USAGE =
|
|
33
33
|
"Allow $(PRODUCT_NAME) to access your location for turn-by-turn navigation.";
|
|
34
34
|
|
|
35
|
+
function resolveToken(config, envKeys = [], extraKeys = []) {
|
|
36
|
+
for (const key of envKeys) {
|
|
37
|
+
const value = process.env[key]?.trim();
|
|
38
|
+
if (value) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const extra = config?.extra ?? {};
|
|
44
|
+
for (const key of extraKeys) {
|
|
45
|
+
const value = typeof extra[key] === "string" ? extra[key].trim() : "";
|
|
46
|
+
if (value) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateMapboxTokenShape(token, expectedPrefix) {
|
|
55
|
+
return token.startsWith(expectedPrefix) && token.length > 20;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertRequiredTokens(config) {
|
|
59
|
+
const publicToken = resolveToken(
|
|
60
|
+
config,
|
|
61
|
+
["EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN", "MAPBOX_PUBLIC_TOKEN"],
|
|
62
|
+
["mapboxPublicToken", "expoPublicMapboxAccessToken", "mapboxAccessToken"]
|
|
63
|
+
);
|
|
64
|
+
const downloadsToken = resolveToken(
|
|
65
|
+
config,
|
|
66
|
+
["MAPBOX_DOWNLOADS_TOKEN"],
|
|
67
|
+
["mapboxDownloadsToken"]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!publicToken) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"[@atomiqlab/react-native-mapbox-navigation] Missing EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN (or MAPBOX_PUBLIC_TOKEN / expo.extra.mapboxPublicToken)."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!validateMapboxTokenShape(publicToken, "pk.")) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"[@atomiqlab/react-native-mapbox-navigation] Invalid public token format. Expected a Mapbox public token starting with 'pk.'."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!downloadsToken) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"[@atomiqlab/react-native-mapbox-navigation] Missing MAPBOX_DOWNLOADS_TOKEN (or expo.extra.mapboxDownloadsToken)."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!validateMapboxTokenShape(downloadsToken, "sk.")) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"[@atomiqlab/react-native-mapbox-navigation] Invalid downloads token format. Expected a Mapbox secret token starting with 'sk.' and DOWNLOADS:READ scope."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
35
93
|
function ensureAndroidPermissions(androidManifest) {
|
|
36
94
|
const manifest = androidManifest.manifest;
|
|
37
95
|
if (!manifest["uses-permission"]) {
|
|
@@ -114,10 +172,11 @@ function withMapboxNavigationAndroid(config) {
|
|
|
114
172
|
function withMapboxNavigationIos(config) {
|
|
115
173
|
return withInfoPlist(config, (config) => {
|
|
116
174
|
const infoPlist = config.modResults;
|
|
117
|
-
const mapboxPublicToken =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
""
|
|
175
|
+
const mapboxPublicToken = resolveToken(
|
|
176
|
+
config,
|
|
177
|
+
["EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN", "MAPBOX_PUBLIC_TOKEN"],
|
|
178
|
+
["mapboxPublicToken", "expoPublicMapboxAccessToken", "mapboxAccessToken"]
|
|
179
|
+
);
|
|
121
180
|
|
|
122
181
|
if (!infoPlist.MBXAccessToken && mapboxPublicToken) {
|
|
123
182
|
infoPlist.MBXAccessToken = mapboxPublicToken;
|
|
@@ -142,6 +201,7 @@ function withMapboxNavigationIos(config) {
|
|
|
142
201
|
}
|
|
143
202
|
|
|
144
203
|
const withMapboxNavigation = (config) => {
|
|
204
|
+
assertRequiredTokens(config);
|
|
145
205
|
config = withMapboxNavigationAndroid(config);
|
|
146
206
|
config = withMapboxNavigationIos(config);
|
|
147
207
|
return config;
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -33,3 +33,25 @@ If not, verify you are running the latest package version and rebuilt native cod
|
|
|
33
33
|
```bash
|
|
34
34
|
npx expo prebuild --clean
|
|
35
35
|
```
|
|
36
|
+
|
|
37
|
+
## Prebuild Fails With Missing Token Error
|
|
38
|
+
|
|
39
|
+
The config plugin now validates tokens early. Ensure one of these provides a public token:
|
|
40
|
+
|
|
41
|
+
- `EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN`
|
|
42
|
+
- `MAPBOX_PUBLIC_TOKEN`
|
|
43
|
+
- `expo.extra.mapboxPublicToken`
|
|
44
|
+
|
|
45
|
+
And provide downloads token:
|
|
46
|
+
|
|
47
|
+
- `MAPBOX_DOWNLOADS_TOKEN` or `expo.extra.mapboxDownloadsToken`
|
|
48
|
+
|
|
49
|
+
## Route Fetch Errors (401/403/429)
|
|
50
|
+
|
|
51
|
+
Listen to `onError` / `addErrorListener` and check `code`:
|
|
52
|
+
|
|
53
|
+
- `MAPBOX_TOKEN_INVALID` (invalid/expired token)
|
|
54
|
+
- `MAPBOX_TOKEN_FORBIDDEN` (missing scopes)
|
|
55
|
+
- `MAPBOX_RATE_LIMITED` (request throttling)
|
|
56
|
+
|
|
57
|
+
For Android dependency download failures during build, verify `MAPBOX_DOWNLOADS_TOKEN` has `DOWNLOADS:READ` scope.
|
|
@@ -159,6 +159,10 @@ public class MapboxNavigationModule: Module {
|
|
|
159
159
|
|
|
160
160
|
private func startNavigation(options: NavigationStartOptions, promise: Promise) {
|
|
161
161
|
guard let destination = options.destination.toCLLocationCoordinate2D() else {
|
|
162
|
+
self.sendEvent("onError", [
|
|
163
|
+
"code": "INVALID_COORDINATES",
|
|
164
|
+
"message": "Invalid coordinates provided"
|
|
165
|
+
])
|
|
162
166
|
promise.reject("INVALID_COORDINATES", "Invalid coordinates provided")
|
|
163
167
|
return
|
|
164
168
|
}
|
|
@@ -184,6 +188,10 @@ public class MapboxNavigationModule: Module {
|
|
|
184
188
|
promise: promise
|
|
185
189
|
)
|
|
186
190
|
case .failure(let error):
|
|
191
|
+
self.sendEvent("onError", [
|
|
192
|
+
"code": "CURRENT_LOCATION_UNAVAILABLE",
|
|
193
|
+
"message": error.localizedDescription
|
|
194
|
+
])
|
|
187
195
|
promise.reject("CURRENT_LOCATION_UNAVAILABLE", error.localizedDescription)
|
|
188
196
|
}
|
|
189
197
|
}
|
|
@@ -225,6 +233,10 @@ public class MapboxNavigationModule: Module {
|
|
|
225
233
|
switch result {
|
|
226
234
|
case .success(let response):
|
|
227
235
|
guard response.routes?.first != nil else {
|
|
236
|
+
self.sendEvent("onError", [
|
|
237
|
+
"code": "NO_ROUTE",
|
|
238
|
+
"message": "No route found"
|
|
239
|
+
])
|
|
228
240
|
promise.reject("NO_ROUTE", "No route found")
|
|
229
241
|
return
|
|
230
242
|
}
|
|
@@ -276,7 +288,12 @@ public class MapboxNavigationModule: Module {
|
|
|
276
288
|
}
|
|
277
289
|
|
|
278
290
|
case .failure(let error):
|
|
279
|
-
|
|
291
|
+
let (code, message) = self.mapDirectionsError(error)
|
|
292
|
+
self.sendEvent("onError", [
|
|
293
|
+
"code": code,
|
|
294
|
+
"message": message
|
|
295
|
+
])
|
|
296
|
+
promise.reject(code, message)
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
}
|
|
@@ -292,6 +309,23 @@ public class MapboxNavigationModule: Module {
|
|
|
292
309
|
}
|
|
293
310
|
}
|
|
294
311
|
|
|
312
|
+
private func mapDirectionsError(_ error: Error) -> (String, String) {
|
|
313
|
+
let message = error.localizedDescription
|
|
314
|
+
let lowered = message.lowercased()
|
|
315
|
+
|
|
316
|
+
if lowered.contains("401") || lowered.contains("unauthorized") {
|
|
317
|
+
return ("MAPBOX_TOKEN_INVALID", "Route fetch failed: unauthorized. Check EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN and token scopes.")
|
|
318
|
+
}
|
|
319
|
+
if lowered.contains("403") || lowered.contains("forbidden") {
|
|
320
|
+
return ("MAPBOX_TOKEN_FORBIDDEN", "Route fetch failed: access forbidden. Verify token scopes and account permissions.")
|
|
321
|
+
}
|
|
322
|
+
if lowered.contains("429") || lowered.contains("rate") {
|
|
323
|
+
return ("MAPBOX_RATE_LIMITED", "Route fetch failed: rate limited by Mapbox.")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return ("ROUTE_ERROR", message)
|
|
327
|
+
}
|
|
328
|
+
|
|
295
329
|
private func stopNavigation(promise: Promise) {
|
|
296
330
|
guard let navVC = navigationViewController else {
|
|
297
331
|
promise.resolve(nil)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomiqlab/react-native-mapbox-navigation",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Native Mapbox turn-by-turn navigation for Expo and React Native (iOS + Android)",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"types": "src/index.tsx",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"README.md",
|
|
28
28
|
"QUICKSTART.md",
|
|
29
29
|
"CHANGELOG.md",
|
|
30
|
+
"SECURITY.md",
|
|
30
31
|
"docs",
|
|
31
32
|
"scripts"
|
|
32
33
|
],
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"react-native": "*"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
|
-
"expo-module-scripts": "^3.0.0"
|
|
74
|
+
"expo-module-scripts": "^3.0.0",
|
|
75
|
+
"typescript": "^5.9.3"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const moduleDir = path.resolve(path.dirname(__filename), "..");
|
|
8
|
-
const repoRoot =
|
|
9
|
-
const androidDir = path.join(
|
|
10
|
-
const envFilePath = path.join(
|
|
8
|
+
const repoRoot = moduleDir;
|
|
9
|
+
const androidDir = path.join(moduleDir, "android");
|
|
10
|
+
const envFilePath = path.join(moduleDir, ".env");
|
|
11
11
|
|
|
12
12
|
function loadDotEnv(filePath) {
|
|
13
13
|
if (!existsSync(filePath)) {
|
|
@@ -49,7 +49,8 @@ function run(command, cwd = moduleDir, options = {}) {
|
|
|
49
49
|
env: {
|
|
50
50
|
...process.env,
|
|
51
51
|
GRADLE_USER_HOME:
|
|
52
|
-
process.env.GRADLE_USER_HOME ||
|
|
52
|
+
process.env.GRADLE_USER_HOME ||
|
|
53
|
+
"/tmp/gradle-user-home-react-native-mapbox-navigation",
|
|
53
54
|
},
|
|
54
55
|
});
|
|
55
56
|
if (output?.length) {
|
|
@@ -72,7 +73,9 @@ function run(command, cwd = moduleDir, options = {}) {
|
|
|
72
73
|
combined.includes("SocketException: Operation not permitted");
|
|
73
74
|
|
|
74
75
|
if (optionalOnOfflineGradle && skippableGradleEnvironmentIssue) {
|
|
75
|
-
console.warn(
|
|
76
|
+
console.warn(
|
|
77
|
+
`\nSkipping optional Gradle check due to restricted/offline environment: ${command}`,
|
|
78
|
+
);
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
81
|
throw error;
|
|
@@ -95,7 +98,14 @@ function runWithFallback(commands, cwd, options = {}) {
|
|
|
95
98
|
|
|
96
99
|
loadDotEnv(envFilePath);
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
const tsconfigPath = path.join(moduleDir, "tsconfig.json");
|
|
102
|
+
if (existsSync(tsconfigPath)) {
|
|
103
|
+
run(`npx --no-install tsc -p ${tsconfigPath} --noEmit`, moduleDir);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(
|
|
106
|
+
"\n> Skipping TypeScript check (tsconfig.json not found in package root).",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
99
109
|
|
|
100
110
|
if (existsSync(path.join(androidDir, "gradlew"))) {
|
|
101
111
|
runWithFallback(
|
|
@@ -104,12 +114,17 @@ if (existsSync(path.join(androidDir, "gradlew"))) {
|
|
|
104
114
|
"./gradlew :mapbox-navigation-native:compileDebugKotlin",
|
|
105
115
|
],
|
|
106
116
|
androidDir,
|
|
107
|
-
{ optionalOnOfflineGradle: true }
|
|
117
|
+
{ optionalOnOfflineGradle: true },
|
|
108
118
|
);
|
|
109
119
|
} else {
|
|
110
|
-
console.log(
|
|
120
|
+
console.log(
|
|
121
|
+
"\n> Skipping Android compile check (android/gradlew not found).",
|
|
122
|
+
);
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
run(
|
|
125
|
+
run(
|
|
126
|
+
"npm pack --dry-run --cache /tmp/npm-cache-react-native-mapbox-navigation",
|
|
127
|
+
moduleDir,
|
|
128
|
+
);
|
|
114
129
|
|
|
115
130
|
console.log("\nRelease verification completed.");
|
|
@@ -1,36 +1,66 @@
|
|
|
1
|
+
/** Geographic coordinate in WGS84 format. */
|
|
1
2
|
export type Coordinate = {
|
|
3
|
+
/** Latitude in range -90..90 */
|
|
2
4
|
latitude: number;
|
|
5
|
+
/** Longitude in range -180..180 */
|
|
3
6
|
longitude: number;
|
|
4
7
|
};
|
|
5
8
|
|
|
9
|
+
/** A route point that can optionally include a display name. */
|
|
6
10
|
export type Waypoint = Coordinate & {
|
|
11
|
+
/** Optional label for UI/debug output. */
|
|
7
12
|
name?: string;
|
|
8
13
|
};
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Options passed to `startNavigation`.
|
|
17
|
+
*/
|
|
10
18
|
export type NavigationOptions = {
|
|
19
|
+
/** Optional route origin. If omitted, native layer may resolve current location. */
|
|
11
20
|
startOrigin?: Coordinate;
|
|
21
|
+
/** Final destination waypoint (required). */
|
|
12
22
|
destination: Waypoint;
|
|
23
|
+
/** Optional intermediate waypoints. */
|
|
13
24
|
waypoints?: Waypoint[];
|
|
25
|
+
/** Enable route simulation for testing without physical movement. */
|
|
14
26
|
shouldSimulateRoute?: boolean;
|
|
27
|
+
/** UI theme selection for native UI. */
|
|
15
28
|
uiTheme?: 'system' | 'light' | 'dark' | 'day' | 'night';
|
|
29
|
+
/** Request alternative routes when available. */
|
|
16
30
|
routeAlternatives?: boolean;
|
|
31
|
+
/** Distance unit for guidance. */
|
|
17
32
|
distanceUnit?: 'metric' | 'imperial';
|
|
33
|
+
/** Guidance language (for example `en`, `fr`). */
|
|
18
34
|
language?: string;
|
|
35
|
+
/** Start muted. */
|
|
19
36
|
mute?: boolean;
|
|
37
|
+
/** Voice volume range `0..1`. */
|
|
20
38
|
voiceVolume?: number;
|
|
39
|
+
/** Camera pitch range `0..85`. */
|
|
21
40
|
cameraPitch?: number;
|
|
41
|
+
/** Camera zoom range `1..22`. */
|
|
22
42
|
cameraZoom?: number;
|
|
43
|
+
/** Camera behavior mode. */
|
|
23
44
|
cameraMode?: 'following' | 'overview';
|
|
45
|
+
/** One style URI used for both day/night fallback. */
|
|
24
46
|
mapStyleUri?: string;
|
|
47
|
+
/** Day style URI override. */
|
|
25
48
|
mapStyleUriDay?: string;
|
|
49
|
+
/** Night style URI override. */
|
|
26
50
|
mapStyleUriNight?: string;
|
|
51
|
+
/** Show speed limit panel when supported. */
|
|
27
52
|
showsSpeedLimits?: boolean;
|
|
53
|
+
/** Show current road name label. */
|
|
28
54
|
showsWayNameLabel?: boolean;
|
|
55
|
+
/** Show trip progress summary. */
|
|
29
56
|
showsTripProgress?: boolean;
|
|
57
|
+
/** Show maneuver/instruction view. */
|
|
30
58
|
showsManeuverView?: boolean;
|
|
59
|
+
/** Show default action buttons in native UI. */
|
|
31
60
|
showsActionButtons?: boolean;
|
|
32
61
|
};
|
|
33
62
|
|
|
63
|
+
/** Runtime settings/state returned by `getNavigationSettings()`. */
|
|
34
64
|
export type NavigationSettings = {
|
|
35
65
|
isNavigating: boolean;
|
|
36
66
|
mute: boolean;
|
|
@@ -39,6 +69,7 @@ export type NavigationSettings = {
|
|
|
39
69
|
language: string;
|
|
40
70
|
};
|
|
41
71
|
|
|
72
|
+
/** Location update payload emitted by native layer. */
|
|
42
73
|
export type LocationUpdate = {
|
|
43
74
|
latitude: number;
|
|
44
75
|
longitude: number;
|
|
@@ -48,6 +79,7 @@ export type LocationUpdate = {
|
|
|
48
79
|
accuracy?: number;
|
|
49
80
|
};
|
|
50
81
|
|
|
82
|
+
/** Route progress payload emitted by native layer. */
|
|
51
83
|
export type RouteProgress = {
|
|
52
84
|
distanceTraveled: number;
|
|
53
85
|
distanceRemaining: number;
|
|
@@ -55,52 +87,45 @@ export type RouteProgress = {
|
|
|
55
87
|
fractionTraveled: number;
|
|
56
88
|
};
|
|
57
89
|
|
|
90
|
+
/** Arrival event payload. */
|
|
58
91
|
export type ArrivalEvent = {
|
|
59
92
|
index?: number;
|
|
60
93
|
name?: string;
|
|
61
94
|
};
|
|
62
95
|
|
|
96
|
+
/** Error payload emitted by native layer. */
|
|
63
97
|
export type NavigationError = {
|
|
98
|
+
/** Machine-readable error code. */
|
|
64
99
|
code: string;
|
|
100
|
+
/** Developer-readable error details. */
|
|
65
101
|
message: string;
|
|
66
102
|
};
|
|
67
103
|
|
|
104
|
+
/** Banner instruction payload emitted during guidance. */
|
|
68
105
|
export type BannerInstruction = {
|
|
69
106
|
primaryText: string;
|
|
70
107
|
secondaryText?: string;
|
|
71
108
|
stepDistanceRemaining?: number;
|
|
72
109
|
};
|
|
73
110
|
|
|
111
|
+
/** Event subscription handle. */
|
|
74
112
|
export type Subscription = {
|
|
75
113
|
remove: () => void;
|
|
76
114
|
};
|
|
77
115
|
|
|
116
|
+
/** Native module interface bridged from iOS/Android. */
|
|
78
117
|
export interface MapboxNavigationModule {
|
|
79
|
-
// Start navigation with options
|
|
80
118
|
startNavigation(options: NavigationOptions): Promise<void>;
|
|
81
|
-
|
|
82
|
-
// Stop/cancel navigation
|
|
83
119
|
stopNavigation(): Promise<void>;
|
|
84
|
-
|
|
85
|
-
// Mute/unmute voice guidance
|
|
86
120
|
setMuted(muted: boolean): Promise<void>;
|
|
87
|
-
|
|
88
|
-
// Set voice volume (0.0 - 1.0)
|
|
89
121
|
setVoiceVolume(volume: number): Promise<void>;
|
|
90
|
-
|
|
91
|
-
// Set distance unit used by spoken and visual instructions
|
|
92
122
|
setDistanceUnit(unit: 'metric' | 'imperial'): Promise<void>;
|
|
93
|
-
|
|
94
|
-
// Set route instruction language (BCP-47, e.g. 'en', 'fr')
|
|
95
123
|
setLanguage(language: string): Promise<void>;
|
|
96
|
-
|
|
97
|
-
// Check if navigation is active
|
|
98
124
|
isNavigating(): Promise<boolean>;
|
|
99
|
-
|
|
100
|
-
// Get current native navigation settings
|
|
101
125
|
getNavigationSettings(): Promise<NavigationSettings>;
|
|
102
126
|
}
|
|
103
127
|
|
|
128
|
+
/** Props for embedded `MapboxNavigationView`. */
|
|
104
129
|
export interface MapboxNavigationViewProps {
|
|
105
130
|
style?: any;
|
|
106
131
|
startOrigin?: Coordinate;
|
|
@@ -125,12 +150,17 @@ export interface MapboxNavigationViewProps {
|
|
|
125
150
|
showsTripProgress?: boolean;
|
|
126
151
|
showsManeuverView?: boolean;
|
|
127
152
|
showsActionButtons?: boolean;
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
|
|
154
|
+
/** Callback for location changes. */
|
|
130
155
|
onLocationChange?: (location: LocationUpdate) => void;
|
|
156
|
+
/** Callback for route progress changes. */
|
|
131
157
|
onRouteProgressChange?: (progress: RouteProgress) => void;
|
|
158
|
+
/** Callback when arrival is detected. */
|
|
132
159
|
onArrive?: (point: ArrivalEvent) => void;
|
|
160
|
+
/** Callback when navigation is canceled by user. */
|
|
133
161
|
onCancelNavigation?: () => void;
|
|
162
|
+
/** Callback for native errors. */
|
|
134
163
|
onError?: (error: NavigationError) => void;
|
|
164
|
+
/** Callback for banner instruction updates. */
|
|
135
165
|
onBannerInstruction?: (instruction: BannerInstruction) => void;
|
|
136
166
|
}
|
package/src/index.tsx
CHANGED
|
@@ -2,24 +2,22 @@ import { requireNativeModule, requireNativeViewManager } from "expo-modules-core
|
|
|
2
2
|
import { ViewProps } from "react-native";
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
ArrivalEvent,
|
|
6
|
+
BannerInstruction,
|
|
7
|
+
LocationUpdate,
|
|
8
|
+
MapboxNavigationModule as MapboxNavigationModuleType,
|
|
9
|
+
MapboxNavigationViewProps,
|
|
10
|
+
NavigationSettings,
|
|
11
|
+
NavigationError,
|
|
12
|
+
NavigationOptions,
|
|
13
|
+
RouteProgress,
|
|
14
|
+
Subscription,
|
|
15
15
|
} from "./MapboxNavigation.types";
|
|
16
16
|
|
|
17
|
-
// Get the native module (use requireNativeModule instead of deprecated NativeModulesProxy)
|
|
18
17
|
const MapboxNavigationModule = requireNativeModule<MapboxNavigationModuleType>(
|
|
19
18
|
"MapboxNavigationModule",
|
|
20
19
|
);
|
|
21
20
|
|
|
22
|
-
// The native module already acts as an EventEmitter; don't wrap with `new EventEmitter`.
|
|
23
21
|
const emitter = MapboxNavigationModule as unknown as {
|
|
24
22
|
addListener: (
|
|
25
23
|
eventName: string,
|
|
@@ -106,74 +104,166 @@ function normalizeNavigationOptions(options: NavigationOptions): NavigationOptio
|
|
|
106
104
|
};
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
|
|
107
|
+
function normalizeNativeError(error: unknown, fallbackCode = "NATIVE_ERROR"): Error {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
return error;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const candidate = error as { code?: string; message?: string } | undefined;
|
|
113
|
+
const code = candidate?.code ?? fallbackCode;
|
|
114
|
+
const message = candidate?.message ?? "Unknown native error";
|
|
115
|
+
return new Error(`[${code}] ${message}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start full-screen native turn-by-turn navigation.
|
|
120
|
+
*
|
|
121
|
+
* @param options Navigation settings such as destination, camera mode, simulation, and map UI options.
|
|
122
|
+
* @throws Error if options are invalid or native route/token setup fails.
|
|
123
|
+
*/
|
|
110
124
|
export async function startNavigation(
|
|
111
125
|
options: NavigationOptions,
|
|
112
126
|
): Promise<void> {
|
|
113
127
|
const normalizedOptions = normalizeNavigationOptions(options);
|
|
114
|
-
|
|
128
|
+
try {
|
|
129
|
+
await MapboxNavigationModule.startNavigation(normalizedOptions);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw normalizeNativeError(error, "START_NAVIGATION_FAILED");
|
|
132
|
+
}
|
|
115
133
|
}
|
|
116
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Stop/dismiss native navigation if active.
|
|
137
|
+
*/
|
|
117
138
|
export async function stopNavigation(): Promise<void> {
|
|
118
|
-
|
|
139
|
+
try {
|
|
140
|
+
await MapboxNavigationModule.stopNavigation();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw normalizeNativeError(error, "STOP_NAVIGATION_FAILED");
|
|
143
|
+
}
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Enable or disable voice guidance.
|
|
148
|
+
*
|
|
149
|
+
* @param muted `true` to mute voice instructions.
|
|
150
|
+
*/
|
|
121
151
|
export async function setMuted(muted: boolean): Promise<void> {
|
|
122
|
-
|
|
152
|
+
try {
|
|
153
|
+
await MapboxNavigationModule.setMuted(muted);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw normalizeNativeError(error, "SET_MUTED_FAILED");
|
|
156
|
+
}
|
|
123
157
|
}
|
|
124
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Set voice instruction volume in range `0..1`.
|
|
161
|
+
*/
|
|
125
162
|
export async function setVoiceVolume(volume: number): Promise<void> {
|
|
126
|
-
|
|
163
|
+
try {
|
|
164
|
+
await MapboxNavigationModule.setVoiceVolume(volume);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw normalizeNativeError(error, "SET_VOICE_VOLUME_FAILED");
|
|
167
|
+
}
|
|
127
168
|
}
|
|
128
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Set spoken and displayed distance units.
|
|
172
|
+
*/
|
|
129
173
|
export async function setDistanceUnit(unit: "metric" | "imperial"): Promise<void> {
|
|
130
|
-
|
|
174
|
+
try {
|
|
175
|
+
await MapboxNavigationModule.setDistanceUnit(unit);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw normalizeNativeError(error, "SET_DISTANCE_UNIT_FAILED");
|
|
178
|
+
}
|
|
131
179
|
}
|
|
132
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Set instruction language (BCP-47-like code, for example `en`, `fr`).
|
|
183
|
+
*/
|
|
133
184
|
export async function setLanguage(language: string): Promise<void> {
|
|
134
|
-
|
|
185
|
+
try {
|
|
186
|
+
await MapboxNavigationModule.setLanguage(language);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw normalizeNativeError(error, "SET_LANGUAGE_FAILED");
|
|
189
|
+
}
|
|
135
190
|
}
|
|
136
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Check whether full-screen native navigation is currently active.
|
|
194
|
+
*/
|
|
137
195
|
export async function isNavigating(): Promise<boolean> {
|
|
138
|
-
|
|
196
|
+
try {
|
|
197
|
+
return await MapboxNavigationModule.isNavigating();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw normalizeNativeError(error, "IS_NAVIGATING_FAILED");
|
|
200
|
+
}
|
|
139
201
|
}
|
|
140
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Read current native navigation runtime settings.
|
|
205
|
+
*/
|
|
141
206
|
export async function getNavigationSettings(): Promise<NavigationSettings> {
|
|
142
|
-
|
|
207
|
+
try {
|
|
208
|
+
return await MapboxNavigationModule.getNavigationSettings();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw normalizeNativeError(error, "GET_NAVIGATION_SETTINGS_FAILED");
|
|
211
|
+
}
|
|
143
212
|
}
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Subscribe to location updates from native navigation.
|
|
216
|
+
*/
|
|
146
217
|
export function addLocationChangeListener(
|
|
147
218
|
listener: (location: LocationUpdate) => void,
|
|
148
219
|
): Subscription {
|
|
149
220
|
return emitter.addListener("onLocationChange", listener);
|
|
150
221
|
}
|
|
151
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Subscribe to route progress updates.
|
|
225
|
+
*/
|
|
152
226
|
export function addRouteProgressChangeListener(
|
|
153
227
|
listener: (progress: RouteProgress) => void,
|
|
154
228
|
): Subscription {
|
|
155
229
|
return emitter.addListener("onRouteProgressChange", listener);
|
|
156
230
|
}
|
|
157
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Subscribe to arrival events.
|
|
234
|
+
*/
|
|
158
235
|
export function addArriveListener(listener: (point: ArrivalEvent) => void): Subscription {
|
|
159
236
|
return emitter.addListener("onArrive", listener);
|
|
160
237
|
}
|
|
161
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Subscribe to cancellation events.
|
|
241
|
+
*/
|
|
162
242
|
export function addCancelNavigationListener(listener: () => void): Subscription {
|
|
163
243
|
return emitter.addListener("onCancelNavigation", listener);
|
|
164
244
|
}
|
|
165
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Subscribe to native errors (token issues, route fetch failures, permission failures, etc.).
|
|
248
|
+
*/
|
|
166
249
|
export function addErrorListener(listener: (error: NavigationError) => void): Subscription {
|
|
167
250
|
return emitter.addListener("onError", listener);
|
|
168
251
|
}
|
|
169
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Subscribe to banner instruction updates.
|
|
255
|
+
*/
|
|
170
256
|
export function addBannerInstructionListener(
|
|
171
257
|
listener: (instruction: BannerInstruction) => void,
|
|
172
258
|
): Subscription {
|
|
173
259
|
return emitter.addListener("onBannerInstruction", listener);
|
|
174
260
|
}
|
|
175
261
|
|
|
176
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Embedded native navigation component.
|
|
264
|
+
*
|
|
265
|
+
* Use this when you need navigation inside your own screen layout instead of full-screen modal navigation.
|
|
266
|
+
*/
|
|
177
267
|
export function MapboxNavigationView(
|
|
178
268
|
props: MapboxNavigationViewProps & ViewProps,
|
|
179
269
|
) {
|
|
@@ -181,10 +271,8 @@ export function MapboxNavigationView(
|
|
|
181
271
|
return <NativeView {...props} />;
|
|
182
272
|
}
|
|
183
273
|
|
|
184
|
-
// Export types
|
|
185
274
|
export * from "./MapboxNavigation.types";
|
|
186
275
|
|
|
187
|
-
// Default export
|
|
188
276
|
export default {
|
|
189
277
|
startNavigation,
|
|
190
278
|
stopNavigation,
|