@jacques_gordon/expo-mapbox-navigation 2.2.6 → 2.2.7

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,6 +2,33 @@ 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
+
5
32
  Pod::Spec.new do |s|
6
33
  s.name = 'ExpoMapboxNavigation'
7
34
  s.version = package['version']
@@ -10,40 +37,46 @@ Pod::Spec.new do |s|
10
37
  s.license = package['license']
11
38
  s.author = package['author']
12
39
  s.homepage = package['homepage']
40
+
41
+ # iOS 14+ as required by Mapbox Navigation SDK v3
13
42
  s.platforms = { :ios => '14.0' }
14
43
  s.swift_version = '5.9'
15
44
  s.source = { git: package['repository']['url'], tag: "v#{s.version}" }
16
45
 
17
46
  s.dependency 'ExpoModulesCore'
18
47
 
19
- # ── iOS: Mapbox Navigation SDK v3 ───────────────────────────────────────────
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.
20
51
  #
21
- # IMPORTANT do NOT use spm_dependency() here.
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).
22
56
  #
23
- # spm_dependency() causes "43029 duplicate symbols" linker errors when used
24
- # alongside @rnmapbox/maps in an Expo project (confirmed: React Native
25
- # GitHub issue #47344, Expo GitHub issue #37813). The root cause is that
26
- # spm_dependency() links the SPM framework into both the Pod target AND
27
- # the main app target simultaneously, so the linker sees every symbol twice.
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
63
  #
29
- # The correct approach for Expo modules that need SPM-only packages:
30
- # add the SPM dependency directly to the Xcode project's main target via
31
- # the config plugin (withXcodeProject + withDangerousMod), which is exactly
32
- # what our plugin/src/index.js does via the addMapboxNavigationSPM function.
33
- # That way there is only ONE copy of each framework in the final binary.
34
- #
35
- # MapboxNavigationCore and MapboxNavigationUIKit are therefore listed as
36
- # weak framework references here so the module compiles against them, while
37
- # the actual linking happens at the app level from the SPM dependency added
38
- # by the config plugin.
39
- s.pod_target_xcconfig = {
40
- 'DEFINES_MODULE' => 'YES',
41
- 'SWIFT_COMPILATION_MODE' => 'wholemodule',
42
- 'OTHER_SWIFT_FLAGS' => '$(inherited) -Xfrontend -disable-reflection-metadata',
43
- 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) $(PLATFORM_DIR)/Developer/Library/Frameworks',
44
- 'OTHER_LDFLAGS' => '$(inherited)',
45
- 'IPHONEOS_DEPLOYMENT_TARGET' => '14.0',
46
- }
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
47
72
 
48
73
  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'
76
+
77
+ s.pod_target_xcconfig = {
78
+ 'DEFINES_MODULE' => 'YES',
79
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule',
80
+ 'IPHONEOS_DEPLOYMENT_TARGET' => '14.0',
81
+ }
49
82
  end
package/app.plugin.js CHANGED
@@ -1,15 +1,12 @@
1
1
  const {
2
2
  withAppBuildGradle,
3
- withProjectBuildGradle,
4
3
  withAndroidManifest,
5
4
  withInfoPlist,
6
5
  withDangerousMod,
7
6
  } = require('@expo/config-plugins');
8
- const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');
9
7
  const fs = require('fs');
10
8
  const path = require('path');
11
9
 
12
-
13
10
  const withMapboxNavigation = (config, options = {}) => {
14
11
  const {
15
12
  accessToken,
@@ -20,17 +17,14 @@ const withMapboxNavigation = (config, options = {}) => {
20
17
 
21
18
  if (!accessToken) {
22
19
  throw new Error(
23
- '[@jacques_gordon/expo-mapbox-navigation] `accessToken` is required in the plugin config.\n' +
24
- 'Add it to your app.json plugins array:\n' +
25
- ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.your_token" }]'
20
+ '[@jacques_gordon/expo-mapbox-navigation] `accessToken` is required.\n' +
21
+ ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx" }]'
26
22
  );
27
23
  }
28
24
 
29
25
  if (!downloadsToken) {
30
26
  throw new Error(
31
- '[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` is required for iOS builds.\n' +
32
- 'This must be a SECRET Mapbox token (starts with "sk.") with the "Downloads:Read" scope.\n' +
33
- 'Add it to your app.json plugins array:\n' +
27
+ '[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` (secret sk.* token) is required for iOS.\n' +
34
28
  ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
35
29
  );
36
30
  }
@@ -46,7 +40,7 @@ const withMapboxNavigation = (config, options = {}) => {
46
40
  return mod;
47
41
  });
48
42
 
49
- // ── iOS: MBXAccessToken in Info.plist ────────────────────────────────────
43
+ // ── iOS: Info.plist — MBXAccessToken + permissions ────────────────────────
50
44
  config = withInfoPlist(config, (mod) => {
51
45
  mod.modResults.MBXAccessToken = accessToken;
52
46
  mod.modResults.NSLocationWhenInUseUsageDescription =
@@ -64,7 +58,11 @@ const withMapboxNavigation = (config, options = {}) => {
64
58
  return mod;
65
59
  });
66
60
 
67
- // ── iOS: .netrc for SPM authentication ───────────────────────────────────
61
+ // ── iOS: .netrc for SPM authentication ────────────────────────────────────
62
+ // The Mapbox Navigation SDK v3 is distributed as source code via SPM only.
63
+ // Our podspec uses spm_dependency() to declare the dependency, and SPM
64
+ // authenticates against api.mapbox.com using ~/.netrc credentials.
65
+ // This is the official Mapbox-documented authentication mechanism.
68
66
  config = withDangerousMod(config, [
69
67
  'ios',
70
68
  (mod) => {
@@ -77,194 +75,18 @@ const withMapboxNavigation = (config, options = {}) => {
77
75
  }
78
76
  if (!existingContent.includes('machine api.mapbox.com')) {
79
77
  fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
78
+ console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Wrote Mapbox credentials to ~/.netrc');
80
79
  }
81
80
  return mod;
82
81
  },
83
82
  ]);
84
83
 
85
- // ── iOS: Inject Mapbox Navigation SPM into project.pbxproj ───────────────
86
- //
87
- // WHY NOT spm_dependency() IN THE PODSPEC:
88
- // spm_dependency() causes "43029 duplicate symbols" linker errors alongside
89
- // @rnmapbox/maps (React Native #47344, Expo #37813) — the framework gets
90
- // linked in both the Pod target AND the main app target simultaneously.
91
- //
92
- // WHY NOT xcodeProject.addSwiftPackage():
93
- // That method does not exist in the `xcode` npm package that Expo uses
94
- // under the hood — it throws "addSwiftPackage is not a function".
95
- //
96
- // THE SOLUTION — withDangerousMod on project.pbxproj:
97
- // We inject the SPM package reference and product dependencies directly
98
- // into the .pbxproj file as text. This is the same technique used by
99
- // several established Expo modules (e.g. expo-notifications, rnmapbox)
100
- // for SPM dependencies that have no CocoaPods distribution.
101
- // The Mapbox Navigation SDK v3 explicitly states "CocoaPods support is
102
- // currently in development" — SPM is the only official distribution.
103
- config = withDangerousMod(config, [
104
- 'ios',
105
- (mod) => {
106
- const projectRoot = mod.modRequest.platformProjectRoot;
107
- const projectName = mod.modRequest.projectName;
108
- const pbxprojPath = path.join(
109
- projectRoot,
110
- `${projectName}.xcodeproj`,
111
- 'project.pbxproj'
112
- );
113
-
114
- if (!fs.existsSync(pbxprojPath)) {
115
- console.warn(`[@jacques_gordon/expo-mapbox-navigation] Could not find ${pbxprojPath} — skipping SPM injection`);
116
- return mod;
117
- }
118
-
119
- let pbxproj = fs.readFileSync(pbxprojPath, 'utf8');
120
-
121
- // Skip if already injected
122
- if (pbxproj.includes('mapbox-navigation-ios')) {
123
- return mod;
124
- }
125
-
126
- // Generate stable UUIDs for the new entries
127
- // (UUIDs must be 24 hex chars in pbxproj format)
128
- const makeUUID = () => {
129
- const { randomBytes } = require('crypto');
130
- return randomBytes(12).toString('hex').toUpperCase();
131
- };
132
-
133
- const pkgRefUUID = makeUUID(); // XCRemoteSwiftPackageReference
134
- const coreDepUUID = makeUUID(); // XCSwiftPackageProductDependency (MapboxNavigationCore)
135
- const uikitDepUUID = makeUUID(); // XCSwiftPackageProductDependency (MapboxNavigationUIKit)
136
- const coreBuildUUID = makeUUID(); // PBXBuildFile (MapboxNavigationCore)
137
- const uikitBuildUUID = makeUUID(); // PBXBuildFile (MapboxNavigationUIKit)
138
-
139
- const NAV_IOS_REPO = 'https://github.com/mapbox/mapbox-navigation-ios.git';
140
- const NAV_IOS_VERSION = '3.25.0';
141
-
142
- // 1. Add XCRemoteSwiftPackageReference
143
- const pkgRefEntry = `
144
- \t\t${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */ = {
145
- \t\t\tisa = XCRemoteSwiftPackageReference;
146
- \t\t\trequirement = {
147
- \t\t\t\tkind = upToNextMajorVersion;
148
- \t\t\t\tminimumVersion = ${NAV_IOS_VERSION};
149
- \t\t\t};
150
- \t\t\trepositoryURL = "${NAV_IOS_REPO}";
151
- \t\t};`;
152
-
153
- // 2. Add XCSwiftPackageProductDependencies
154
- const coreDepEntry = `
155
- \t\t${coreDepUUID} /* MapboxNavigationCore */ = {
156
- \t\t\tisa = XCSwiftPackageProductDependency;
157
- \t\t\tpackage = ${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */;
158
- \t\t\tproductName = MapboxNavigationCore;
159
- \t\t};`;
160
-
161
- const uikitDepEntry = `
162
- \t\t${uikitDepUUID} /* MapboxNavigationUIKit */ = {
163
- \t\t\tisa = XCSwiftPackageProductDependency;
164
- \t\t\tpackage = ${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */;
165
- \t\t\tproductName = MapboxNavigationUIKit;
166
- \t\t};`;
167
-
168
- // 3. Add PBXBuildFile entries for the frameworks
169
- const coreBuildEntry = `
170
- \t\t${coreBuildUUID} /* MapboxNavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = ${coreDepUUID} /* MapboxNavigationCore */; };`;
171
-
172
- const uikitBuildEntry = `
173
- \t\t${uikitBuildUUID} /* MapboxNavigationUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = ${uikitDepUUID} /* MapboxNavigationUIKit */; };`;
174
-
175
- // Inject into the relevant sections
176
- // Section: XCRemoteSwiftPackageReference
177
- if (pbxproj.includes('/* Begin XCRemoteSwiftPackageReference section */')) {
178
- pbxproj = pbxproj.replace(
179
- '/* Begin XCRemoteSwiftPackageReference section */',
180
- `/* Begin XCRemoteSwiftPackageReference section */${pkgRefEntry}`
181
- );
182
- } else {
183
- // Section doesn't exist yet — add it before the end of the project
184
- pbxproj = pbxproj.replace(
185
- '/* End XCConfigurationList section */',
186
- `/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */${pkgRefEntry}\n/* End XCRemoteSwiftPackageReference section */`
187
- );
188
- }
189
-
190
- // Section: XCSwiftPackageProductDependency
191
- if (pbxproj.includes('/* Begin XCSwiftPackageProductDependency section */')) {
192
- pbxproj = pbxproj.replace(
193
- '/* Begin XCSwiftPackageProductDependency section */',
194
- `/* Begin XCSwiftPackageProductDependency section */${coreDepEntry}${uikitDepEntry}`
195
- );
196
- } else {
197
- pbxproj = pbxproj.replace(
198
- '/* End XCRemoteSwiftPackageReference section */',
199
- `/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */${coreDepEntry}${uikitDepEntry}\n/* End XCSwiftPackageProductDependency section */`
200
- );
201
- }
202
-
203
- // Section: PBXBuildFile
204
- pbxproj = pbxproj.replace(
205
- '/* Begin PBXBuildFile section */',
206
- `/* Begin PBXBuildFile section */${coreBuildEntry}${uikitBuildEntry}`
207
- );
208
-
209
- // Add to main target's Frameworks build phase
210
- // FIX: the regex was non-greedy and could match the test target instead
211
- // of the main app target. We now inject into ALL PBXFrameworksBuildPhase
212
- // files lists — Xcode deduplicates at link time, and only the main app
213
- // target actually links frameworks, so this is safe.
214
- // Use a replace-all approach on PBXBuildFile section only (already done above),
215
- // and find all 'files = (' inside PBXFrameworksBuildPhase section precisely.
216
- const frameworksSectionMatch = pbxproj.match(
217
- /\/\* Begin PBXFrameworksBuildPhase section \*\/([\s\S]*?)\/\* End PBXFrameworksBuildPhase section \*\//
218
- );
219
- if (frameworksSectionMatch) {
220
- const originalSection = frameworksSectionMatch[0];
221
- // Replace only the FIRST 'files = (' inside this section (= main app target)
222
- const patchedSection = originalSection.replace(
223
- 'files = (',
224
- `files = (\n\t\t\t\t${coreBuildUUID} /* MapboxNavigationCore in Frameworks */,\n\t\t\t\t${uikitBuildUUID} /* MapboxNavigationUIKit in Frameworks */,`
225
- );
226
- pbxproj = pbxproj.replace(originalSection, patchedSection);
227
- }
228
-
229
- // Add to main target's packageProductDependencies
230
- // FIX: only replace the FIRST occurrence (main app target, not test target)
231
- if (pbxproj.includes('packageProductDependencies = (')) {
232
- pbxproj = pbxproj.replace(
233
- 'packageProductDependencies = (',
234
- `packageProductDependencies = (\n\t\t\t\t${coreDepUUID} /* MapboxNavigationCore */,\n\t\t\t\t${uikitDepUUID} /* MapboxNavigationUIKit */,`
235
- );
236
- } else {
237
- // packageProductDependencies doesn't exist yet — add it to the first PBXNativeTarget
238
- pbxproj = pbxproj.replace(
239
- /(isa = PBXNativeTarget;[\s\S]*?)(packageReferences = \([\s\S]*?\);)/,
240
- `$1$2\n\t\t\tpackageProductDependencies = (\n\t\t\t\t${coreDepUUID} /* MapboxNavigationCore */,\n\t\t\t\t${uikitDepUUID} /* MapboxNavigationUIKit */,\n\t\t\t);`
241
- );
242
- }
243
-
244
- // Add to project's packageReferences list (first occurrence only = main project)
245
- if (pbxproj.includes('packageReferences = (')) {
246
- pbxproj = pbxproj.replace(
247
- 'packageReferences = (',
248
- `packageReferences = (\n\t\t\t\t${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */,`
249
- );
250
- }
251
-
252
- fs.writeFileSync(pbxprojPath, pbxproj, 'utf8');
253
- console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Injected Mapbox Navigation SPM into project.pbxproj');
254
-
255
- return mod;
256
- },
257
- ]);
258
-
259
84
  return config;
260
85
  };
261
86
 
262
- // ── Android helpers ──────────────────────────────────────────────────────────
87
+ // ── Android helpers ───────────────────────────────────────────────────────────
263
88
 
264
89
  function addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides) {
265
- const MAPBOX_DOWNLOADS_TOKEN_KEY = 'MAPBOX_DOWNLOADS_TOKEN';
266
- // (kept for backward compat — actual token is in gradle.properties)
267
-
268
90
  if (!mod.modResults.contents.includes('abiFilters')) {
269
91
  mod.modResults.contents = mod.modResults.contents.replace(
270
92
  /defaultConfig {([\s\S]*?)}/,
@@ -317,9 +139,22 @@ function addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides) {
317
139
  `;
318
140
  }
319
141
 
142
+ // Android color overrides for Mapbox resource colors (route line, banner, etc.)
320
143
  if (Object.keys(androidColorOverrides).length > 0) {
321
- // inject color overrides as Android resource values
322
- // (handled downstream by the Mapbox gradle plugin or manual resource injection)
144
+ const resDir = path.join(
145
+ mod.modRequest?.platformProjectRoot || '',
146
+ 'app', 'src', 'main', 'res', 'values'
147
+ );
148
+ try {
149
+ fs.mkdirSync(resDir, { recursive: true });
150
+ const colorEntries = Object.entries(androidColorOverrides)
151
+ .map(([name, value]) => ` <color name="${name}">${value}</color>`)
152
+ .join('\n');
153
+ const xmlContent = `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`;
154
+ fs.writeFileSync(path.join(resDir, 'mapbox_color_overrides.xml'), xmlContent);
155
+ } catch (e) {
156
+ // Ignore — resDir may not exist at plugin resolution time
157
+ }
323
158
  }
324
159
  }
325
160
 
@@ -0,0 +1,69 @@
1
+ // swift-tools-version:5.9
2
+ // This Package.swift is used ONLY to download and build xcframeworks for
3
+ // the ExpoMapboxNavigation module's vendored_frameworks in the podspec.
4
+ // It is NOT the main entry point for Expo/CocoaPods — that is
5
+ // ExpoMapboxNavigation.podspec.
6
+ //
7
+ // Based on the approach by youssefhenna/expo-mapbox-navigation (the only
8
+ // community package proven to work in production EAS iOS builds with the
9
+ // Mapbox Navigation SDK v3, which is SPM-only and has no CocoaPods support).
10
+ //
11
+ // The xcframeworks produced by building this package are vendored directly
12
+ // into the npm package (ios/Frameworks/*.xcframework), so no network access
13
+ // to api.mapbox.com is needed at `pod install` time — only the .netrc file
14
+ // is required during `npm install` / package download, NOT during the build.
15
+
16
+ import PackageDescription
17
+
18
+ let navNativeVersion = "324.25.0"
19
+ let navNativeChecksum = "placeholder_run_swift_build_to_get_real_checksum"
20
+ let mapsVersion: Version = "11.25.0"
21
+ let commonVersion: Version = "24.25.0"
22
+ let mapboxApiDownloads = "https://api.mapbox.com/downloads/v2"
23
+
24
+ let package = Package(
25
+ name: "MapboxNavigation",
26
+ defaultLocalization: "en",
27
+ platforms: [.iOS(.v14)],
28
+ products: [
29
+ .library(name: "MapboxNavigationUIKit", targets: ["MapboxNavigationUIKit"]),
30
+ .library(name: "MapboxNavigationCore", targets: ["MapboxNavigationCore"]),
31
+ ],
32
+ dependencies: [
33
+ .package(url: "https://github.com/mapbox/mapbox-maps-ios.git", exact: mapsVersion),
34
+ .package(url: "https://github.com/mapbox/mapbox-common-ios.git", exact: commonVersion),
35
+ .package(url: "https://github.com/mapbox/turf-swift.git", exact: "4.0.0"),
36
+ ],
37
+ targets: [
38
+ .target(
39
+ name: "MapboxNavigationUIKit",
40
+ dependencies: ["MapboxNavigationCore"],
41
+ exclude: ["Info.plist"],
42
+ resources: [
43
+ .copy("Resources/MBXInfo.plist"),
44
+ .copy("Resources/PrivacyInfo.xcprivacy"),
45
+ ]
46
+ ),
47
+ .target(name: "_MapboxNavigationHelpers"),
48
+ .target(
49
+ name: "MapboxNavigationCore",
50
+ dependencies: [
51
+ .product(name: "MapboxCommon", package: "mapbox-common-ios"),
52
+ "MapboxNavigationNative",
53
+ "MapboxDirections",
54
+ "_MapboxNavigationHelpers",
55
+ .product(name: "MapboxMaps", package: "mapbox-maps-ios"),
56
+ ],
57
+ resources: [.process("Resources")]
58
+ ),
59
+ .target(
60
+ name: "MapboxDirections",
61
+ dependencies: [.product(name: "Turf", package: "turf-swift")]
62
+ ),
63
+ .binaryTarget(
64
+ name: "MapboxNavigationNative",
65
+ url: "\(mapboxApiDownloads)/dash-native/releases/ios/packages/\(navNativeVersion)/MapboxNavigationNative.xcframework.zip",
66
+ checksum: navNativeChecksum
67
+ ),
68
+ ]
69
+ )
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # build-xcframeworks.sh
4
+ # Builds xcframeworks for the ExpoMapboxNavigation module.
5
+ #
6
+ # PREREQUISITES:
7
+ # 1. macOS + Xcode 16+
8
+ # 2. ~/.netrc configured with your Mapbox Downloads token:
9
+ # machine api.mapbox.com
10
+ # login mapbox
11
+ # password sk.your_downloads_token
12
+ # 3. Swift Package Manager available (comes with Xcode)
13
+ # 4. Scipio installed (https://github.com/giginet/Scipio):
14
+ # swift package --disable-sandbox experimental-publish-xcbundles
15
+ # OR install via Homebrew: brew install giginet/scipio/scipio
16
+ #
17
+ # This script builds MapboxNavigationCore and MapboxNavigationUIKit
18
+ # (and their dependencies) as xcframeworks and copies them into the
19
+ # ios/Frameworks/ directory, which is referenced by ExpoMapboxNavigation.podspec.
20
+ #
21
+ # Based on the approach by youssefhenna/expo-mapbox-navigation:
22
+ # https://github.com/uju777/expo-mapbox-navigation#getting-the-xcframework-files
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ set -e
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ MODULE_ROOT="$(dirname "$SCRIPT_DIR")"
29
+ FRAMEWORKS_DIR="$SCRIPT_DIR/Frameworks"
30
+
31
+ # ── Config ────────────────────────────────────────────────────────────────────
32
+ MAPBOX_NAV_VERSION="3.25.0"
33
+ MAPBOX_MAPS_VERSION="11.25.0"
34
+ MAPBOX_COMMON_VERSION="24.25.0"
35
+ MAPBOX_NAV_NATIVE_VERSION="324.25.0"
36
+
37
+ echo "🔧 Building xcframeworks for Mapbox Navigation SDK v$MAPBOX_NAV_VERSION"
38
+ echo " Output: $FRAMEWORKS_DIR"
39
+ echo ""
40
+
41
+ # ── Step 1: Clone mapbox-navigation-ios ──────────────────────────────────────
42
+ TMPDIR=$(mktemp -d)
43
+ echo "📦 Cloning mapbox-navigation-ios v$MAPBOX_NAV_VERSION..."
44
+ git clone --branch "v$MAPBOX_NAV_VERSION" --depth 1 \
45
+ https://github.com/mapbox/mapbox-navigation-ios.git \
46
+ "$TMPDIR/mapbox-navigation-ios"
47
+
48
+ cd "$TMPDIR/mapbox-navigation-ios"
49
+
50
+ # ── Step 2: Replace Package.swift with the modified version ──────────────────
51
+ echo "📝 Patching Package.swift..."
52
+ cp "$SCRIPT_DIR/Package.swift" Package.swift
53
+
54
+ # ── Step 3: Get the correct navNative checksum ───────────────────────────────
55
+ echo "🔍 Resolving navNative checksum (this may take a moment)..."
56
+ # Run swift build once to get the correct checksum — it will fail and print it
57
+ CHECKSUM=$(swift build -c release 2>&1 | grep -oE '"[a-f0-9]{64}"' | head -1 | tr -d '"' || true)
58
+
59
+ if [ -z "$CHECKSUM" ]; then
60
+ echo "⚠️ Could not auto-detect checksum. Please run manually and update Package.swift."
61
+ else
62
+ echo " Checksum: $CHECKSUM"
63
+ sed -i '' "s/placeholder_run_swift_build_to_get_real_checksum/$CHECKSUM/g" Package.swift
64
+ fi
65
+
66
+ # ── Step 4: Build xcframeworks with Scipio ────────────────────────────────────
67
+ echo ""
68
+ echo "🏗️ Building xcframeworks with Scipio..."
69
+ echo " This will take 10-30 minutes on first run."
70
+ echo ""
71
+
72
+ # Clone Scipio inside the navigation-ios repo
73
+ git clone --depth 1 https://github.com/giginet/Scipio.git Scipio
74
+ cd Scipio
75
+ swift build -c release
76
+
77
+ # Build the xcframeworks
78
+ cd "$TMPDIR/mapbox-navigation-ios"
79
+ Scipio/.build/release/scipio create ./ -f \
80
+ --platforms iOS \
81
+ --only-use-versions-from-resolved-file \
82
+ --enable-library-evolution \
83
+ --support-simulators \
84
+ --embed-debug-symbols \
85
+ --verbose
86
+
87
+ # ── Step 5: Copy to module ────────────────────────────────────────────────────
88
+ echo ""
89
+ echo "📋 Copying xcframeworks to $FRAMEWORKS_DIR..."
90
+ mkdir -p "$FRAMEWORKS_DIR"
91
+
92
+ BUILT_FRAMEWORKS="$TMPDIR/mapbox-navigation-ios/XCFrameworks"
93
+
94
+ # Copy the frameworks we need (others like MapboxMaps come from @rnmapbox/maps)
95
+ NEEDED_FRAMEWORKS=(
96
+ "MapboxNavigationCore"
97
+ "MapboxNavigationUIKit"
98
+ "MapboxNavigationNative"
99
+ "MapboxDirections"
100
+ "_MapboxNavigationHelpers"
101
+ )
102
+
103
+ for fw in "${NEEDED_FRAMEWORKS[@]}"; do
104
+ if [ -d "$BUILT_FRAMEWORKS/$fw.xcframework" ]; then
105
+ echo " ✅ $fw.xcframework"
106
+ cp -R "$BUILT_FRAMEWORKS/$fw.xcframework" "$FRAMEWORKS_DIR/"
107
+ else
108
+ echo " ❌ $fw.xcframework not found in $BUILT_FRAMEWORKS"
109
+ fi
110
+ done
111
+
112
+ # ── Cleanup ────────────────────────────────────────────────────────────────────
113
+ cd /
114
+ rm -rf "$TMPDIR"
115
+
116
+ echo ""
117
+ echo "✅ Done! xcframeworks are in $FRAMEWORKS_DIR"
118
+ echo " Commit the Frameworks/ directory and publish the package."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacques_gordon/expo-mapbox-navigation",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
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",
@@ -1,15 +1,12 @@
1
1
  const {
2
2
  withAppBuildGradle,
3
- withProjectBuildGradle,
4
3
  withAndroidManifest,
5
4
  withInfoPlist,
6
5
  withDangerousMod,
7
6
  } = require('@expo/config-plugins');
8
- const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');
9
7
  const fs = require('fs');
10
8
  const path = require('path');
11
9
 
12
-
13
10
  const withMapboxNavigation = (config, options = {}) => {
14
11
  const {
15
12
  accessToken,
@@ -20,17 +17,14 @@ const withMapboxNavigation = (config, options = {}) => {
20
17
 
21
18
  if (!accessToken) {
22
19
  throw new Error(
23
- '[@jacques_gordon/expo-mapbox-navigation] `accessToken` is required in the plugin config.\n' +
24
- 'Add it to your app.json plugins array:\n' +
25
- ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.your_token" }]'
20
+ '[@jacques_gordon/expo-mapbox-navigation] `accessToken` is required.\n' +
21
+ ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx" }]'
26
22
  );
27
23
  }
28
24
 
29
25
  if (!downloadsToken) {
30
26
  throw new Error(
31
- '[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` is required for iOS builds.\n' +
32
- 'This must be a SECRET Mapbox token (starts with "sk.") with the "Downloads:Read" scope.\n' +
33
- 'Add it to your app.json plugins array:\n' +
27
+ '[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` (secret sk.* token) is required for iOS.\n' +
34
28
  ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
35
29
  );
36
30
  }
@@ -46,7 +40,7 @@ const withMapboxNavigation = (config, options = {}) => {
46
40
  return mod;
47
41
  });
48
42
 
49
- // ── iOS: MBXAccessToken in Info.plist ────────────────────────────────────
43
+ // ── iOS: Info.plist — MBXAccessToken + permissions ────────────────────────
50
44
  config = withInfoPlist(config, (mod) => {
51
45
  mod.modResults.MBXAccessToken = accessToken;
52
46
  mod.modResults.NSLocationWhenInUseUsageDescription =
@@ -64,7 +58,11 @@ const withMapboxNavigation = (config, options = {}) => {
64
58
  return mod;
65
59
  });
66
60
 
67
- // ── iOS: .netrc for SPM authentication ───────────────────────────────────
61
+ // ── iOS: .netrc for SPM authentication ────────────────────────────────────
62
+ // The Mapbox Navigation SDK v3 is distributed as source code via SPM only.
63
+ // Our podspec uses spm_dependency() to declare the dependency, and SPM
64
+ // authenticates against api.mapbox.com using ~/.netrc credentials.
65
+ // This is the official Mapbox-documented authentication mechanism.
68
66
  config = withDangerousMod(config, [
69
67
  'ios',
70
68
  (mod) => {
@@ -77,194 +75,18 @@ const withMapboxNavigation = (config, options = {}) => {
77
75
  }
78
76
  if (!existingContent.includes('machine api.mapbox.com')) {
79
77
  fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
78
+ console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Wrote Mapbox credentials to ~/.netrc');
80
79
  }
81
80
  return mod;
82
81
  },
83
82
  ]);
84
83
 
85
- // ── iOS: Inject Mapbox Navigation SPM into project.pbxproj ───────────────
86
- //
87
- // WHY NOT spm_dependency() IN THE PODSPEC:
88
- // spm_dependency() causes "43029 duplicate symbols" linker errors alongside
89
- // @rnmapbox/maps (React Native #47344, Expo #37813) — the framework gets
90
- // linked in both the Pod target AND the main app target simultaneously.
91
- //
92
- // WHY NOT xcodeProject.addSwiftPackage():
93
- // That method does not exist in the `xcode` npm package that Expo uses
94
- // under the hood — it throws "addSwiftPackage is not a function".
95
- //
96
- // THE SOLUTION — withDangerousMod on project.pbxproj:
97
- // We inject the SPM package reference and product dependencies directly
98
- // into the .pbxproj file as text. This is the same technique used by
99
- // several established Expo modules (e.g. expo-notifications, rnmapbox)
100
- // for SPM dependencies that have no CocoaPods distribution.
101
- // The Mapbox Navigation SDK v3 explicitly states "CocoaPods support is
102
- // currently in development" — SPM is the only official distribution.
103
- config = withDangerousMod(config, [
104
- 'ios',
105
- (mod) => {
106
- const projectRoot = mod.modRequest.platformProjectRoot;
107
- const projectName = mod.modRequest.projectName;
108
- const pbxprojPath = path.join(
109
- projectRoot,
110
- `${projectName}.xcodeproj`,
111
- 'project.pbxproj'
112
- );
113
-
114
- if (!fs.existsSync(pbxprojPath)) {
115
- console.warn(`[@jacques_gordon/expo-mapbox-navigation] Could not find ${pbxprojPath} — skipping SPM injection`);
116
- return mod;
117
- }
118
-
119
- let pbxproj = fs.readFileSync(pbxprojPath, 'utf8');
120
-
121
- // Skip if already injected
122
- if (pbxproj.includes('mapbox-navigation-ios')) {
123
- return mod;
124
- }
125
-
126
- // Generate stable UUIDs for the new entries
127
- // (UUIDs must be 24 hex chars in pbxproj format)
128
- const makeUUID = () => {
129
- const { randomBytes } = require('crypto');
130
- return randomBytes(12).toString('hex').toUpperCase();
131
- };
132
-
133
- const pkgRefUUID = makeUUID(); // XCRemoteSwiftPackageReference
134
- const coreDepUUID = makeUUID(); // XCSwiftPackageProductDependency (MapboxNavigationCore)
135
- const uikitDepUUID = makeUUID(); // XCSwiftPackageProductDependency (MapboxNavigationUIKit)
136
- const coreBuildUUID = makeUUID(); // PBXBuildFile (MapboxNavigationCore)
137
- const uikitBuildUUID = makeUUID(); // PBXBuildFile (MapboxNavigationUIKit)
138
-
139
- const NAV_IOS_REPO = 'https://github.com/mapbox/mapbox-navigation-ios.git';
140
- const NAV_IOS_VERSION = '3.25.0';
141
-
142
- // 1. Add XCRemoteSwiftPackageReference
143
- const pkgRefEntry = `
144
- \t\t${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */ = {
145
- \t\t\tisa = XCRemoteSwiftPackageReference;
146
- \t\t\trequirement = {
147
- \t\t\t\tkind = upToNextMajorVersion;
148
- \t\t\t\tminimumVersion = ${NAV_IOS_VERSION};
149
- \t\t\t};
150
- \t\t\trepositoryURL = "${NAV_IOS_REPO}";
151
- \t\t};`;
152
-
153
- // 2. Add XCSwiftPackageProductDependencies
154
- const coreDepEntry = `
155
- \t\t${coreDepUUID} /* MapboxNavigationCore */ = {
156
- \t\t\tisa = XCSwiftPackageProductDependency;
157
- \t\t\tpackage = ${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */;
158
- \t\t\tproductName = MapboxNavigationCore;
159
- \t\t};`;
160
-
161
- const uikitDepEntry = `
162
- \t\t${uikitDepUUID} /* MapboxNavigationUIKit */ = {
163
- \t\t\tisa = XCSwiftPackageProductDependency;
164
- \t\t\tpackage = ${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */;
165
- \t\t\tproductName = MapboxNavigationUIKit;
166
- \t\t};`;
167
-
168
- // 3. Add PBXBuildFile entries for the frameworks
169
- const coreBuildEntry = `
170
- \t\t${coreBuildUUID} /* MapboxNavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = ${coreDepUUID} /* MapboxNavigationCore */; };`;
171
-
172
- const uikitBuildEntry = `
173
- \t\t${uikitBuildUUID} /* MapboxNavigationUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = ${uikitDepUUID} /* MapboxNavigationUIKit */; };`;
174
-
175
- // Inject into the relevant sections
176
- // Section: XCRemoteSwiftPackageReference
177
- if (pbxproj.includes('/* Begin XCRemoteSwiftPackageReference section */')) {
178
- pbxproj = pbxproj.replace(
179
- '/* Begin XCRemoteSwiftPackageReference section */',
180
- `/* Begin XCRemoteSwiftPackageReference section */${pkgRefEntry}`
181
- );
182
- } else {
183
- // Section doesn't exist yet — add it before the end of the project
184
- pbxproj = pbxproj.replace(
185
- '/* End XCConfigurationList section */',
186
- `/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */${pkgRefEntry}\n/* End XCRemoteSwiftPackageReference section */`
187
- );
188
- }
189
-
190
- // Section: XCSwiftPackageProductDependency
191
- if (pbxproj.includes('/* Begin XCSwiftPackageProductDependency section */')) {
192
- pbxproj = pbxproj.replace(
193
- '/* Begin XCSwiftPackageProductDependency section */',
194
- `/* Begin XCSwiftPackageProductDependency section */${coreDepEntry}${uikitDepEntry}`
195
- );
196
- } else {
197
- pbxproj = pbxproj.replace(
198
- '/* End XCRemoteSwiftPackageReference section */',
199
- `/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */${coreDepEntry}${uikitDepEntry}\n/* End XCSwiftPackageProductDependency section */`
200
- );
201
- }
202
-
203
- // Section: PBXBuildFile
204
- pbxproj = pbxproj.replace(
205
- '/* Begin PBXBuildFile section */',
206
- `/* Begin PBXBuildFile section */${coreBuildEntry}${uikitBuildEntry}`
207
- );
208
-
209
- // Add to main target's Frameworks build phase
210
- // FIX: the regex was non-greedy and could match the test target instead
211
- // of the main app target. We now inject into ALL PBXFrameworksBuildPhase
212
- // files lists — Xcode deduplicates at link time, and only the main app
213
- // target actually links frameworks, so this is safe.
214
- // Use a replace-all approach on PBXBuildFile section only (already done above),
215
- // and find all 'files = (' inside PBXFrameworksBuildPhase section precisely.
216
- const frameworksSectionMatch = pbxproj.match(
217
- /\/\* Begin PBXFrameworksBuildPhase section \*\/([\s\S]*?)\/\* End PBXFrameworksBuildPhase section \*\//
218
- );
219
- if (frameworksSectionMatch) {
220
- const originalSection = frameworksSectionMatch[0];
221
- // Replace only the FIRST 'files = (' inside this section (= main app target)
222
- const patchedSection = originalSection.replace(
223
- 'files = (',
224
- `files = (\n\t\t\t\t${coreBuildUUID} /* MapboxNavigationCore in Frameworks */,\n\t\t\t\t${uikitBuildUUID} /* MapboxNavigationUIKit in Frameworks */,`
225
- );
226
- pbxproj = pbxproj.replace(originalSection, patchedSection);
227
- }
228
-
229
- // Add to main target's packageProductDependencies
230
- // FIX: only replace the FIRST occurrence (main app target, not test target)
231
- if (pbxproj.includes('packageProductDependencies = (')) {
232
- pbxproj = pbxproj.replace(
233
- 'packageProductDependencies = (',
234
- `packageProductDependencies = (\n\t\t\t\t${coreDepUUID} /* MapboxNavigationCore */,\n\t\t\t\t${uikitDepUUID} /* MapboxNavigationUIKit */,`
235
- );
236
- } else {
237
- // packageProductDependencies doesn't exist yet — add it to the first PBXNativeTarget
238
- pbxproj = pbxproj.replace(
239
- /(isa = PBXNativeTarget;[\s\S]*?)(packageReferences = \([\s\S]*?\);)/,
240
- `$1$2\n\t\t\tpackageProductDependencies = (\n\t\t\t\t${coreDepUUID} /* MapboxNavigationCore */,\n\t\t\t\t${uikitDepUUID} /* MapboxNavigationUIKit */,\n\t\t\t);`
241
- );
242
- }
243
-
244
- // Add to project's packageReferences list (first occurrence only = main project)
245
- if (pbxproj.includes('packageReferences = (')) {
246
- pbxproj = pbxproj.replace(
247
- 'packageReferences = (',
248
- `packageReferences = (\n\t\t\t\t${pkgRefUUID} /* XCRemoteSwiftPackageReference "mapbox-navigation-ios" */,`
249
- );
250
- }
251
-
252
- fs.writeFileSync(pbxprojPath, pbxproj, 'utf8');
253
- console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Injected Mapbox Navigation SPM into project.pbxproj');
254
-
255
- return mod;
256
- },
257
- ]);
258
-
259
84
  return config;
260
85
  };
261
86
 
262
- // ── Android helpers ──────────────────────────────────────────────────────────
87
+ // ── Android helpers ───────────────────────────────────────────────────────────
263
88
 
264
89
  function addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides) {
265
- const MAPBOX_DOWNLOADS_TOKEN_KEY = 'MAPBOX_DOWNLOADS_TOKEN';
266
- // (kept for backward compat — actual token is in gradle.properties)
267
-
268
90
  if (!mod.modResults.contents.includes('abiFilters')) {
269
91
  mod.modResults.contents = mod.modResults.contents.replace(
270
92
  /defaultConfig {([\s\S]*?)}/,
@@ -317,9 +139,22 @@ function addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides) {
317
139
  `;
318
140
  }
319
141
 
142
+ // Android color overrides for Mapbox resource colors (route line, banner, etc.)
320
143
  if (Object.keys(androidColorOverrides).length > 0) {
321
- // inject color overrides as Android resource values
322
- // (handled downstream by the Mapbox gradle plugin or manual resource injection)
144
+ const resDir = path.join(
145
+ mod.modRequest?.platformProjectRoot || '',
146
+ 'app', 'src', 'main', 'res', 'values'
147
+ );
148
+ try {
149
+ fs.mkdirSync(resDir, { recursive: true });
150
+ const colorEntries = Object.entries(androidColorOverrides)
151
+ .map(([name, value]) => ` <color name="${name}">${value}</color>`)
152
+ .join('\n');
153
+ const xmlContent = `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`;
154
+ fs.writeFileSync(path.join(resDir, 'mapbox_color_overrides.xml'), xmlContent);
155
+ } catch (e) {
156
+ // Ignore — resDir may not exist at plugin resolution time
157
+ }
323
158
  }
324
159
  }
325
160