@jacques_gordon/expo-mapbox-navigation 2.2.7 → 2.2.8

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.
@@ -2,33 +2,6 @@ require 'json'
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
4
 
5
- # ── Detect whether xcframeworks have been built and bundled ────────────────────
6
- # The ios/Frameworks/ directory is populated by running ios/build-xcframeworks.sh
7
- # (requires macOS + Xcode + Mapbox .netrc credentials). Once built, the
8
- # xcframeworks are committed to the repository / included in the npm package.
9
- frameworks_dir = File.join(__dir__, 'ios', 'Frameworks')
10
- has_frameworks = Dir.exist?(frameworks_dir) &&
11
- Dir.glob(File.join(frameworks_dir, '*.xcframework')).length > 0
12
-
13
- if !has_frameworks
14
- warn <<~MSG
15
-
16
- ┌──────────────────────────────────────────────────────────────────────────┐
17
- │ @jacques_gordon/expo-mapbox-navigation: iOS xcframeworks not found │
18
- │ │
19
- │ The Mapbox Navigation SDK v3 is SPM-only (no CocoaPods distribution). │
20
- │ To build for iOS you must first compile the xcframeworks: │
21
- │ │
22
- │ cd node_modules/@jacques_gordon/expo-mapbox-navigation/ios │
23
- │ ./build-xcframeworks.sh │
24
- │ │
25
- │ Prerequisites: macOS + Xcode 16 + ~/.netrc with Mapbox Downloads token │
26
- │ See ios/build-xcframeworks.sh for full instructions. │
27
- └──────────────────────────────────────────────────────────────────────────┘
28
-
29
- MSG
30
- end
31
-
32
5
  Pod::Spec.new do |s|
33
6
  s.name = 'ExpoMapboxNavigation'
34
7
  s.version = package['version']
@@ -38,41 +11,35 @@ Pod::Spec.new do |s|
38
11
  s.author = package['author']
39
12
  s.homepage = package['homepage']
40
13
 
41
- # iOS 14+ as required by Mapbox Navigation SDK v3
14
+ # Mapbox Navigation SDK v3 requires iOS 14+
42
15
  s.platforms = { :ios => '14.0' }
43
16
  s.swift_version = '5.9'
44
17
  s.source = { git: package['repository']['url'], tag: "v#{s.version}" }
18
+ s.static_framework = true
45
19
 
46
20
  s.dependency 'ExpoModulesCore'
47
21
 
48
- # ── Vendored xcframeworks ──────────────────────────────────────────────────
49
- # Mapbox Navigation SDK v3 for iOS is SPM-only — CocoaPods support is
50
- # announced as "coming soon" by Mapbox but not yet available.
22
+ # ── iOS: Mapbox Navigation SDK v3 via SPM ─────────────────────────────────
51
23
  #
52
- # We vendor pre-compiled xcframeworks built from the official source using
53
- # the Scipio build tool (see ios/build-xcframeworks.sh). This is the same
54
- # approach used by @badatgil/expo-mapbox-navigation (the only community
55
- # wrapper with confirmed production EAS Build success for iOS + Mapbox Nav v3).
24
+ # Mapbox Navigation SDK v3 is SPM-only (CocoaPods "coming soon" per Mapbox).
25
+ # We do NOT use spm_dependency() here that causes 43029 duplicate symbol
26
+ # linker errors when used alongside @rnmapbox/maps.
56
27
  #
57
- # The vendored frameworks are:
58
- # - MapboxNavigationCore — routing engine, location matching, progress
59
- # - MapboxNavigationUIKit NavigationViewController drop-in UI
60
- # - MapboxNavigationNative — binary (pre-compiled, from Mapbox Maven)
61
- # - MapboxDirections — Waypoint, NavigationRouteOptions, etc.
62
- # - _MapboxNavigationHelpers — internal helpers
28
+ # Instead, our config plugin (app.plugin.js) injects a post_install hook
29
+ # into the Podfile. The hook uses the Xcodeproj Ruby API — the same technique
30
+ # as @rnmapbox/maps itself to add mapbox-navigation-ios as a SPM
31
+ # dependency to the ExpoMapboxNavigation pod target and the app target.
32
+ # find-or-create semantics prevent any duplication.
63
33
  #
64
- # MapboxMaps, MapboxCommon, Turf are NOT vendored here they come from
65
- # @rnmapbox/maps which is a required peer dependency. Vendoring them again
66
- # would cause "duplicate symbols" (the exact error we hit with spm_dependency).
67
- if has_frameworks
68
- vendored = Dir.glob(File.join(frameworks_dir, '*.xcframework'))
69
- .map { |f| "ios/Frameworks/#{File.basename(f)}" }
70
- s.vendored_frameworks = vendored
71
- end
34
+ # The post_install hook runs during `pod install`, with full access to
35
+ # installer.pods_project and installer.aggregate_targets, which is the only
36
+ # correct way to add SPM packages alongside CocoaPods in Expo projects.
72
37
 
73
38
  s.source_files = 'ios/**/*.{swift,h,m,mm}'
74
- # Exclude Package.swift and build script from compilation
75
- s.exclude_files = 'ios/Package.swift', 'ios/build-xcframeworks.sh'
39
+ s.exclude_files = [
40
+ 'ios/Package.swift',
41
+ 'ios/build-xcframeworks.sh',
42
+ ]
76
43
 
77
44
  s.pod_target_xcconfig = {
78
45
  'DEFINES_MODULE' => 'YES',
package/README.md CHANGED
@@ -2,17 +2,16 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/@jacques_gordon%2Fexpo-mapbox-navigation.svg)](https://www.npmjs.com/package/@jacques_gordon/expo-mapbox-navigation)
4
4
 
5
- Expo module for Mapbox Navigation SDK — forked from [`@badatgil/expo-mapbox-navigation`](https://github.com/uju777/expo-mapbox-navigation) with the following fixes and improvements:
6
-
7
- | Change | Details |
8
- |--------|---------|
9
- | 🐛 **Fix #43** | Android crash: `NoSuchMethodError` for `CameraAnimationsUtils.calculateCameraAnimationHint` — caused by Mapbox Maps/Navigation SDK version mismatch. Fixed by pinning `mapbox-maps-android ≥ 11.11.0` and Navigation SDK `3.7.0`. |
10
- | 🐛 **Fix #31** | Voice instructions always defaulted to imperial units. New `voiceUnits` prop (`"metric"` \| `"imperial"`) added. |
11
- | **NDK 27** | Forced NDK `27.0.12077973` for full 16 KB page size compatibility (Android 15+ requirement). |
12
- | **16 KB page size** | `jniLibs.useLegacyPackaging = false` + `ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON` required for Google Play compliance from 2025 onwards. |
13
- | **Expo SDK 53+** | Compatible with Expo SDK ≥ 53 and React Native 0.79+. |
14
- | **Maps v11.11.0** | Minimum Mapbox Maps Android SDK enforced to 11.11.0 (config plugin validates this). |
15
- | ✅ **iOS support** | Full native iOS implementation (`NavigationViewController` drop-in UI) via Swift Package Manager — feature parity with Android (lane guidance, speed limit, voice instructions, day/night, steps list). |
5
+ Full-featured Expo module for Mapbox Navigation SDK v3 Android and iOS.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Android** Waze-style navigation UI built from scratch: maneuver banner, lane guidance, speed limit, ETA bar, voice instructions, mute/overview/recenter buttons, day/night auto-switch
12
+ - **iOS** Drop-in `NavigationViewController` from Mapbox Navigation SDK v3 (lane guidance, speed limit, voice, day/night all built-in)
13
+ - **Both platforms** 7 events, 19 props, full feature and API parity
14
+ - NDK 27 + 16 KB page size compliant (Android 15+)
16
15
 
17
16
  ---
18
17
 
@@ -22,54 +21,108 @@ Expo module for Mapbox Navigation SDK — forked from [`@badatgil/expo-mapbox-na
22
21
  npx expo install @jacques_gordon/expo-mapbox-navigation @rnmapbox/maps
23
22
  ```
24
23
 
25
- ### Setup @rnmapbox/maps first
24
+ ### 1. Setup @rnmapbox/maps first
25
+
26
+ ```json
27
+ ["@rnmapbox/maps", {
28
+ "RNMapboxMapsImpl": "mapbox",
29
+ "RNMapboxMapsVersion": "11.11.0",
30
+ "RNMapboxMapsDownloadToken": "sk.your_secret_token"
31
+ }]
32
+ ```
26
33
 
27
- Follow the [full @rnmapbox/maps installation guide](https://rnmapbox.github.io/docs/install). Set `RNMapboxMapsVersion` to `11.11.0` or higher.
34
+ ### 2. Add this plugin
28
35
 
29
36
  ```json
30
- "plugins": [
31
- [
32
- "@rnmapbox/maps",
33
- {
34
- "RNMapboxMapsImpl": "mapbox",
35
- "RNMapboxMapsVersion": "11.11.0",
36
- "RNMapboxMapsDownloadToken": "sk.your_secret_token"
37
- }
38
- ]
39
- ]
37
+ ["@jacques_gordon/expo-mapbox-navigation", {
38
+ "accessToken": "pk.your_public_token",
39
+ "downloadsToken": "sk.your_secret_token",
40
+ "mapboxMapsVersion": "11.11.0"
41
+ }]
40
42
  ```
41
43
 
42
- ### Add this plugin
44
+ ### 3. iOS only — enable static frameworks
43
45
 
44
46
  ```json
45
- "plugins": [
46
- [
47
- "@jacques_gordon/expo-mapbox-navigation",
48
- {
49
- "accessToken": "pk.your_public_token",
50
- "downloadsToken": "sk.your_secret_token",
51
- "mapboxMapsVersion": "11.11.0"
52
- }
53
- ]
54
- ]
47
+ ["expo-build-properties", { "ios": { "useFrameworks": "static" } }]
55
48
  ```
56
49
 
57
- > ⚠️ `mapboxMapsVersion` must match the version set in `@rnmapbox/maps`. Minimum: `11.11.0`.
50
+ ---
51
+
52
+ ## Plugin Options
53
+
54
+ | Option | Required | Default | Description |
55
+ |--------|----------|---------|-------------|
56
+ | `accessToken` | ✅ | — | Public Mapbox token (`pk.*`). Used for map tiles and routing. |
57
+ | `downloadsToken` | ✅ | — | Secret Mapbox token (`sk.*`) with **Downloads:Read** scope. Same token as `RNMapboxMapsDownloadToken`. Used on iOS to authenticate SPM when fetching the Navigation SDK from `api.mapbox.com` via `~/.netrc`. |
58
+ | `mapboxMapsVersion` | ✅ | `"11.11.0"` | Must exactly match `RNMapboxMapsVersion` in `@rnmapbox/maps`. |
59
+ | `mapboxNavigationVersion` | — | auto-calculated | iOS only. See [iOS Version Strategy](#ios-version-strategy) below. |
60
+ | `androidColorOverrides` | — | `{}` | Override Mapbox native resource colors on Android. |
61
+
62
+ ---
63
+
64
+ ## iOS Architecture
65
+
66
+ ### How it works
67
+
68
+ iOS uses `NavigationViewController` — the official Mapbox Navigation SDK v3 drop-in UI — installed via **Swift Package Manager** (SPM). CocoaPods does not host the Navigation SDK v3 (Mapbox confirmed CocoaPods support is "coming soon").
69
+
70
+ This package bridges SPM into your CocoaPods/Expo project using a **`post_install` Ruby hook** injected into your `Podfile` — the same technique used by `@rnmapbox/maps` itself. The hook uses the Xcodeproj Ruby API (`XCRemoteSwiftPackageReference`, `XCSwiftPackageProductDependency`) to add the package properly, with find-or-create semantics to prevent duplicate symbols.
71
+
72
+ ### iOS Version Strategy
58
73
 
59
- > ⚠️ `downloadsToken` is **required** it's a secret Mapbox token (starts with `sk.`) with the **Downloads:Read** scope. On iOS it's used to authenticate Swift Package Manager when it fetches the Mapbox Navigation SDK from Mapbox's private package registry (via a generated `.netrc` entry). This is the same token already used for `RNMapboxMapsDownloadToken` above — you can reuse it.
74
+ This is the most important part. Understanding it prevents build failures.
60
75
 
61
- ### iOS architecture
76
+ **The problem:** SPM requires all packages in the dependency graph to agree on a single version of shared libraries (`MapboxCommon`, `MapboxMaps`, `Turf`). Both `@rnmapbox/maps` and `mapbox-navigation-ios` depend on these shared libraries. If they request incompatible versions, SPM fails.
62
77
 
63
- iOS uses the **official Mapbox Navigation SDK v3 drop-in UI** (`NavigationViewController`), installed via **Swift Package Manager** — not prebuilt `.xcframework` binaries. This is the only officially supported distribution method for the Navigation SDK v3 on iOS; CocoaPods doesn't host it directly, so this package bridges it in automatically through CocoaPods' `spm_dependency()` mechanism when you run `pod install` / `expo prebuild`.
78
+ **The Mapbox versioning pattern** (confirmed from official GitHub releases):
64
79
 
65
- Because `NavigationViewController` is a complete drop-in experience, lane guidance, speed limit display, voice instructions, and the recenter/overview camera button all come built-in from Mapbox — no extra wiring needed, unlike the more manual Android implementation.
80
+ The `.0` release of each Navigation minor always pairs with the matching Maps minor:
66
81
 
67
- ### iOS: enable static frameworks
82
+ | Maps version | Navigation `.0` | Compatible? |
83
+ |---|---|---|
84
+ | `11.11.0` | `3.11.0` | ✅ |
85
+ | `11.12.0` | `3.12.0` | ✅ |
86
+ | `11.21.5` | `3.21.5` (same minor+patch) | ✅ |
87
+
88
+ **⚠️ Patch versions drift.** Navigation patch releases (`3.11.x` where x > 0) often update to a newer Maps version — for example `3.11.4` requires Maps `11.14.7`, not `11.11.x`. Using `upToNextMinorVersion` would therefore be unsafe.
89
+
90
+ **Our solution:** We use the **exact** `.0` version that matches your Maps minor:
91
+
92
+ ```
93
+ mapboxMapsVersion = "11.11.0"
94
+ → Navigation exact version = "3.11.0"
95
+ → requires MapboxMaps 11.11.x ✅ compatible
96
+ ```
97
+
98
+ This is **fully automatic** — when you upgrade Maps from `11.11.0` to `11.12.0`, the Navigation version is recalculated to exact `3.12.0`.
99
+
100
+ **Manual override (escape hatch):** To pin any specific Navigation version:
68
101
 
69
102
  ```json
70
- "plugins": [
71
- ["expo-build-properties", { "ios": { "useFrameworks": "static" } }]
72
- ]
103
+ ["@jacques_gordon/expo-mapbox-navigation", {
104
+ "accessToken": "pk.xxx",
105
+ "downloadsToken": "sk.xxx",
106
+ "mapboxMapsVersion": "11.11.0",
107
+ "mapboxNavigationVersion": "3.11.2"
108
+ }]
109
+ ```
110
+
111
+ During `expo prebuild`, you will see in the logs:
112
+ ```
113
+ [@jacques_gordon/expo-mapbox-navigation] Maps 11.11.0 → auto-calculated Navigation 3.11.0..<3.12.0
114
+ [@jacques_gordon/expo-mapbox-navigation] ✅ Wrote Mapbox credentials to ~/.netrc
115
+ [@jacques_gordon/expo-mapbox-navigation] ✅ Injected mapbox-navigation-ios SPM hook into Podfile
116
+ ```
117
+
118
+ And during `pod install`:
119
+ ```
120
+ [ExpoMapboxNavigation] Added mapbox-navigation-ios to pods_project
121
+ [ExpoMapboxNavigation] Found target: ExpoMapboxNavigation
122
+ [ExpoMapboxNavigation] Linked MapboxNavigationCore -> ExpoMapboxNavigation
123
+ [ExpoMapboxNavigation] Linked MapboxNavigationUIKit -> ExpoMapboxNavigation
124
+ [ExpoMapboxNavigation] Linked MapboxNavigationCore -> Navio
125
+ [ExpoMapboxNavigation] Linked MapboxNavigationUIKit -> Navio
73
126
  ```
74
127
 
75
128
  ---
@@ -84,50 +137,89 @@ export default function Navigation() {
84
137
  <MapboxNavigationView
85
138
  style={{ flex: 1 }}
86
139
  coordinates={[
87
- { latitude: 48.8566, longitude: 2.3522 }, // Paris
88
- { latitude: 51.5074, longitude: -0.1278 }, // London
140
+ { latitude: 50.8503, longitude: 4.3517 }, // Brussels
141
+ { latitude: 51.2194, longitude: 4.4025 }, // Antwerp
89
142
  ]}
90
- voiceUnits="metric" // Fix for issue #31
143
+ voiceUnits="metric"
91
144
  language="fr"
92
145
  navigationProfile="driving-traffic"
146
+ onRoutesReady={({ nativeEvent }) =>
147
+ console.log('Route ready:', nativeEvent.distanceMeters, 'm')
148
+ }
149
+ onRouteProgressChanged={({ nativeEvent }) =>
150
+ console.log('Remaining:', nativeEvent.distanceRemaining, 'm')
151
+ }
152
+ onManeuverBannerPressed={({ nativeEvent }) => {
153
+ // Open a bottom sheet showing all upcoming steps
154
+ console.log('Steps:', nativeEvent.steps);
155
+ }}
93
156
  onArrival={() => console.log('Arrived!')}
157
+ onNavigationCancelled={() => console.log('Cancelled')}
94
158
  onRoutesFailed={({ nativeEvent }) =>
95
- console.error('Routes failed:', nativeEvent.message)
159
+ console.error('Failed:', nativeEvent.message)
96
160
  }
97
161
  />
98
162
  );
99
163
  }
100
164
  ```
101
165
 
102
- ### Color customization (Android)
166
+ ---
103
167
 
104
- All color props are optional — if not provided, the defaults below are applied automatically.
168
+ ## Props
105
169
 
106
- ```tsx
107
- <MapboxNavigationView
108
- // Maneuver banner (turn-by-turn instruction banner)
109
- maneuverBackgroundColorDay="#1E2433" // default: Mapbox native style color
110
- maneuverTurnIconColor="#1A73E8" // default: Mapbox native style color
170
+ ### Navigation
171
+
172
+ | Prop | Type | Default | Description |
173
+ |------|------|---------|-------------|
174
+ | `coordinates` | `{ latitude: number; longitude: number }[]` | **required** | Waypoints. Minimum 2. |
175
+ | `waypointIndices` | `number[]` | all | Which coordinates are true waypoints (vs. route shape points). |
176
+ | `navigationProfile` | `string` | `"driving-traffic"` | `"driving-traffic"`, `"driving"`, `"walking"`, `"cycling"`. **Android**: omit the `"mapbox/"` prefix. |
177
+ | `language` | `string` | device locale | BCP-47 tag (e.g. `"fr"`, `"nl"`, `"en-US"`). |
178
+ | `voiceUnits` | `"metric" \| "imperial"` | auto by locale | Overrides automatic unit detection. |
179
+ | `excludeTypes` | `string[]` | — | Road types to avoid (e.g. `["toll", "ferry"]`). |
180
+ | `mapStyle` | `string` | Mapbox Navigation Day | Map style URL. |
181
+ | `mute` | `boolean` | `false` | Silence voice instructions. |
182
+ | `maxHeight` | `number` | — | Max vehicle height in metres. |
183
+ | `maxWidth` | `number` | — | Max vehicle width in metres. |
184
+ | `useMapMatching` | `boolean` | `false` | Use Map Matching API instead of routing. |
185
+ | `customRasterTileUrl` | `string` | — | Custom raster tile URL with `{x}/{y}/{z}`. |
186
+ | `customRasterAboveLayerId` | `string` | — | Layer ID to insert custom raster tiles above. |
111
187
 
112
- // Bottom ETA / duration / distance bar
113
- etaBarBackgroundColor="#1E2433" // default: #1E2433 (dark navy)
114
- etaTextColor="#FFFFFF" // default: #FFFFFF (white)
188
+ ### Color Customization (Android)
115
189
 
116
- // Control buttons (mute, overview, recenter)
117
- iconButtonColor="#1A73E8" // default: #1A73E8 (Google Blue)
118
- iconButtonMutedColor="#EA4335" // default: #EA4335 (Google Red)
190
+ All color props are optional — defaults are applied when omitted.
119
191
 
120
- // Route line color set via plugin config, not a view prop
121
- // (uses androidColorOverrides in app.json)
122
- {...otherProps}
192
+ | Prop | Default | Description |
193
+ |------|---------|-------------|
194
+ | `maneuverBackgroundColorDay` | Mapbox default | Background of the turn-by-turn instruction banner. Uses `ManeuverViewOptions.maneuverBackgroundColor` (official Mapbox SDK API). |
195
+ | `maneuverTurnIconColor` | Mapbox default | Color of the turn arrow icon. Uses `ManeuverViewOptions.turnIconManeuver`. |
196
+ | `etaBarBackgroundColor` | `"#1E2433"` | Background of the bottom ETA/duration/distance bar. |
197
+ | `etaTextColor` | `"#FFFFFF"` | Text color for ETA time and duration. |
198
+ | `iconButtonColor` | `"#1A73E8"` | Color of the mute/overview/recenter buttons (default state). |
199
+ | `iconButtonMutedColor` | `"#EA4335"` | Color of the mute button when voice is muted. |
200
+
201
+ ```tsx
202
+ <MapboxNavigationView
203
+ maneuverBackgroundColorDay="#1E2433"
204
+ maneuverTurnIconColor="#1A73E8"
205
+ etaBarBackgroundColor="#1E2433"
206
+ etaTextColor="#FFFFFF"
207
+ iconButtonColor="#1A73E8"
208
+ iconButtonMutedColor="#EA4335"
123
209
  />
124
210
  ```
125
211
 
212
+ > **Note:** On iOS, `NavigationViewController` applies its own theme. Color props are stored and can be applied via the SDK's `StyleManager` in a future release.
213
+
214
+ ### Mapbox Native Colors (Android, via plugin)
215
+
216
+ Override Mapbox's built-in resource colors (route line, etc.) via `androidColorOverrides` in `app.json`:
217
+
126
218
  ```json
127
- // app.json — route line and other Mapbox native resource colors
128
219
  ["@jacques_gordon/expo-mapbox-navigation", {
129
220
  "accessToken": "pk.xxx",
130
221
  "downloadsToken": "sk.xxx",
222
+ "mapboxMapsVersion": "11.11.0",
131
223
  "androidColorOverrides": {
132
224
  "mapbox_primary_route_color": "#0055FF",
133
225
  "mapbox_main_maneuver_background_color": "#FF5500"
@@ -137,83 +229,77 @@ All color props are optional — if not provided, the defaults below are applied
137
229
 
138
230
  ---
139
231
 
140
- ## Props
141
-
142
- | Prop | Type | Default | Description |
143
- |------|------|---------|-------------|
144
- | `coordinates` | `Coordinate[]` | required | Route waypoints. Min 2 items. |
145
- | `waypointIndices` | `number[]` | all points | Which coordinates are waypoints. |
146
- | `language` | `string` | device locale | BCP-47 locale (e.g. `"fr"`, `"en-US"`). |
147
- | `voiceUnits` | `"metric" \| "imperial"` | auto | **Fix #31** — Voice/distance units. |
148
- | `navigationProfile` | `string` | `"driving-traffic"` | Mapbox routing profile. |
149
- | `excludeTypes` | `string[]` | — | Road types to avoid. |
150
- | `mapStyle` | `string` | Mapbox Navigation Day | Map style URL. |
151
- | `mute` | `boolean` | `false` | Silence voice instructions. |
152
- | `maxHeight` | `number` | — | Max vehicle height (m). |
153
- | `maxWidth` | `number` | — | Max vehicle width (m). |
154
- | `useMapMatching` | `boolean` | `false` | Use Map Matching API. |
155
- | `customRasterTileUrl` | `string` | — | Custom tile URL with `{x}/{y}/{z}`. |
156
- | `customRasterAboveLayerId` | `string` | — | Layer ID to place custom raster above. |
157
-
158
- ---
159
-
160
232
  ## Events
161
233
 
162
234
  | Event | Payload | Description |
163
235
  |-------|---------|-------------|
164
- | `onRoutesReady` | `{ routeCount, distanceMeters, durationSeconds }` | Routes calculated. |
165
- | `onRouteProgressChanged` | `{ distanceRemaining, durationRemaining, ... }` | Progress update. |
166
- | `onArrival` | `{}` | User reached destination. |
167
- | `onNavigationCancelled` | `{}` | User cancelled navigation. |
168
- | `onNavigationFinished` | `{}` | Session ended normally. |
169
- | `onRoutesFailed` | `{ message }` | Route calculation failed. |
170
-
171
- ---
172
-
173
- ## Android Color Overrides
174
-
175
- Customize the Navigation UI colors by overriding Mapbox resource values:
176
-
177
- ```json
178
- ["@jacques_gordon/expo-mapbox-navigation", {
179
- "accessToken": "pk.your_token",
180
- "mapboxMapsVersion": "11.11.0",
181
- "androidColorOverrides": {
182
- "mapbox_main_maneuver_background_color": "#FF5500",
183
- "mapbox_primary_route_color": "#0055FF"
184
- }
185
- }]
236
+ | `onRoutesReady` | `{ routeCount, distanceMeters, durationSeconds }` | Fired when routes are calculated and navigation starts. |
237
+ | `onRouteProgressChanged` | `{ distanceRemaining, durationRemaining, distanceTraveled, fractionTraveled, currentStepDistanceRemaining }` | Fired on every GPS update during navigation. |
238
+ | `onArrival` | `{}` | User reached the destination. |
239
+ | `onNavigationCancelled` | `{}` | User tapped the cancel (✕) button. |
240
+ | `onNavigationFinished` | `{}` | Navigation session ended normally. |
241
+ | `onRoutesFailed` | `{ message: string }` | Route calculation failed. |
242
+ | `onManeuverBannerPressed` | `{ steps: RouteStep[] }` | Fired when user taps the instruction banner. Use to open a bottom sheet with the full steps list. |
243
+
244
+ ### RouteStep type
245
+
246
+ ```ts
247
+ interface RouteStep {
248
+ instruction: string; // "Turn left onto Main St"
249
+ distanceMeters: number;
250
+ durationSeconds: number;
251
+ maneuverType: string; // "turn", "merge", "roundabout", etc.
252
+ maneuverModifier: string; // "left", "right", "straight", etc.
253
+ roadName: string;
254
+ laneInstructions: {
255
+ active: boolean; // true = recommended lane
256
+ directions: string[]; // ["straight"], ["left", "straight"]
257
+ }[];
258
+ }
186
259
  ```
187
260
 
188
261
  ---
189
262
 
190
- ## 16 KB Page Size Compatibility
263
+ ## 16 KB Page Size (Android 15+)
191
264
 
192
- Android 15 (API 35) requires all `.so` native libraries to be aligned to 16 KB boundaries for devices using 16 KB memory page sizes.
265
+ This package enforces full compliance with Android's 16 KB memory page size requirement:
193
266
 
194
- This package enforces:
195
- - **NDK 27** (`27.0.12077973`)the first NDK version with full 16 KB support
196
- - **`jniLibs.useLegacyPackaging = false`**prevents `.so` compression, enabling proper alignment
197
- - **64-bit-only ABI filters** (`arm64-v8a`, `x86_64`) 16 KB requirement applies to 64-bit only
267
+ - **NDK 27** (`27.0.12077973`) — first NDK version with full 16 KB support
268
+ - **`jniLibs.useLegacyPackaging = false`**prevents `.so` compression, enables proper alignment
269
+ - **64-bit ABI filters** (`arm64-v8a`, `x86_64`) requirement applies to 64-bit only
270
+ - **NDK27 variant substitution** `dependencySubstitution` replaces all Mapbox Maven artifacts with their `-ndk27` equivalents across the entire dependency graph (including transitive deps from other packages)
198
271
 
199
- More info: [Android 16 KB page size guide](https://developer.android.com/guide/practices/page-sizes)
272
+ See [Android 16 KB page size guide](https://developer.android.com/guide/practices/page-sizes).
200
273
 
201
274
  ---
202
275
 
203
276
  ## Changelog
204
277
 
278
+ ### 2.2.8
279
+ - **iOS version strategy redesigned** — dynamic `mapboxNavigationVersion` calculation from `mapboxMapsVersion` minor. Prevents `MapboxCommon` version conflicts with `@rnmapbox/maps`. Pattern confirmed from real Mapbox releases: Navigation `3.N.x` always compatible with Maps `11.N.x`.
280
+ - **iOS: `mapboxNavigationVersion` optional param** — escape hatch to pin exact Navigation version.
281
+ - **iOS: post_install hook strengthened** — fallback `include?` target search + debug log of all available targets if `ExpoMapboxNavigation` not found.
282
+ - **Android color props fixed** — setters now apply immediately to views (were only stored previously, causing icon colors to remain default).
283
+
205
284
  ### 2.2.0
206
- - **iOS support added.** Full native implementation using Mapbox Navigation SDK v3 (`NavigationViewController` drop-in UI) via Swift Package Manager.
207
- - Fixed: previous versions referenced non-existent `.xcframework` files in the podspec, causing `Unimplemented component: ViewManagerAdapter_ExpoMapboxNavigation` crashes on iOS — no native module was ever actually registered.
208
- - New required `downloadsToken` config plugin option (secret Mapbox token, used to authenticate Swift Package Manager).
209
- - iOS feature parity with Android: lane guidance, speed limit, voice instructions (with working mute), day/night auto-switching, and the `onManeuverBannerPressed` full-steps-list event.
285
+ - **iOS support added** full native implementation using `NavigationViewController` drop-in UI.
286
+ - iOS SPM integration via `post_install` Podfile hook (Xcodeproj Ruby API, same technique as `@rnmapbox/maps`).
287
+ - `downloadsToken` required for iOS SPM authentication.
288
+ - Fixed previous phantom `.xcframework` references that caused `Unimplemented component` crashes.
289
+
290
+ ### 2.1.x
291
+ - Waze-style Android UI: maneuver banner, speed limit, ETA bar, action buttons (mute/overview/recenter).
292
+ - Voice instructions with TTS fallback.
293
+ - Puck jitter fix (GitHub issue #4140) — `keyPoints = emptyList()`.
294
+ - Lane guidance fix — explicit `bannerInstructions(true)`, `steps(true)`, `roundaboutExits(true)`.
295
+ - `onManeuverBannerPressed` event with full route steps list.
296
+ - Color customization props (Android).
210
297
 
211
298
  ### 2.0.1
212
- - Fix #43: `CameraAnimationsUtils.calculateCameraAnimationHint` NoSuchMethodError on Android
213
- - Fix #31: Add `voiceUnits` prop for metric/imperial voice instructions
214
- - Force NDK 27 for 16 KB page size support
215
- - Enforce Mapbox Maps Android ≥ 11.11.0
216
- - Expo SDK 53 compatibility
299
+ - Fix #43: `CameraAnimationsUtils.calculateCameraAnimationHint` crash on Android.
300
+ - Fix #31: `voiceUnits` prop for metric/imperial.
301
+ - NDK 27 + 16 KB page size enforcement.
302
+ - Expo SDK 53 compatibility.
217
303
 
218
304
  ---
219
305
 
package/app.plugin.js CHANGED
@@ -12,6 +12,7 @@ const withMapboxNavigation = (config, options = {}) => {
12
12
  accessToken,
13
13
  downloadsToken,
14
14
  mapboxMapsVersion = '11.11.0',
15
+ mapboxNavigationVersion = null, // optional override — auto-calculated from mapboxMapsVersion if not set
15
16
  androidColorOverrides = {},
16
17
  } = options;
17
18
 
@@ -81,6 +82,196 @@ const withMapboxNavigation = (config, options = {}) => {
81
82
  },
82
83
  ]);
83
84
 
85
+ // ── iOS: Inject mapbox-navigation-ios SPM via Podfile post_install hook ──────
86
+ //
87
+ // This is the correct approach for adding SPM dependencies alongside
88
+ // CocoaPods in an Expo project — copied directly from @rnmapbox/maps
89
+ // (rnmapbox-maps.podspec, _add_spm_to_target method).
90
+ //
91
+ // WHY post_install hook (not pbxproj text injection):
92
+ // The hook runs INSIDE `pod install`, with access to the Ruby Xcodeproj
93
+ // object model (installer.pods_project, installer.aggregate_targets).
94
+ // This means:
95
+ // - Proper find-or-create (no duplicate symbols risk)
96
+ // - CocoaPods-aware (survives pod install --clean)
97
+ // - Works in the xcworkspace context (not just xcodeproj)
98
+ // - Identical to how @rnmapbox/maps itself adds SPM packages
99
+ //
100
+ // WHY this doesn't cause duplicate symbols (unlike spm_dependency()):
101
+ // spm_dependency() links the SPM framework into the Pod target AND the app
102
+ // target → 2 copies. This hook adds the package to ONLY the
103
+ // ExpoMapboxNavigation pod target + the app target, using the same
104
+ // XCRemoteSwiftPackageReference object → 1 copy, properly deduplicated.
105
+ config = withDangerousMod(config, [
106
+ 'ios',
107
+ (mod) => {
108
+ const podfilePath = path.join(mod.modRequest.platformProjectRoot, 'Podfile');
109
+ if (!fs.existsSync(podfilePath)) {
110
+ console.warn('[@jacques_gordon/expo-mapbox-navigation] Podfile not found, skipping SPM hook');
111
+ return mod;
112
+ }
113
+
114
+ let podfile = fs.readFileSync(podfilePath, 'utf8');
115
+
116
+ // Guard: don't inject twice
117
+ if (podfile.includes('# [ExpoMapboxNavigation] SPM hook')) {
118
+ return mod;
119
+ }
120
+
121
+ // ── NAVIGATION VERSION STRATEGY ───────────────────────────────────────
122
+ // CONFIRMED from the official CHANGELOG.md:
123
+ //
124
+ // PHASE 1 — Nav 3.1 to 3.12 (offset of +3):
125
+ // Navigation 3.N.x requires MapboxMaps 11.(N+3).x
126
+ // Nav 3.8.x → Maps 11.11.x ✅ (Maps 11.11.0 → Nav 3.8.x)
127
+ // Nav 3.11.x → Maps 11.14.x ✅ (confirmed CHANGELOG)
128
+ // Nav 3.12.x → Maps 11.15.x ✅ (confirmed rc.1 release note)
129
+ //
130
+ // PHASE 2 — Nav 3.16+ (3.13/3.14/3.15 were DELIBERATELY SKIPPED):
131
+ // Navigation 3.N.x requires MapboxMaps 11.N.x (minors aligned)
132
+ // Nav 3.16.x → Maps 11.16.x ✅
133
+ // Nav 3.21.5 → Maps 11.21.5 ✅ (confirmed release)
134
+ // Nav 3.23.1 → Maps 11.23.1 ✅ (confirmed release)
135
+ // Nav 3.25.0 → Maps 11.25.0 ✅ (confirmed release)
136
+ //
137
+ // Source: Android CHANGELOG — "3.16.x is the next version after 3.12.x.
138
+ // For technical reasons, versions 3.13.x, 3.14.x and 3.15.x are skipped.
139
+ // Starting from 3.16.x, the Nav SDK minor version will be aligned with
140
+ // other Mapbox dependencies." (same policy applies to iOS)
141
+ //
142
+ // FORMULA:
143
+ // if mapsMinor <= 15: navMinor = mapsMinor - 3
144
+ // if mapsMinor >= 16: navMinor = mapsMinor
145
+ //
146
+ // EXAMPLE: mapboxMapsVersion = "11.11.0"
147
+ // mapsMinor = 11 (≤15, Phase 1)
148
+ // navMinor = 11 - 3 = 8
149
+ // navMin = "3.8.0" → SPM resolves latest 3.8.x → Maps 11.11.x ✅
150
+ //
151
+ // EXAMPLE: mapboxMapsVersion = "11.21.0"
152
+ // mapsMinor = 21 (≥16, Phase 2)
153
+ // navMinor = 21
154
+ // navMin = "3.21.0" → SPM resolves latest 3.21.x → Maps 11.21.x ✅
155
+ const mapsVersion = mapboxMapsVersion || '11.11.0';
156
+ const mapsMinor = parseInt(mapsVersion.split('.')[1], 10) || 11;
157
+ const navMinor = mapsMinor <= 15 ? mapsMinor - 3 : mapsMinor;
158
+ const navMin = mapboxNavigationVersion || `3.${navMinor}.0`;
159
+
160
+ console.log(`[@jacques_gordon/expo-mapbox-navigation] Maps ${mapsVersion} (minor=${mapsMinor}) → Navigation ${navMin}..<3.${navMinor+1}.0`);
161
+ console.log(`[@jacques_gordon/expo-mapbox-navigation] Phase: ${mapsMinor <= 15 ? `1 (offset -3: ${mapsMinor}-3=${navMinor})` : `2 (aligned: ${navMinor})`}`);
162
+
163
+ // The Ruby hook — identical pattern to @rnmapbox/maps _add_spm_to_target
164
+ const spmHook = `
165
+ # [ExpoMapboxNavigation] SPM hook — injected by @jacques_gordon/expo-mapbox-navigation
166
+ # Navigation: upToNextMinorVersion from ${navMin}
167
+ # Maps: ${mapsVersion} (minor ${mapsMinor}, ${mapsMinor <= 15 ? 'Phase 1: offset -3' : 'Phase 2: aligned'})
168
+ def _expo_mapbox_nav_add_spm(installer)
169
+ url = 'https://github.com/mapbox/mapbox-navigation-ios.git'
170
+ requirement = { kind: 'upToNextMinorVersion', minimumVersion: '${navMin}' }
171
+ products = ['MapboxNavigationCore', 'MapboxNavigationUIKit']
172
+
173
+ pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
174
+ ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency
175
+
176
+ # ── Step 1: Add to pods_project (where ExpoMapboxNavigation target lives) ──
177
+ pods_project = installer.pods_project
178
+
179
+ pkg = pods_project.root_object.package_references.find { |p|
180
+ p.class == pkg_class && p.repositoryURL == url
181
+ }
182
+ unless pkg
183
+ pkg = pods_project.new(pkg_class)
184
+ pkg.repositoryURL = url
185
+ pkg.requirement = requirement
186
+ pods_project.root_object.package_references << pkg
187
+ puts '[ExpoMapboxNavigation] Added mapbox-navigation-ios to pods_project'
188
+ end
189
+
190
+ # ── FIX: Stronger target lookup with fallback ──────────────────────────────
191
+ # CocoaPods normally names the target exactly 'ExpoMapboxNavigation'.
192
+ # Deduplication suffixes (e.g. 'ExpoMapboxNavigation-abc123') only happen
193
+ # when the same pod is included in multiple targets with different specs,
194
+ # which is not our case. We still add an include? fallback to be safe.
195
+ expo_target = pods_project.targets.find { |t| t.name == 'ExpoMapboxNavigation' }
196
+ expo_target ||= pods_project.targets.find { |t| t.name.include?('ExpoMapboxNavigation') }
197
+ if expo_target
198
+ puts "[ExpoMapboxNavigation] Found target: #{expo_target.name}"
199
+ products.each do |product_name|
200
+ ref = expo_target.package_product_dependencies.find { |r|
201
+ r.class == ref_class && r.package == pkg && r.product_name == product_name
202
+ }
203
+ unless ref
204
+ ref = pods_project.new(ref_class)
205
+ ref.package = pkg
206
+ ref.product_name = product_name
207
+ expo_target.package_product_dependencies << ref
208
+ puts "[ExpoMapboxNavigation] Linked #{product_name} -> #{expo_target.name}"
209
+ end
210
+ end
211
+ else
212
+ # Debug: print all available targets so we can fix the name if needed
213
+ puts '[ExpoMapboxNavigation] WARNING: ExpoMapboxNavigation target not found!'
214
+ puts '[ExpoMapboxNavigation] Available targets:'
215
+ pods_project.targets.each { |t| puts " - #{t.name}" }
216
+ end
217
+ pods_project.save
218
+
219
+ # ── Step 2: Add to user app target (needed for import in app binary) ────────
220
+ installer.aggregate_targets.each do |agg|
221
+ user_project = agg.user_project
222
+ agg.user_targets.each do |user_target|
223
+ user_pkg = user_project.root_object.package_references.find { |p|
224
+ p.class == pkg_class && p.repositoryURL == url
225
+ }
226
+ unless user_pkg
227
+ user_pkg = user_project.new(pkg_class)
228
+ user_pkg.repositoryURL = url
229
+ user_pkg.requirement = requirement
230
+ user_project.root_object.package_references << user_pkg
231
+ end
232
+
233
+ products.each do |product_name|
234
+ ref = user_target.package_product_dependencies.find { |r|
235
+ r.class == ref_class && r.package == user_pkg && r.product_name == product_name
236
+ }
237
+ unless ref
238
+ ref = user_project.new(ref_class)
239
+ ref.package = user_pkg
240
+ ref.product_name = product_name
241
+ user_target.package_product_dependencies << ref
242
+ puts "[ExpoMapboxNavigation] Linked #{product_name} -> #{user_target.name}"
243
+ end
244
+ end
245
+ end
246
+ user_project.save
247
+ end
248
+ end
249
+ `;
250
+
251
+ // Find the last post_install block and add our call inside it,
252
+ // or add a new post_install block if none exists.
253
+ if (podfile.includes('post_install do |installer|')) {
254
+ // Add our helper def before the first post_install
255
+ // and our call inside the existing post_install
256
+ podfile = spmHook + podfile.replace(
257
+ 'post_install do |installer|',
258
+ 'post_install do |installer|\n _expo_mapbox_nav_add_spm(installer)'
259
+ );
260
+ } else {
261
+ // No post_install block — add both the helper and a new block
262
+ podfile = podfile + spmHook + `
263
+ post_install do |installer|
264
+ _expo_mapbox_nav_add_spm(installer)
265
+ end
266
+ `;
267
+ }
268
+
269
+ fs.writeFileSync(podfilePath, podfile, 'utf8');
270
+ console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Injected mapbox-navigation-ios SPM hook into Podfile');
271
+ return mod;
272
+ },
273
+ ]);
274
+
84
275
  return config;
85
276
  };
86
277
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
4
4
  "description": "Expo module for Mapbox Navigation SDK with 16KB page size support, NDK27, and Mapbox Maps v11.11.0+",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -12,6 +12,7 @@ const withMapboxNavigation = (config, options = {}) => {
12
12
  accessToken,
13
13
  downloadsToken,
14
14
  mapboxMapsVersion = '11.11.0',
15
+ mapboxNavigationVersion = null, // optional override — auto-calculated from mapboxMapsVersion if not set
15
16
  androidColorOverrides = {},
16
17
  } = options;
17
18
 
@@ -81,6 +82,196 @@ const withMapboxNavigation = (config, options = {}) => {
81
82
  },
82
83
  ]);
83
84
 
85
+ // ── iOS: Inject mapbox-navigation-ios SPM via Podfile post_install hook ──────
86
+ //
87
+ // This is the correct approach for adding SPM dependencies alongside
88
+ // CocoaPods in an Expo project — copied directly from @rnmapbox/maps
89
+ // (rnmapbox-maps.podspec, _add_spm_to_target method).
90
+ //
91
+ // WHY post_install hook (not pbxproj text injection):
92
+ // The hook runs INSIDE `pod install`, with access to the Ruby Xcodeproj
93
+ // object model (installer.pods_project, installer.aggregate_targets).
94
+ // This means:
95
+ // - Proper find-or-create (no duplicate symbols risk)
96
+ // - CocoaPods-aware (survives pod install --clean)
97
+ // - Works in the xcworkspace context (not just xcodeproj)
98
+ // - Identical to how @rnmapbox/maps itself adds SPM packages
99
+ //
100
+ // WHY this doesn't cause duplicate symbols (unlike spm_dependency()):
101
+ // spm_dependency() links the SPM framework into the Pod target AND the app
102
+ // target → 2 copies. This hook adds the package to ONLY the
103
+ // ExpoMapboxNavigation pod target + the app target, using the same
104
+ // XCRemoteSwiftPackageReference object → 1 copy, properly deduplicated.
105
+ config = withDangerousMod(config, [
106
+ 'ios',
107
+ (mod) => {
108
+ const podfilePath = path.join(mod.modRequest.platformProjectRoot, 'Podfile');
109
+ if (!fs.existsSync(podfilePath)) {
110
+ console.warn('[@jacques_gordon/expo-mapbox-navigation] Podfile not found, skipping SPM hook');
111
+ return mod;
112
+ }
113
+
114
+ let podfile = fs.readFileSync(podfilePath, 'utf8');
115
+
116
+ // Guard: don't inject twice
117
+ if (podfile.includes('# [ExpoMapboxNavigation] SPM hook')) {
118
+ return mod;
119
+ }
120
+
121
+ // ── NAVIGATION VERSION STRATEGY ───────────────────────────────────────
122
+ // CONFIRMED from the official CHANGELOG.md:
123
+ //
124
+ // PHASE 1 — Nav 3.1 to 3.12 (offset of +3):
125
+ // Navigation 3.N.x requires MapboxMaps 11.(N+3).x
126
+ // Nav 3.8.x → Maps 11.11.x ✅ (Maps 11.11.0 → Nav 3.8.x)
127
+ // Nav 3.11.x → Maps 11.14.x ✅ (confirmed CHANGELOG)
128
+ // Nav 3.12.x → Maps 11.15.x ✅ (confirmed rc.1 release note)
129
+ //
130
+ // PHASE 2 — Nav 3.16+ (3.13/3.14/3.15 were DELIBERATELY SKIPPED):
131
+ // Navigation 3.N.x requires MapboxMaps 11.N.x (minors aligned)
132
+ // Nav 3.16.x → Maps 11.16.x ✅
133
+ // Nav 3.21.5 → Maps 11.21.5 ✅ (confirmed release)
134
+ // Nav 3.23.1 → Maps 11.23.1 ✅ (confirmed release)
135
+ // Nav 3.25.0 → Maps 11.25.0 ✅ (confirmed release)
136
+ //
137
+ // Source: Android CHANGELOG — "3.16.x is the next version after 3.12.x.
138
+ // For technical reasons, versions 3.13.x, 3.14.x and 3.15.x are skipped.
139
+ // Starting from 3.16.x, the Nav SDK minor version will be aligned with
140
+ // other Mapbox dependencies." (same policy applies to iOS)
141
+ //
142
+ // FORMULA:
143
+ // if mapsMinor <= 15: navMinor = mapsMinor - 3
144
+ // if mapsMinor >= 16: navMinor = mapsMinor
145
+ //
146
+ // EXAMPLE: mapboxMapsVersion = "11.11.0"
147
+ // mapsMinor = 11 (≤15, Phase 1)
148
+ // navMinor = 11 - 3 = 8
149
+ // navMin = "3.8.0" → SPM resolves latest 3.8.x → Maps 11.11.x ✅
150
+ //
151
+ // EXAMPLE: mapboxMapsVersion = "11.21.0"
152
+ // mapsMinor = 21 (≥16, Phase 2)
153
+ // navMinor = 21
154
+ // navMin = "3.21.0" → SPM resolves latest 3.21.x → Maps 11.21.x ✅
155
+ const mapsVersion = mapboxMapsVersion || '11.11.0';
156
+ const mapsMinor = parseInt(mapsVersion.split('.')[1], 10) || 11;
157
+ const navMinor = mapsMinor <= 15 ? mapsMinor - 3 : mapsMinor;
158
+ const navMin = mapboxNavigationVersion || `3.${navMinor}.0`;
159
+
160
+ console.log(`[@jacques_gordon/expo-mapbox-navigation] Maps ${mapsVersion} (minor=${mapsMinor}) → Navigation ${navMin}..<3.${navMinor+1}.0`);
161
+ console.log(`[@jacques_gordon/expo-mapbox-navigation] Phase: ${mapsMinor <= 15 ? `1 (offset -3: ${mapsMinor}-3=${navMinor})` : `2 (aligned: ${navMinor})`}`);
162
+
163
+ // The Ruby hook — identical pattern to @rnmapbox/maps _add_spm_to_target
164
+ const spmHook = `
165
+ # [ExpoMapboxNavigation] SPM hook — injected by @jacques_gordon/expo-mapbox-navigation
166
+ # Navigation: upToNextMinorVersion from ${navMin}
167
+ # Maps: ${mapsVersion} (minor ${mapsMinor}, ${mapsMinor <= 15 ? 'Phase 1: offset -3' : 'Phase 2: aligned'})
168
+ def _expo_mapbox_nav_add_spm(installer)
169
+ url = 'https://github.com/mapbox/mapbox-navigation-ios.git'
170
+ requirement = { kind: 'upToNextMinorVersion', minimumVersion: '${navMin}' }
171
+ products = ['MapboxNavigationCore', 'MapboxNavigationUIKit']
172
+
173
+ pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
174
+ ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency
175
+
176
+ # ── Step 1: Add to pods_project (where ExpoMapboxNavigation target lives) ──
177
+ pods_project = installer.pods_project
178
+
179
+ pkg = pods_project.root_object.package_references.find { |p|
180
+ p.class == pkg_class && p.repositoryURL == url
181
+ }
182
+ unless pkg
183
+ pkg = pods_project.new(pkg_class)
184
+ pkg.repositoryURL = url
185
+ pkg.requirement = requirement
186
+ pods_project.root_object.package_references << pkg
187
+ puts '[ExpoMapboxNavigation] Added mapbox-navigation-ios to pods_project'
188
+ end
189
+
190
+ # ── FIX: Stronger target lookup with fallback ──────────────────────────────
191
+ # CocoaPods normally names the target exactly 'ExpoMapboxNavigation'.
192
+ # Deduplication suffixes (e.g. 'ExpoMapboxNavigation-abc123') only happen
193
+ # when the same pod is included in multiple targets with different specs,
194
+ # which is not our case. We still add an include? fallback to be safe.
195
+ expo_target = pods_project.targets.find { |t| t.name == 'ExpoMapboxNavigation' }
196
+ expo_target ||= pods_project.targets.find { |t| t.name.include?('ExpoMapboxNavigation') }
197
+ if expo_target
198
+ puts "[ExpoMapboxNavigation] Found target: #{expo_target.name}"
199
+ products.each do |product_name|
200
+ ref = expo_target.package_product_dependencies.find { |r|
201
+ r.class == ref_class && r.package == pkg && r.product_name == product_name
202
+ }
203
+ unless ref
204
+ ref = pods_project.new(ref_class)
205
+ ref.package = pkg
206
+ ref.product_name = product_name
207
+ expo_target.package_product_dependencies << ref
208
+ puts "[ExpoMapboxNavigation] Linked #{product_name} -> #{expo_target.name}"
209
+ end
210
+ end
211
+ else
212
+ # Debug: print all available targets so we can fix the name if needed
213
+ puts '[ExpoMapboxNavigation] WARNING: ExpoMapboxNavigation target not found!'
214
+ puts '[ExpoMapboxNavigation] Available targets:'
215
+ pods_project.targets.each { |t| puts " - #{t.name}" }
216
+ end
217
+ pods_project.save
218
+
219
+ # ── Step 2: Add to user app target (needed for import in app binary) ────────
220
+ installer.aggregate_targets.each do |agg|
221
+ user_project = agg.user_project
222
+ agg.user_targets.each do |user_target|
223
+ user_pkg = user_project.root_object.package_references.find { |p|
224
+ p.class == pkg_class && p.repositoryURL == url
225
+ }
226
+ unless user_pkg
227
+ user_pkg = user_project.new(pkg_class)
228
+ user_pkg.repositoryURL = url
229
+ user_pkg.requirement = requirement
230
+ user_project.root_object.package_references << user_pkg
231
+ end
232
+
233
+ products.each do |product_name|
234
+ ref = user_target.package_product_dependencies.find { |r|
235
+ r.class == ref_class && r.package == user_pkg && r.product_name == product_name
236
+ }
237
+ unless ref
238
+ ref = user_project.new(ref_class)
239
+ ref.package = user_pkg
240
+ ref.product_name = product_name
241
+ user_target.package_product_dependencies << ref
242
+ puts "[ExpoMapboxNavigation] Linked #{product_name} -> #{user_target.name}"
243
+ end
244
+ end
245
+ end
246
+ user_project.save
247
+ end
248
+ end
249
+ `;
250
+
251
+ // Find the last post_install block and add our call inside it,
252
+ // or add a new post_install block if none exists.
253
+ if (podfile.includes('post_install do |installer|')) {
254
+ // Add our helper def before the first post_install
255
+ // and our call inside the existing post_install
256
+ podfile = spmHook + podfile.replace(
257
+ 'post_install do |installer|',
258
+ 'post_install do |installer|\n _expo_mapbox_nav_add_spm(installer)'
259
+ );
260
+ } else {
261
+ // No post_install block — add both the helper and a new block
262
+ podfile = podfile + spmHook + `
263
+ post_install do |installer|
264
+ _expo_mapbox_nav_add_spm(installer)
265
+ end
266
+ `;
267
+ }
268
+
269
+ fs.writeFileSync(podfilePath, podfile, 'utf8');
270
+ console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Injected mapbox-navigation-ios SPM hook into Podfile');
271
+ return mod;
272
+ },
273
+ ]);
274
+
84
275
  return config;
85
276
  };
86
277