@atomiqlab/react-native-mapbox-navigation 1.1.1 → 1.1.3
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 +379 -0
- package/SECURITY.md +36 -0
- package/android/src/main/java/expo/modules/mapboxnavigation/MapboxNavigationActivity.kt +96 -8
- package/app.plugin.js +64 -4
- package/docs/TROUBLESHOOTING.md +22 -0
- package/docs/imgs/support_me_on_kofi_badge_red.png +0 -0
- package/docs/screenshots/.gitkeep +0 -0
- package/docs/screenshots/android.png +0 -0
- package/docs/screenshots/ios.png +0 -0
- package/ios/MapboxNavigationModule.swift +143 -2
- 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
|
@@ -11,6 +11,17 @@ Native Mapbox turn-by-turn navigation for Expo apps on iOS and Android.
|
|
|
11
11
|
- Navigation customization: camera mode/pitch/zoom, theme, map style, and UI visibility toggles.
|
|
12
12
|
- Expo config plugin that applies required Android and iOS native setup.
|
|
13
13
|
|
|
14
|
+
## Screenshots
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Support Us
|
|
20
|
+
|
|
21
|
+
[](https://ko-fi.com/atomiqlab)
|
|
22
|
+
|
|
23
|
+
If this package helps your work, support development on Ko-fi.
|
|
24
|
+
|
|
14
25
|
## Requirements
|
|
15
26
|
|
|
16
27
|
- Expo SDK `>=50`
|
|
@@ -50,6 +61,16 @@ Regenerate native projects:
|
|
|
50
61
|
npx expo prebuild --clean
|
|
51
62
|
```
|
|
52
63
|
|
|
64
|
+
### Token validation behavior
|
|
65
|
+
|
|
66
|
+
The config plugin now fails fast during prebuild/build when tokens are missing or malformed:
|
|
67
|
+
|
|
68
|
+
- Missing/invalid public token (`pk...`)
|
|
69
|
+
- Missing/invalid downloads token (`sk...`)
|
|
70
|
+
|
|
71
|
+
This prevents silent runtime failures and surfaces setup issues early.
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
## Quick Start
|
|
54
75
|
|
|
55
76
|
```ts
|
|
@@ -107,6 +128,353 @@ import { MapboxNavigationView } from "@atomiqlab/react-native-mapbox-navigation"
|
|
|
107
128
|
/>;
|
|
108
129
|
```
|
|
109
130
|
|
|
131
|
+
```tsx
|
|
132
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
133
|
+
import {
|
|
134
|
+
addArriveListener,
|
|
135
|
+
addBannerInstructionListener,
|
|
136
|
+
addCancelNavigationListener,
|
|
137
|
+
addErrorListener,
|
|
138
|
+
addLocationChangeListener,
|
|
139
|
+
addRouteProgressChangeListener,
|
|
140
|
+
getNavigationSettings,
|
|
141
|
+
isNavigating,
|
|
142
|
+
MapboxNavigationView,
|
|
143
|
+
setDistanceUnit,
|
|
144
|
+
setLanguage,
|
|
145
|
+
setMuted,
|
|
146
|
+
setVoiceVolume,
|
|
147
|
+
startNavigation,
|
|
148
|
+
stopNavigation,
|
|
149
|
+
type Waypoint,
|
|
150
|
+
} from "@atomiqlab/react-native-mapbox-navigation";
|
|
151
|
+
import {
|
|
152
|
+
Platform,
|
|
153
|
+
Pressable,
|
|
154
|
+
SafeAreaView,
|
|
155
|
+
ScrollView,
|
|
156
|
+
StyleSheet,
|
|
157
|
+
Text,
|
|
158
|
+
TextInput,
|
|
159
|
+
View,
|
|
160
|
+
} from "react-native";
|
|
161
|
+
|
|
162
|
+
const START: Waypoint = {
|
|
163
|
+
latitude: 37.7749,
|
|
164
|
+
longitude: -122.4194,
|
|
165
|
+
name: "San Francisco",
|
|
166
|
+
};
|
|
167
|
+
const DEST: Waypoint = {
|
|
168
|
+
latitude: 37.7847,
|
|
169
|
+
longitude: -122.4073,
|
|
170
|
+
name: "Downtown",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default function Index() {
|
|
174
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
175
|
+
const [navigating, setNavigating] = useState(false);
|
|
176
|
+
const [showEmbedded, setShowEmbedded] = useState(false);
|
|
177
|
+
|
|
178
|
+
const [mute, setMuteState] = useState(false);
|
|
179
|
+
const [unit, setUnit] = useState<"metric" | "imperial">("metric");
|
|
180
|
+
const [language, setLanguageState] = useState("en");
|
|
181
|
+
const [volumeInput, setVolumeInput] = useState("1");
|
|
182
|
+
|
|
183
|
+
const pushLog = (line: string) =>
|
|
184
|
+
setLogs((prev) =>
|
|
185
|
+
[`${new Date().toLocaleTimeString()} ${line}`, ...prev].slice(0, 40),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const s1 = addLocationChangeListener((e) =>
|
|
190
|
+
pushLog(`location: ${e.latitude.toFixed(5)}, ${e.longitude.toFixed(5)}`),
|
|
191
|
+
);
|
|
192
|
+
const s2 = addRouteProgressChangeListener((e) =>
|
|
193
|
+
pushLog(`progress: ${(e.fractionTraveled * 100).toFixed(1)}%`),
|
|
194
|
+
);
|
|
195
|
+
const s3 = addBannerInstructionListener((e) =>
|
|
196
|
+
pushLog(`banner: ${e.primaryText}`),
|
|
197
|
+
);
|
|
198
|
+
const s4 = addArriveListener((e) => {
|
|
199
|
+
pushLog(`arrive: ${e.name ?? "destination"}`);
|
|
200
|
+
setNavigating(false);
|
|
201
|
+
});
|
|
202
|
+
const s5 = addCancelNavigationListener(() => {
|
|
203
|
+
pushLog("cancelled");
|
|
204
|
+
setNavigating(false);
|
|
205
|
+
});
|
|
206
|
+
const s6 = addErrorListener((e) =>
|
|
207
|
+
pushLog(`error: ${e.message ?? e.code}`),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
setNavigating(await isNavigating());
|
|
213
|
+
pushLog(`isNavigating() loaded`);
|
|
214
|
+
} catch (e: any) {
|
|
215
|
+
pushLog(`isNavigating failed: ${e?.message ?? "unknown"}`);
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
s1.remove();
|
|
221
|
+
s2.remove();
|
|
222
|
+
s3.remove();
|
|
223
|
+
s4.remove();
|
|
224
|
+
s5.remove();
|
|
225
|
+
s6.remove();
|
|
226
|
+
};
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
const canRun = useMemo(
|
|
230
|
+
() => Platform.OS === "ios" || Platform.OS === "android",
|
|
231
|
+
[],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!canRun) {
|
|
235
|
+
return (
|
|
236
|
+
<SafeAreaView style={styles.center}>
|
|
237
|
+
<Text style={styles.title}>Run on iOS/Android only</Text>
|
|
238
|
+
</SafeAreaView>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<SafeAreaView style={styles.container}>
|
|
244
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
245
|
+
<Text style={styles.title}>Mapbox Navigation Full API Test</Text>
|
|
246
|
+
<Text style={styles.status}>
|
|
247
|
+
Status: {navigating ? "Navigating" : "Idle"}
|
|
248
|
+
</Text>
|
|
249
|
+
|
|
250
|
+
<View style={styles.row}>
|
|
251
|
+
<Pressable
|
|
252
|
+
style={styles.btn}
|
|
253
|
+
onPress={async () => {
|
|
254
|
+
try {
|
|
255
|
+
await startNavigation({
|
|
256
|
+
startOrigin: START,
|
|
257
|
+
destination: DEST,
|
|
258
|
+
shouldSimulateRoute: true,
|
|
259
|
+
routeAlternatives: true,
|
|
260
|
+
cameraMode: "following",
|
|
261
|
+
uiTheme: "system",
|
|
262
|
+
mute,
|
|
263
|
+
voiceVolume: Math.max(
|
|
264
|
+
0,
|
|
265
|
+
Math.min(Number(volumeInput) || 1, 1),
|
|
266
|
+
),
|
|
267
|
+
distanceUnit: unit,
|
|
268
|
+
language,
|
|
269
|
+
});
|
|
270
|
+
setNavigating(true);
|
|
271
|
+
pushLog("startNavigation() ok");
|
|
272
|
+
} catch (e: any) {
|
|
273
|
+
pushLog(`startNavigation failed: ${e?.message ?? "unknown"}`);
|
|
274
|
+
}
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
<Text style={styles.btnText}>startNavigation</Text>
|
|
278
|
+
</Pressable>
|
|
279
|
+
|
|
280
|
+
<Pressable
|
|
281
|
+
style={styles.btn}
|
|
282
|
+
onPress={async () => {
|
|
283
|
+
try {
|
|
284
|
+
await stopNavigation();
|
|
285
|
+
setNavigating(false);
|
|
286
|
+
pushLog("stopNavigation() ok");
|
|
287
|
+
} catch (e: any) {
|
|
288
|
+
pushLog(`stopNavigation failed: ${e?.message ?? "unknown"}`);
|
|
289
|
+
}
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
<Text style={styles.btnText}>stopNavigation</Text>
|
|
293
|
+
</Pressable>
|
|
294
|
+
</View>
|
|
295
|
+
|
|
296
|
+
<View style={styles.row}>
|
|
297
|
+
<Pressable
|
|
298
|
+
style={styles.btn}
|
|
299
|
+
onPress={async () =>
|
|
300
|
+
pushLog(`isNavigating(): ${await isNavigating()}`)
|
|
301
|
+
}
|
|
302
|
+
>
|
|
303
|
+
<Text style={styles.btnText}>isNavigating</Text>
|
|
304
|
+
</Pressable>
|
|
305
|
+
|
|
306
|
+
<Pressable
|
|
307
|
+
style={styles.btn}
|
|
308
|
+
onPress={async () =>
|
|
309
|
+
pushLog(
|
|
310
|
+
`getNavigationSettings(): ${JSON.stringify(await getNavigationSettings())}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
>
|
|
314
|
+
<Text style={styles.btnText}>getNavigationSettings</Text>
|
|
315
|
+
</Pressable>
|
|
316
|
+
</View>
|
|
317
|
+
|
|
318
|
+
<View style={styles.card}>
|
|
319
|
+
<Text style={styles.label}>mute / setMuted</Text>
|
|
320
|
+
<View style={styles.row}>
|
|
321
|
+
<Pressable
|
|
322
|
+
style={styles.btn}
|
|
323
|
+
onPress={async () => {
|
|
324
|
+
const next = !mute;
|
|
325
|
+
setMuteState(next);
|
|
326
|
+
await setMuted(next);
|
|
327
|
+
pushLog(`setMuted(${next})`);
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
<Text style={styles.btnText}>{mute ? "Unmute" : "Mute"}</Text>
|
|
331
|
+
</Pressable>
|
|
332
|
+
</View>
|
|
333
|
+
|
|
334
|
+
<Text style={styles.label}>setVoiceVolume (0..1)</Text>
|
|
335
|
+
<View style={styles.row}>
|
|
336
|
+
<TextInput
|
|
337
|
+
style={styles.input}
|
|
338
|
+
value={volumeInput}
|
|
339
|
+
onChangeText={setVolumeInput}
|
|
340
|
+
keyboardType="decimal-pad"
|
|
341
|
+
/>
|
|
342
|
+
<Pressable
|
|
343
|
+
style={styles.btn}
|
|
344
|
+
onPress={async () => {
|
|
345
|
+
const vol = Math.max(0, Math.min(Number(volumeInput) || 1, 1));
|
|
346
|
+
await setVoiceVolume(vol);
|
|
347
|
+
pushLog(`setVoiceVolume(${vol})`);
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
<Text style={styles.btnText}>Apply</Text>
|
|
351
|
+
</Pressable>
|
|
352
|
+
</View>
|
|
353
|
+
|
|
354
|
+
<Text style={styles.label}>setDistanceUnit</Text>
|
|
355
|
+
<View style={styles.row}>
|
|
356
|
+
<Pressable
|
|
357
|
+
style={styles.btn}
|
|
358
|
+
onPress={async () => {
|
|
359
|
+
setUnit("metric");
|
|
360
|
+
await setDistanceUnit("metric");
|
|
361
|
+
pushLog("setDistanceUnit(metric)");
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
<Text style={styles.btnText}>Metric</Text>
|
|
365
|
+
</Pressable>
|
|
366
|
+
<Pressable
|
|
367
|
+
style={styles.btn}
|
|
368
|
+
onPress={async () => {
|
|
369
|
+
setUnit("imperial");
|
|
370
|
+
await setDistanceUnit("imperial");
|
|
371
|
+
pushLog("setDistanceUnit(imperial)");
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
<Text style={styles.btnText}>Imperial</Text>
|
|
375
|
+
</Pressable>
|
|
376
|
+
</View>
|
|
377
|
+
|
|
378
|
+
<Text style={styles.label}>setLanguage</Text>
|
|
379
|
+
<View style={styles.row}>
|
|
380
|
+
<TextInput
|
|
381
|
+
style={styles.input}
|
|
382
|
+
value={language}
|
|
383
|
+
onChangeText={setLanguageState}
|
|
384
|
+
/>
|
|
385
|
+
<Pressable
|
|
386
|
+
style={styles.btn}
|
|
387
|
+
onPress={async () => {
|
|
388
|
+
await setLanguage(language.trim() || "en");
|
|
389
|
+
pushLog(`setLanguage(${language.trim() || "en"})`);
|
|
390
|
+
}}
|
|
391
|
+
>
|
|
392
|
+
<Text style={styles.btnText}>Apply</Text>
|
|
393
|
+
</Pressable>
|
|
394
|
+
</View>
|
|
395
|
+
</View>
|
|
396
|
+
|
|
397
|
+
<Pressable
|
|
398
|
+
style={styles.btn}
|
|
399
|
+
onPress={() => setShowEmbedded((v) => !v)}
|
|
400
|
+
>
|
|
401
|
+
<Text style={styles.btnText}>
|
|
402
|
+
{showEmbedded ? "Hide" : "Show"} Embedded MapboxNavigationView
|
|
403
|
+
</Text>
|
|
404
|
+
</Pressable>
|
|
405
|
+
|
|
406
|
+
{showEmbedded ? (
|
|
407
|
+
<View style={styles.embeddedWrap}>
|
|
408
|
+
<MapboxNavigationView
|
|
409
|
+
style={{ flex: 1 }}
|
|
410
|
+
destination={DEST}
|
|
411
|
+
startOrigin={START}
|
|
412
|
+
shouldSimulateRoute
|
|
413
|
+
cameraMode="overview"
|
|
414
|
+
uiTheme="system"
|
|
415
|
+
onError={(e) => pushLog(`embedded error: ${e.message ?? e.code}`)}
|
|
416
|
+
onBannerInstruction={(e) =>
|
|
417
|
+
pushLog(`embedded banner: ${e.primaryText}`)
|
|
418
|
+
}
|
|
419
|
+
onArrive={(e) =>
|
|
420
|
+
pushLog(`embedded arrive: ${e.name ?? "destination"}`)
|
|
421
|
+
}
|
|
422
|
+
onCancelNavigation={() => pushLog("embedded cancelled")}
|
|
423
|
+
/>
|
|
424
|
+
</View>
|
|
425
|
+
) : null}
|
|
426
|
+
|
|
427
|
+
<View style={styles.card}>
|
|
428
|
+
<Text style={styles.label}>Event Logs</Text>
|
|
429
|
+
{logs.map((l, i) => (
|
|
430
|
+
<Text key={`${l}-${i}`} style={styles.log}>
|
|
431
|
+
{l}
|
|
432
|
+
</Text>
|
|
433
|
+
))}
|
|
434
|
+
</View>
|
|
435
|
+
</ScrollView>
|
|
436
|
+
</SafeAreaView>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const styles = StyleSheet.create({
|
|
441
|
+
container: { flex: 1, backgroundColor: "#0b1020" },
|
|
442
|
+
center: {
|
|
443
|
+
flex: 1,
|
|
444
|
+
justifyContent: "center",
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
backgroundColor: "#0b1020",
|
|
447
|
+
},
|
|
448
|
+
content: { padding: 14, gap: 10, paddingBottom: 28 },
|
|
449
|
+
title: { color: "#fff", fontSize: 22, fontWeight: "800" },
|
|
450
|
+
status: { color: "#8ec5ff", fontSize: 13 },
|
|
451
|
+
row: { flexDirection: "row", gap: 8, alignItems: "center" },
|
|
452
|
+
card: { backgroundColor: "#131a2d", borderRadius: 12, padding: 10, gap: 8 },
|
|
453
|
+
label: { color: "#cde4ff", fontSize: 12, fontWeight: "700" },
|
|
454
|
+
input: {
|
|
455
|
+
flex: 1,
|
|
456
|
+
backgroundColor: "#0e1426",
|
|
457
|
+
borderColor: "#2b3a5d",
|
|
458
|
+
borderWidth: 1,
|
|
459
|
+
borderRadius: 8,
|
|
460
|
+
color: "#fff",
|
|
461
|
+
paddingHorizontal: 10,
|
|
462
|
+
paddingVertical: 8,
|
|
463
|
+
},
|
|
464
|
+
btn: {
|
|
465
|
+
flex: 1,
|
|
466
|
+
backgroundColor: "#2563eb",
|
|
467
|
+
borderRadius: 10,
|
|
468
|
+
paddingVertical: 10,
|
|
469
|
+
paddingHorizontal: 10,
|
|
470
|
+
alignItems: "center",
|
|
471
|
+
},
|
|
472
|
+
btnText: { color: "#fff", fontWeight: "700", fontSize: 12 },
|
|
473
|
+
embeddedWrap: { height: 320, borderRadius: 12, overflow: "hidden" },
|
|
474
|
+
log: { color: "#dbeafe", fontSize: 11, marginBottom: 2 },
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
110
478
|
## API Overview
|
|
111
479
|
|
|
112
480
|
Core functions:
|
|
@@ -139,6 +507,17 @@ Main `NavigationOptions` fields:
|
|
|
139
507
|
|
|
140
508
|
Full types: `src/MapboxNavigation.types.ts`
|
|
141
509
|
|
|
510
|
+
## Common Error Codes
|
|
511
|
+
|
|
512
|
+
- `MAPBOX_TOKEN_INVALID`: invalid/expired token or unauthorized access
|
|
513
|
+
- `MAPBOX_TOKEN_FORBIDDEN`: token lacks required scopes/permissions
|
|
514
|
+
- `MAPBOX_RATE_LIMITED`: Mapbox rate limit reached
|
|
515
|
+
- `ROUTE_FETCH_FAILED`: route request failed with native details
|
|
516
|
+
- `CURRENT_LOCATION_UNAVAILABLE`: unable to resolve device location
|
|
517
|
+
- `INVALID_COORDINATES`: invalid origin/destination coordinates
|
|
518
|
+
|
|
519
|
+
Subscribe via `addErrorListener` or `onError` to surface these to developers during testing and production diagnostics.
|
|
520
|
+
|
|
142
521
|
## Platform Notes
|
|
143
522
|
|
|
144
523
|
- 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`)
|
|
@@ -8,7 +8,10 @@ import android.os.Handler
|
|
|
8
8
|
import android.os.Looper
|
|
9
9
|
import android.util.Log
|
|
10
10
|
import android.view.Gravity
|
|
11
|
+
import android.view.ViewGroup
|
|
12
|
+
import android.widget.Button
|
|
11
13
|
import android.widget.FrameLayout
|
|
14
|
+
import android.widget.LinearLayout
|
|
12
15
|
import android.widget.TextView
|
|
13
16
|
import androidx.appcompat.app.AppCompatDelegate
|
|
14
17
|
import androidx.appcompat.app.AppCompatActivity
|
|
@@ -95,7 +98,7 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
95
98
|
result[Manifest.permission.ACCESS_COARSE_LOCATION] == true
|
|
96
99
|
|
|
97
100
|
if (!granted) {
|
|
98
|
-
|
|
101
|
+
showErrorScreen("Location permission is required to start navigation.", null)
|
|
99
102
|
return@registerForActivityResult
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -112,6 +115,15 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
112
115
|
|
|
113
116
|
override fun onRouteFetchFailed(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
|
|
114
117
|
Log.e(TAG, "Route fetch failed. reasons=${reasons.size}, options=$routeOptions")
|
|
118
|
+
val (code, message) = mapRouteFetchFailure(reasons)
|
|
119
|
+
MapboxNavigationEventBridge.emit(
|
|
120
|
+
"onError",
|
|
121
|
+
mapOf(
|
|
122
|
+
"code" to code,
|
|
123
|
+
"message" to message
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
showErrorScreen(message, null)
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
override fun onRouteFetchSuccessful(routes: List<NavigationRoute>) {
|
|
@@ -127,6 +139,13 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
127
139
|
|
|
128
140
|
override fun onRouteFetchCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
|
|
129
141
|
Log.w(TAG, "Route fetch canceled. origin=$routerOrigin, options=$routeOptions")
|
|
142
|
+
MapboxNavigationEventBridge.emit(
|
|
143
|
+
"onError",
|
|
144
|
+
mapOf(
|
|
145
|
+
"code" to "ROUTE_FETCH_CANCELED",
|
|
146
|
+
"message" to "Route fetch canceled (origin: $routerOrigin)."
|
|
147
|
+
)
|
|
148
|
+
)
|
|
130
149
|
}
|
|
131
150
|
}
|
|
132
151
|
|
|
@@ -166,7 +185,7 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
166
185
|
Log.w(TAG, "No valid destination extras provided, starting without preview")
|
|
167
186
|
}
|
|
168
187
|
} catch (throwable: Throwable) {
|
|
169
|
-
|
|
188
|
+
showErrorScreen("Navigation init failed: ${throwable.message}", throwable)
|
|
170
189
|
return
|
|
171
190
|
}
|
|
172
191
|
|
|
@@ -256,7 +275,7 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
} catch (throwable: Throwable) {
|
|
259
|
-
|
|
278
|
+
showErrorScreen("Failed to create NavigationView: ${throwable.message}", throwable)
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
|
|
@@ -301,6 +320,25 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
301
320
|
}
|
|
302
321
|
}
|
|
303
322
|
|
|
323
|
+
private fun mapRouteFetchFailure(reasons: List<RouterFailure>): Pair<String, String> {
|
|
324
|
+
val details = reasons.joinToString(" | ") { it.message.orEmpty() }.trim()
|
|
325
|
+
if (details.contains("401") || details.contains("unauthorized", ignoreCase = true)) {
|
|
326
|
+
return "MAPBOX_TOKEN_INVALID" to
|
|
327
|
+
"Route fetch failed: unauthorized. Check EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN and token scopes."
|
|
328
|
+
}
|
|
329
|
+
if (details.contains("403") || details.contains("forbidden", ignoreCase = true)) {
|
|
330
|
+
return "MAPBOX_TOKEN_FORBIDDEN" to
|
|
331
|
+
"Route fetch failed: access forbidden. Verify token scopes and account permissions."
|
|
332
|
+
}
|
|
333
|
+
if (details.contains("429") || details.contains("rate", ignoreCase = true)) {
|
|
334
|
+
return "MAPBOX_RATE_LIMITED" to
|
|
335
|
+
"Route fetch failed: rate limited by Mapbox."
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return "ROUTE_FETCH_FAILED" to
|
|
339
|
+
(if (details.isNotEmpty()) "Route fetch failed: $details" else "Route fetch failed for unknown reason.")
|
|
340
|
+
}
|
|
341
|
+
|
|
304
342
|
private fun hasLocationPermission(): Boolean {
|
|
305
343
|
val fineGranted = ContextCompat.checkSelfPermission(
|
|
306
344
|
this,
|
|
@@ -315,23 +353,73 @@ class MapboxNavigationActivity : AppCompatActivity() {
|
|
|
315
353
|
return fineGranted || coarseGranted
|
|
316
354
|
}
|
|
317
355
|
|
|
318
|
-
private fun
|
|
356
|
+
private fun showErrorScreen(message: String, throwable: Throwable?) {
|
|
319
357
|
if (throwable != null) {
|
|
320
358
|
Log.e(TAG, message, throwable)
|
|
321
359
|
} else {
|
|
322
360
|
Log.e(TAG, message)
|
|
323
361
|
}
|
|
324
362
|
|
|
325
|
-
|
|
363
|
+
MapboxNavigationEventBridge.emit(
|
|
364
|
+
"onError",
|
|
365
|
+
mapOf(
|
|
366
|
+
"code" to "NATIVE_ERROR",
|
|
367
|
+
"message" to message
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
val titleView = TextView(this).apply {
|
|
372
|
+
text = "Navigation Error"
|
|
373
|
+
textSize = 22f
|
|
374
|
+
setTextColor(0xFFFFFFFF.toInt())
|
|
375
|
+
gravity = Gravity.CENTER
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
val messageView = TextView(this).apply {
|
|
326
379
|
text = message
|
|
380
|
+
textSize = 15f
|
|
381
|
+
setTextColor(0xFFD6E4FF.toInt())
|
|
327
382
|
gravity = Gravity.CENTER
|
|
328
|
-
|
|
329
|
-
setPadding(32, 32, 32, 32)
|
|
383
|
+
setPadding(0, 24, 0, 32)
|
|
330
384
|
}
|
|
385
|
+
|
|
386
|
+
val closeButton = Button(this).apply {
|
|
387
|
+
text = "Back"
|
|
388
|
+
setOnClickListener { finish() }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
val content = LinearLayout(this).apply {
|
|
392
|
+
orientation = LinearLayout.VERTICAL
|
|
393
|
+
gravity = Gravity.CENTER
|
|
394
|
+
setBackgroundColor(0xFF0B1020.toInt())
|
|
395
|
+
setPadding(48, 48, 48, 48)
|
|
396
|
+
addView(
|
|
397
|
+
titleView,
|
|
398
|
+
LinearLayout.LayoutParams(
|
|
399
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
400
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
addView(
|
|
404
|
+
messageView,
|
|
405
|
+
LinearLayout.LayoutParams(
|
|
406
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
407
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
addView(
|
|
411
|
+
closeButton,
|
|
412
|
+
LinearLayout.LayoutParams(
|
|
413
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
414
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
331
419
|
setContentView(
|
|
332
420
|
FrameLayout(this).apply {
|
|
333
421
|
addView(
|
|
334
|
-
|
|
422
|
+
content,
|
|
335
423
|
FrameLayout.LayoutParams(
|
|
336
424
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
337
425
|
FrameLayout.LayoutParams.MATCH_PARENT
|
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;
|