@jacques_gordon/expo-mapbox-navigation 2.2.4 → 2.2.6

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.
@@ -10,46 +10,40 @@ Pod::Spec.new do |s|
10
10
  s.license = package['license']
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
-
14
- # ── 16 KB / SPM minimum: Mapbox Navigation SDK v3 for iOS requires iOS 14+,
15
- # but Swift Package Manager dependencies resolved through CocoaPods'
16
- # spm_dependency() mechanism require iOS 15.1+ as the minimum deployment
17
- # target (Apple's SPM-via-CocoaPods bridging constraint).
18
- s.platforms = { :ios => '15.1' }
13
+ s.platforms = { :ios => '14.0' }
19
14
  s.swift_version = '5.9'
20
15
  s.source = { git: package['repository']['url'], tag: "v#{s.version}" }
21
- s.static_framework = true
22
16
 
23
17
  s.dependency 'ExpoModulesCore'
24
18
 
25
- # ── iOS: Mapbox Navigation SDK v3 via Swift Package Manager ─────────────────
19
+ # ── iOS: Mapbox Navigation SDK v3 ───────────────────────────────────────────
26
20
  #
27
- # IMPORTANT: earlier versions of this podspec referenced prebuilt
28
- # .xcframework files (MapboxNavigationUIKit.xcframework,
29
- # MapboxNavigationCore.xcframework, MapboxDirections.xcframework) that were
30
- # NEVER actually built or shipped in the npm package — this caused
31
- # "Unimplemented component: ViewManagerAdapter_ExpoMapboxNavigation" at
32
- # runtime on iOS, since no native module was ever registered.
21
+ # IMPORTANT do NOT use spm_dependency() here.
33
22
  #
34
- # The Mapbox Navigation SDK v3 for iOS is officially distributed ONLY via
35
- # Swift Package Manager (CocoaPods support is not provided by Mapbox).
36
- # We use CocoaPods' built-in spm_dependency() helper (available since
37
- # CocoaPods 1.13+, and exactly the mechanism Expo's own modules use for
38
- # this exact situation) to pull the SPM package transparently as part of
39
- # `pod install` / `expo prebuild`. No manual Xcode steps, no vendored
40
- # binaries to build or maintain.
41
- spm_dependency(
42
- s,
43
- url: 'https://github.com/mapbox/mapbox-navigation-ios.git',
44
- requirement: { kind: 'upToNextMajorVersion', minimumVersion: '3.24.2' },
45
- products: ['MapboxNavigationCore', 'MapboxNavigationUIKit', 'MapboxDirections']
46
- )
47
-
48
- s.source_files = 'ios/**/*.{swift,h,m,mm}'
49
-
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.
28
+ #
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.
50
39
  s.pod_target_xcconfig = {
51
- 'DEFINES_MODULE' => 'YES',
52
- 'SWIFT_COMPILATION_MODE' => 'wholemodule',
53
- 'IPHONEOS_DEPLOYMENT_TARGET' => '15.1'
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',
54
46
  }
47
+
48
+ s.source_files = 'ios/**/*.{swift,h,m,mm}'
55
49
  end
@@ -933,12 +933,54 @@ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) :
933
933
  fun setUseMapMatching(u: Boolean) { useMapMatching = u }
934
934
  fun setCustomRasterTileUrl(u: String?) { customRasterTileUrl = u }
935
935
  fun setCustomRasterAboveLayerId(l: String?) { customRasterAboveLayerId = l }
936
- fun setManeuverBackgroundColorDay(c: String?) { maneuverBackgroundColorDay = c }
936
+
937
+ fun setManeuverBackgroundColorDay(c: String?) {
938
+ maneuverBackgroundColorDay = c
939
+ // ManeuverView is constructed with options in buildUI() which runs before
940
+ // props are received — we cannot rebuild it, but the color can be applied
941
+ // by updating the ManeuverView background tint directly on the view itself.
942
+ c?.let { hex ->
943
+ try { maneuverView?.setBackgroundColor(Color.parseColor(hex)) } catch (e: Exception) {}
944
+ }
945
+ }
946
+
937
947
  fun setManeuverTurnIconColor(c: String?) { maneuverTurnIconColor = c }
938
- fun setEtaBarBackgroundColor(c: String?) { etaBarBackgroundColor = c }
939
- fun setEtaTextColor(c: String?) { etaTextColor = c }
940
- fun setIconButtonColor(c: String?) { iconButtonColor = c }
941
- fun setIconButtonMutedColor(c: String?) { iconButtonMutedColor = c }
948
+
949
+ fun setEtaBarBackgroundColor(c: String?) {
950
+ etaBarBackgroundColor = c
951
+ c?.let { hex ->
952
+ try { etaBar?.setBackgroundColor(Color.parseColor(hex)) } catch (e: Exception) {}
953
+ }
954
+ }
955
+
956
+ fun setEtaTextColor(c: String?) {
957
+ etaTextColor = c
958
+ c?.let { hex ->
959
+ try {
960
+ val color = Color.parseColor(hex)
961
+ tvEtaTime?.setTextColor(color)
962
+ tvDuration?.setTextColor(color)
963
+ } catch (e: Exception) {}
964
+ }
965
+ }
966
+
967
+ fun setIconButtonColor(c: String?) {
968
+ iconButtonColor = c
969
+ // Redraw overview and recenter icons with new color
970
+ btnOverviewView?.setImageBitmap(drawRouteOverviewIcon())
971
+ // Recenter keeps its current alpha (0.4 following / 1.0 overview)
972
+ val currentAlpha = btnRecenterView?.alpha ?: 0.4f
973
+ btnRecenterView?.setImageBitmap(drawNavigationArrowIcon())
974
+ btnRecenterView?.alpha = currentAlpha
975
+ // Redraw mute icon only if not currently muted (muted uses iconButtonMutedColor)
976
+ if (!isMuted) btnMuteView?.setImageBitmap(drawSpeakerIcon(false))
977
+ }
978
+
979
+ fun setIconButtonMutedColor(c: String?) {
980
+ iconButtonMutedColor = c
981
+ // Redraw mute icon only if currently muted
982
+ if (isMuted) btnMuteView?.setImageBitmap(drawSpeakerIcon(true))
983
+ }
942
984
 
943
985
  override fun onDetachedFromWindow() {
944
986
  super.onDetachedFromWindow()
package/app.plugin.js CHANGED
@@ -1,15 +1,20 @@
1
- const { withAppBuildGradle, withProjectBuildGradle, withAndroidManifest, withInfoPlist, withDangerousMod } = require('@expo/config-plugins');
1
+ const {
2
+ withAppBuildGradle,
3
+ withProjectBuildGradle,
4
+ withAndroidManifest,
5
+ withInfoPlist,
6
+ withDangerousMod,
7
+ } = require('@expo/config-plugins');
8
+ const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');
2
9
  const fs = require('fs');
3
10
  const path = require('path');
4
11
 
5
- const NDK_VERSION = '27.0.12077973';
6
- const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
7
12
 
8
13
  const withMapboxNavigation = (config, options = {}) => {
9
14
  const {
10
15
  accessToken,
11
16
  downloadsToken,
12
- mapboxMapsVersion = MAPBOX_MAPS_MIN_VERSION,
17
+ mapboxMapsVersion = '11.11.0',
13
18
  androidColorOverrides = {},
14
19
  } = options;
15
20
 
@@ -21,181 +26,35 @@ const withMapboxNavigation = (config, options = {}) => {
21
26
  );
22
27
  }
23
28
 
24
- // downloadsToken (a secret sk.* token with the Downloads:Read scope) is
25
- // required on iOS to authenticate Swift Package Manager's fetch of the
26
- // Mapbox Navigation SDK package — without it, `pod install`/`expo
27
- // prebuild` will fail to resolve the SPM dependency declared in
28
- // ExpoMapboxNavigation.podspec.
29
29
  if (!downloadsToken) {
30
30
  throw new Error(
31
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
- 'used to authenticate Swift Package Manager when fetching the Mapbox Navigation SDK.\n' +
32
+ 'This must be a SECRET Mapbox token (starts with "sk.") with the "Downloads:Read" scope.\n' +
34
33
  'Add it to your app.json plugins array:\n' +
35
34
  ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
36
35
  );
37
36
  }
38
37
 
39
- const [major, minor] = mapboxMapsVersion.split('.').map(Number);
40
- if (major < 11 || (major === 11 && minor < 11)) {
41
- throw new Error(
42
- `[@jacques_gordon/expo-mapbox-navigation] mapboxMapsVersion must be >= 11.11.0.\n` +
43
- `Provided: ${mapboxMapsVersion}`
44
- );
45
- }
46
-
47
- // ── Android: project-level build.gradle ─────────────────────────────────
48
- config = withProjectBuildGradle(config, (mod) => {
49
- let contents = mod.modResults.contents;
50
-
51
- if (!contents.includes('api.mapbox.com/downloads/v2/releases/maven')) {
52
- const mavenBlock = `
53
- maven {
54
- url 'https://api.mapbox.com/downloads/v2/releases/maven'
55
- authentication { basic(BasicAuthentication) }
56
- credentials {
57
- username = 'mapbox'
58
- password = project.hasProperty('MAPBOX_DOWNLOADS_TOKEN')
59
- ? project.property('MAPBOX_DOWNLOADS_TOKEN')
60
- : System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: ""
61
- }
62
- }`;
63
- if (contents.includes('allprojects') && contents.includes('repositories')) {
64
- contents = contents.replace(
65
- /allprojects\s*\{[\s\S]*?repositories\s*\{/,
66
- (match) => match + mavenBlock
67
- );
68
- }
69
- }
70
-
71
- mod.modResults.contents = contents;
72
- return mod;
73
- });
74
-
75
- // ── Android: app-level build.gradle ─────────────────────────────────────
38
+ // ── Android ───────────────────────────────────────────────────────────────
76
39
  config = withAppBuildGradle(config, (mod) => {
77
- let contents = mod.modResults.contents;
78
-
79
- if (!contents.includes('ndkVersion')) {
80
- contents = contents.replace(
81
- /android\s*\{/,
82
- `android {\n ndkVersion "${NDK_VERSION}"`
83
- );
84
- } else if (!contents.includes(NDK_VERSION)) {
85
- contents = contents.replace(
86
- /ndkVersion\s+["'][^"']*["']/,
87
- `ndkVersion "${NDK_VERSION}"`
88
- );
89
- }
90
-
91
- if (!contents.includes('useLegacyPackaging')) {
92
- contents = contents.replace(
93
- /android\s*\{/,
94
- `android {\n packagingOptions {\n jniLibs {\n useLegacyPackaging = false\n }\n }`
95
- );
96
- }
97
-
98
- const resolutionBlock = `
99
- configurations.all {
100
- resolutionStrategy {
101
- force "com.mapbox.maps:android:${mapboxMapsVersion}"
102
- force "com.mapbox.maps:android-ndk27:${mapboxMapsVersion}"
103
- }
104
- }`;
105
- if (!contents.includes('com.mapbox.maps:android:')) {
106
- contents = contents + '\n' + resolutionBlock;
107
- }
108
-
109
- mod.modResults.contents = contents;
40
+ addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides);
110
41
  return mod;
111
42
  });
112
43
 
113
- // ── Android: AndroidManifest ─────────────────────────────────────────────
114
44
  config = withAndroidManifest(config, (mod) => {
115
- const manifest = mod.modResults.manifest;
116
-
117
- // ── Permissions required by Mapbox Navigation SDK (targetSdk 35) ────────
118
- //
119
- // SecurityException: Starting FGS with type location requires:
120
- // - android.permission.FOREGROUND_SERVICE_LOCATION (mandatory API 34+)
121
- // - android.permission.FOREGROUND_SERVICE (mandatory)
122
- // - android.permission.ACCESS_FINE_LOCATION (at least one of coarse/fine)
123
- // - android.permission.ACCESS_COARSE_LOCATION
124
- //
125
- const requiredPermissions = [
126
- 'android.permission.ACCESS_FINE_LOCATION',
127
- 'android.permission.ACCESS_COARSE_LOCATION',
128
- 'android.permission.FOREGROUND_SERVICE',
129
- 'android.permission.FOREGROUND_SERVICE_LOCATION', // ← fixes the crash
130
- 'android.permission.POST_NOTIFICATIONS', // needed for nav notification (API 33+)
131
- ];
132
-
133
- if (!manifest['uses-permission']) {
134
- manifest['uses-permission'] = [];
135
- }
136
-
137
- for (const permission of requiredPermissions) {
138
- const already = manifest['uses-permission'].some(
139
- (p) => p.$?.['android:name'] === permission
140
- );
141
- if (!already) {
142
- manifest['uses-permission'].push({ $: { 'android:name': permission } });
143
- }
144
- }
145
-
146
- // ── Mapbox access token meta-data ────────────────────────────────────────
147
- const mainApp = manifest.application?.[0];
148
- if (mainApp) {
149
- if (!mainApp['meta-data']) mainApp['meta-data'] = [];
150
- const tokenMeta = mainApp['meta-data'].find(
151
- (m) => m.$?.['android:name'] === 'com.mapbox.token'
152
- );
153
- if (!tokenMeta) {
154
- mainApp['meta-data'].push({
155
- $: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
156
- });
157
- }
158
- }
159
-
45
+ addAndroidPermissions(mod, accessToken);
160
46
  return mod;
161
47
  });
162
48
 
163
- // ── Android: color overrides ─────────────────────────────────────────────
164
- if (Object.keys(androidColorOverrides).length > 0) {
165
- config = withDangerousMod(config, [
166
- 'android',
167
- async (mod) => {
168
- const colorsDir = path.join(
169
- mod.modRequest.platformProjectRoot,
170
- 'app/src/main/res/values'
171
- );
172
- if (!fs.existsSync(colorsDir)) fs.mkdirSync(colorsDir, { recursive: true });
173
-
174
- const colorEntries = Object.entries(androidColorOverrides)
175
- .map(([name, value]) => ` <color name="${name}">${value}</color>`)
176
- .join('\n');
177
-
178
- fs.writeFileSync(
179
- path.join(colorsDir, 'mapbox_navigation_colors.xml'),
180
- `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n${colorEntries}\n</resources>\n`
181
- );
182
- return mod;
183
- },
184
- ]);
185
- }
186
-
187
- // ── iOS: Info.plist ──────────────────────────────────────────────────────
49
+ // ── iOS: MBXAccessToken in Info.plist ────────────────────────────────────
188
50
  config = withInfoPlist(config, (mod) => {
189
51
  mod.modResults.MBXAccessToken = accessToken;
190
-
191
- if (!mod.modResults.NSLocationWhenInUseUsageDescription) {
192
- mod.modResults.NSLocationWhenInUseUsageDescription =
193
- 'Your location is used for turn-by-turn navigation.';
194
- }
195
- if (!mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription) {
196
- mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
197
- 'Your location is used for turn-by-turn navigation even when the app is in the background.';
198
- }
52
+ mod.modResults.NSLocationWhenInUseUsageDescription =
53
+ mod.modResults.NSLocationWhenInUseUsageDescription ||
54
+ 'Your location is used for navigation.';
55
+ mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
56
+ mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription ||
57
+ 'Your location is used for navigation.';
199
58
  if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
200
59
  for (const mode of ['audio', 'location']) {
201
60
  if (!mod.modResults.UIBackgroundModes.includes(mode)) {
@@ -206,27 +65,192 @@ configurations.all {
206
65
  });
207
66
 
208
67
  // ── iOS: .netrc for SPM authentication ───────────────────────────────────
209
- // Swift Package Manager (used by ExpoMapboxNavigation.podspec's
210
- // spm_dependency() to fetch the Mapbox Navigation SDK) authenticates
211
- // against api.mapbox.com using a machine entry in the user's ~/.netrc
212
- // file — this is Mapbox's documented mechanism for private SPM package
213
- // access, identical in spirit to the MAPBOX_DOWNLOADS_TOKEN gradle
214
- // property used on Android.
215
68
  config = withDangerousMod(config, [
216
69
  'ios',
217
70
  (mod) => {
218
71
  const homeDir = require('os').homedir();
219
72
  const netrcPath = path.join(homeDir, '.netrc');
220
73
  const netrcEntry = `machine api.mapbox.com\nlogin mapbox\npassword ${downloadsToken}\n`;
221
-
222
74
  let existingContent = '';
223
75
  if (fs.existsSync(netrcPath)) {
224
76
  existingContent = fs.readFileSync(netrcPath, 'utf8');
225
77
  }
226
-
227
78
  if (!existingContent.includes('machine api.mapbox.com')) {
228
79
  fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
229
80
  }
81
+ return mod;
82
+ },
83
+ ]);
84
+
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');
230
254
 
231
255
  return mod;
232
256
  },
@@ -235,4 +259,109 @@ configurations.all {
235
259
  return config;
236
260
  };
237
261
 
262
+ // ── Android helpers ──────────────────────────────────────────────────────────
263
+
264
+ 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
+ if (!mod.modResults.contents.includes('abiFilters')) {
269
+ mod.modResults.contents = mod.modResults.contents.replace(
270
+ /defaultConfig {([\s\S]*?)}/,
271
+ `defaultConfig {\n ndk {\n abiFilters "arm64-v8a", "x86_64"\n }\n $1\n }`
272
+ );
273
+ }
274
+
275
+ const workVersion = '2.8.0';
276
+ mod.modResults.contents = mod.modResults.contents.replace(
277
+ /implementation ['"]androidx.work:work-runtime-ktx:[\d.]+['"]/,
278
+ ''
279
+ );
280
+ if (!mod.modResults.contents.includes('work-runtime')) {
281
+ mod.modResults.contents += `
282
+ dependencies {
283
+ implementation 'androidx.work:work-runtime:${workVersion}'
284
+ implementation 'androidx.work:work-runtime-ktx:${workVersion}'
285
+ }
286
+ `;
287
+ }
288
+
289
+ if (!mod.modResults.contents.includes('dependencySubstitution')) {
290
+ const MAPS_VER = mapboxMapsVersion || '11.11.0';
291
+ const COMMON_VER = '24.11.3';
292
+ mod.modResults.contents += `
293
+ configurations.all {
294
+ resolutionStrategy {
295
+ dependencySubstitution {
296
+ substitute module('com.mapbox.maps:android') using module('com.mapbox.maps:android-ndk27:${MAPS_VER}')
297
+ substitute module('com.mapbox.maps:android-core') using module('com.mapbox.maps:android-core-ndk27:${MAPS_VER}')
298
+ substitute module('com.mapbox.maps:base') using module('com.mapbox.maps:base-ndk27:${MAPS_VER}')
299
+ substitute module('com.mapbox.common:common') using module('com.mapbox.common:common-ndk27:${COMMON_VER}')
300
+ substitute module('com.mapbox.module:maps-telemetry') using module('com.mapbox.module:maps-telemetry-ndk27:${MAPS_VER}')
301
+ substitute module('com.mapbox.plugin:maps-attribution') using module('com.mapbox.plugin:maps-attribution-ndk27:${MAPS_VER}')
302
+ substitute module('com.mapbox.plugin:maps-scalebar') using module('com.mapbox.plugin:maps-scalebar-ndk27:${MAPS_VER}')
303
+ substitute module('com.mapbox.plugin:maps-gestures') using module('com.mapbox.plugin:maps-gestures-ndk27:${MAPS_VER}')
304
+ substitute module('com.mapbox.plugin:maps-logo') using module('com.mapbox.plugin:maps-logo-ndk27:${MAPS_VER}')
305
+ substitute module('com.mapbox.plugin:maps-compass') using module('com.mapbox.plugin:maps-compass-ndk27:${MAPS_VER}')
306
+ substitute module('com.mapbox.plugin:maps-lifecycle') using module('com.mapbox.plugin:maps-lifecycle-ndk27:${MAPS_VER}')
307
+ substitute module('com.mapbox.plugin:maps-animation') using module('com.mapbox.plugin:maps-animation-ndk27:${MAPS_VER}')
308
+ substitute module('com.mapbox.plugin:maps-overlay') using module('com.mapbox.plugin:maps-overlay-ndk27:${MAPS_VER}')
309
+ substitute module('com.mapbox.plugin:maps-annotation') using module('com.mapbox.plugin:maps-annotation-ndk27:${MAPS_VER}')
310
+ substitute module('com.mapbox.plugin:maps-locationcomponent') using module('com.mapbox.plugin:maps-locationcomponent-ndk27:${MAPS_VER}')
311
+ substitute module('com.mapbox.plugin:maps-viewport') using module('com.mapbox.plugin:maps-viewport-ndk27:${MAPS_VER}')
312
+ substitute module('com.mapbox.extension:maps-localization') using module('com.mapbox.extension:maps-localization-ndk27:${MAPS_VER}')
313
+ substitute module('com.mapbox.extension:maps-style') using module('com.mapbox.extension:maps-style-ndk27:${MAPS_VER}')
314
+ }
315
+ }
316
+ }
317
+ `;
318
+ }
319
+
320
+ 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)
323
+ }
324
+ }
325
+
326
+ function addAndroidPermissions(mod, accessToken) {
327
+ const manifest = mod.modResults.manifest;
328
+ const application = manifest.application[0];
329
+
330
+ if (!application['meta-data']) application['meta-data'] = [];
331
+ const existingToken = application['meta-data'].find(
332
+ (item) => item['$']['android:name'] === 'com.mapbox.token'
333
+ );
334
+ if (!existingToken) {
335
+ application['meta-data'].push({
336
+ $: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
337
+ });
338
+ }
339
+
340
+ const requiredPermissions = [
341
+ 'android.permission.ACCESS_FINE_LOCATION',
342
+ 'android.permission.ACCESS_COARSE_LOCATION',
343
+ 'android.permission.FOREGROUND_SERVICE',
344
+ 'android.permission.FOREGROUND_SERVICE_LOCATION',
345
+ 'android.permission.POST_NOTIFICATIONS',
346
+ ];
347
+ if (!manifest['uses-permission']) manifest['uses-permission'] = [];
348
+ for (const perm of requiredPermissions) {
349
+ if (!manifest['uses-permission'].find((p) => p['$']['android:name'] === perm)) {
350
+ manifest['uses-permission'].push({ $: { 'android:name': perm } });
351
+ }
352
+ }
353
+
354
+ if (!manifest.service) manifest.service = [];
355
+ const svcName = 'com.mapbox.navigation.core.trip.service.NavigationNotificationService';
356
+ if (!manifest.service.find((s) => s['$']['android:name'] === svcName)) {
357
+ manifest.service.push({
358
+ $: {
359
+ 'android:name': svcName,
360
+ 'android:foregroundServiceType': 'location',
361
+ 'android:exported': 'false',
362
+ },
363
+ });
364
+ }
365
+ }
366
+
238
367
  module.exports = withMapboxNavigation;