@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.
@@ -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.
File without changes
Binary file
Binary file
@@ -157,8 +157,23 @@ public class MapboxNavigationModule: Module {
157
157
  }
158
158
  }
159
159
 
160
+ private func configuredMapboxPublicToken() -> String? {
161
+ guard let raw = Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String else {
162
+ return nil
163
+ }
164
+ let token = raw.trimmingCharacters(in: .whitespacesAndNewlines)
165
+ guard token.hasPrefix("pk."), token.count > 20 else {
166
+ return nil
167
+ }
168
+ return token
169
+ }
170
+
160
171
  private func startNavigation(options: NavigationStartOptions, promise: Promise) {
161
172
  guard let destination = options.destination.toCLLocationCoordinate2D() else {
173
+ self.emitErrorAndShowScreen([
174
+ "code": "INVALID_COORDINATES",
175
+ "message": "Invalid coordinates provided"
176
+ ])
162
177
  promise.reject("INVALID_COORDINATES", "Invalid coordinates provided")
163
178
  return
164
179
  }
@@ -184,6 +199,10 @@ public class MapboxNavigationModule: Module {
184
199
  promise: promise
185
200
  )
186
201
  case .failure(let error):
202
+ self.emitErrorAndShowScreen([
203
+ "code": "CURRENT_LOCATION_UNAVAILABLE",
204
+ "message": error.localizedDescription
205
+ ])
187
206
  promise.reject("CURRENT_LOCATION_UNAVAILABLE", error.localizedDescription)
188
207
  }
189
208
  }
@@ -195,7 +214,16 @@ public class MapboxNavigationModule: Module {
195
214
  options: NavigationStartOptions,
196
215
  promise: Promise
197
216
  ) {
198
-
217
+ guard configuredMapboxPublicToken() != nil else {
218
+ let message = "Missing or invalid MBXAccessToken. Add the package plugin to app.json and set EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN before prebuild."
219
+ self.emitErrorAndShowScreen([
220
+ "code": "MISSING_ACCESS_TOKEN",
221
+ "message": message
222
+ ])
223
+ promise.reject("MISSING_ACCESS_TOKEN", message)
224
+ return
225
+ }
226
+
199
227
  var waypoints = [Waypoint(coordinate: origin)]
200
228
 
201
229
  // Add intermediate waypoints if provided
@@ -225,6 +253,10 @@ public class MapboxNavigationModule: Module {
225
253
  switch result {
226
254
  case .success(let response):
227
255
  guard response.routes?.first != nil else {
256
+ self.emitErrorAndShowScreen([
257
+ "code": "NO_ROUTE",
258
+ "message": "No route found"
259
+ ])
228
260
  promise.reject("NO_ROUTE", "No route found")
229
261
  return
230
262
  }
@@ -272,11 +304,20 @@ public class MapboxNavigationModule: Module {
272
304
  promise.resolve(nil)
273
305
  }
274
306
  } else {
307
+ self.emitErrorAndShowScreen([
308
+ "code": "NO_ROOT_VC",
309
+ "message": "Could not find root view controller"
310
+ ])
275
311
  promise.reject("NO_ROOT_VC", "Could not find root view controller")
276
312
  }
277
313
 
278
314
  case .failure(let error):
279
- promise.reject("ROUTE_ERROR", error.localizedDescription)
315
+ let (code, message) = self.mapDirectionsError(error)
316
+ self.emitErrorAndShowScreen([
317
+ "code": code,
318
+ "message": message
319
+ ])
320
+ promise.reject(code, message)
280
321
  }
281
322
  }
282
323
  }
@@ -292,6 +333,42 @@ public class MapboxNavigationModule: Module {
292
333
  }
293
334
  }
294
335
 
336
+ private func emitErrorAndShowScreen(_ payload: [String: Any]) {
337
+ sendEvent("onError", payload)
338
+
339
+ guard let navVC = navigationViewController else {
340
+ return
341
+ }
342
+
343
+ if navVC.presentingViewController == nil {
344
+ navigationViewController = nil
345
+ isCurrentlyNavigating = false
346
+ return
347
+ }
348
+
349
+ navVC.dismiss(animated: true) {
350
+ self.navigationViewController = nil
351
+ self.isCurrentlyNavigating = false
352
+ }
353
+ }
354
+
355
+ private func mapDirectionsError(_ error: Error) -> (String, String) {
356
+ let message = error.localizedDescription
357
+ let lowered = message.lowercased()
358
+
359
+ if lowered.contains("401") || lowered.contains("unauthorized") {
360
+ return ("MAPBOX_TOKEN_INVALID", "Route fetch failed: unauthorized. Check EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN and token scopes.")
361
+ }
362
+ if lowered.contains("403") || lowered.contains("forbidden") {
363
+ return ("MAPBOX_TOKEN_FORBIDDEN", "Route fetch failed: access forbidden. Verify token scopes and account permissions.")
364
+ }
365
+ if lowered.contains("429") || lowered.contains("rate") {
366
+ return ("MAPBOX_RATE_LIMITED", "Route fetch failed: rate limited by Mapbox.")
367
+ }
368
+
369
+ return ("ROUTE_ERROR", message)
370
+ }
371
+
295
372
  private func stopNavigation(promise: Promise) {
296
373
  guard let navVC = navigationViewController else {
297
374
  promise.resolve(nil)
@@ -611,3 +688,67 @@ private final class CurrentLocationResolver: NSObject, CLLocationManagerDelegate
611
688
  completion(result)
612
689
  }
613
690
  }
691
+
692
+ private final class NavigationErrorViewController: UIViewController {
693
+ private let message: String
694
+
695
+ init(message: String) {
696
+ self.message = message
697
+ super.init(nibName: nil, bundle: nil)
698
+ }
699
+
700
+ required init?(coder: NSCoder) {
701
+ nil
702
+ }
703
+
704
+ override func viewDidLoad() {
705
+ super.viewDidLoad()
706
+
707
+ view.backgroundColor = UIColor(red: 11 / 255, green: 16 / 255, blue: 32 / 255, alpha: 1)
708
+
709
+ let titleLabel = UILabel()
710
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
711
+ titleLabel.text = "Navigation Error"
712
+ titleLabel.textColor = .white
713
+ titleLabel.font = UIFont.systemFont(ofSize: 28, weight: .bold)
714
+ titleLabel.textAlignment = .center
715
+ titleLabel.numberOfLines = 0
716
+
717
+ let messageLabel = UILabel()
718
+ messageLabel.translatesAutoresizingMaskIntoConstraints = false
719
+ messageLabel.text = message
720
+ messageLabel.textColor = UIColor(red: 214 / 255, green: 228 / 255, blue: 255 / 255, alpha: 1)
721
+ messageLabel.font = UIFont.systemFont(ofSize: 16, weight: .regular)
722
+ messageLabel.textAlignment = .center
723
+ messageLabel.numberOfLines = 0
724
+
725
+ let closeButton = UIButton(type: .system)
726
+ closeButton.translatesAutoresizingMaskIntoConstraints = false
727
+ closeButton.setTitle("Back", for: .normal)
728
+ closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
729
+ closeButton.backgroundColor = UIColor.white.withAlphaComponent(0.15)
730
+ closeButton.setTitleColor(.white, for: .normal)
731
+ closeButton.layer.cornerRadius = 10
732
+ closeButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
733
+ closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
734
+
735
+ let stack = UIStackView(arrangedSubviews: [titleLabel, messageLabel, closeButton])
736
+ stack.translatesAutoresizingMaskIntoConstraints = false
737
+ stack.axis = .vertical
738
+ stack.alignment = .fill
739
+ stack.spacing = 20
740
+
741
+ view.addSubview(stack)
742
+
743
+ NSLayoutConstraint.activate([
744
+ stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
745
+ stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
746
+ stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
747
+ ])
748
+ }
749
+
750
+ @objc
751
+ private func closeTapped() {
752
+ dismiss(animated: true)
753
+ }
754
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlab/react-native-mapbox-navigation",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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 = path.resolve(moduleDir, "..", "..");
9
- const androidDir = path.join(repoRoot, "android");
10
- const envFilePath = path.join(repoRoot, ".env");
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 || "/tmp/gradle-user-home-react-native-mapbox-navigation",
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(`\nSkipping optional Gradle check due to restricted/offline environment: ${command}`);
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
- run("npx tsc --noEmit", repoRoot);
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("\n> Skipping Android compile check (android/gradlew not found).");
120
+ console.log(
121
+ "\n> Skipping Android compile check (android/gradlew not found).",
122
+ );
111
123
  }
112
124
 
113
- run("npm pack --dry-run --cache /tmp/npm-cache-react-native-mapbox-navigation", moduleDir);
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
- // Event callbacks
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
  }