@jacques_gordon/expo-mapbox-navigation 2.2.5 → 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
@@ -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,201 +1,54 @@
1
- const { withAppBuildGradle, withProjectBuildGradle, withAndroidManifest, withInfoPlist, withDangerousMod, withXcodeProject } = require('@expo/config-plugins');
1
+ const {
2
+ withAppBuildGradle,
3
+ withAndroidManifest,
4
+ withInfoPlist,
5
+ withDangerousMod,
6
+ } = require('@expo/config-plugins');
2
7
  const fs = require('fs');
3
8
  const path = require('path');
4
9
 
5
- const NDK_VERSION = '27.0.12077973';
6
- const MAPBOX_MAPS_MIN_VERSION = '11.11.0';
7
-
8
10
  const withMapboxNavigation = (config, options = {}) => {
9
11
  const {
10
12
  accessToken,
11
13
  downloadsToken,
12
- mapboxMapsVersion = MAPBOX_MAPS_MIN_VERSION,
14
+ mapboxMapsVersion = '11.11.0',
13
15
  androidColorOverrides = {},
14
16
  } = options;
15
17
 
16
18
  if (!accessToken) {
17
19
  throw new Error(
18
- '[@jacques_gordon/expo-mapbox-navigation] `accessToken` is required in the plugin config.\n' +
19
- 'Add it to your app.json plugins array:\n' +
20
- ' ["@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" }]'
21
22
  );
22
23
  }
23
24
 
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
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
- 'used to authenticate Swift Package Manager when fetching the Mapbox Navigation SDK.\n' +
34
- 'Add it to your app.json plugins array:\n' +
27
+ '[@jacques_gordon/expo-mapbox-navigation] `downloadsToken` (secret sk.* token) is required for iOS.\n' +
35
28
  ' ["@jacques_gordon/expo-mapbox-navigation", { "accessToken": "pk.xxx", "downloadsToken": "sk.xxx" }]'
36
29
  );
37
30
  }
38
31
 
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 ─────────────────────────────────────
32
+ // ── Android ───────────────────────────────────────────────────────────────
76
33
  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;
34
+ addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides);
110
35
  return mod;
111
36
  });
112
37
 
113
- // ── Android: AndroidManifest ─────────────────────────────────────────────
114
38
  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
-
39
+ addAndroidPermissions(mod, accessToken);
160
40
  return mod;
161
41
  });
162
42
 
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 ──────────────────────────────────────────────────────
43
+ // ── iOS: Info.plist MBXAccessToken + permissions ────────────────────────
188
44
  config = withInfoPlist(config, (mod) => {
189
45
  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
- }
46
+ mod.modResults.NSLocationWhenInUseUsageDescription =
47
+ mod.modResults.NSLocationWhenInUseUsageDescription ||
48
+ 'Your location is used for navigation.';
49
+ mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription =
50
+ mod.modResults.NSLocationAlwaysAndWhenInUseUsageDescription ||
51
+ 'Your location is used for navigation.';
199
52
  if (!mod.modResults.UIBackgroundModes) mod.modResults.UIBackgroundModes = [];
200
53
  for (const mode of ['audio', 'location']) {
201
54
  if (!mod.modResults.UIBackgroundModes.includes(mode)) {
@@ -205,82 +58,145 @@ configurations.all {
205
58
  return mod;
206
59
  });
207
60
 
208
- // ── 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.
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.
215
66
  config = withDangerousMod(config, [
216
67
  'ios',
217
68
  (mod) => {
218
69
  const homeDir = require('os').homedir();
219
70
  const netrcPath = path.join(homeDir, '.netrc');
220
71
  const netrcEntry = `machine api.mapbox.com\nlogin mapbox\npassword ${downloadsToken}\n`;
221
-
222
72
  let existingContent = '';
223
73
  if (fs.existsSync(netrcPath)) {
224
74
  existingContent = fs.readFileSync(netrcPath, 'utf8');
225
75
  }
226
-
227
76
  if (!existingContent.includes('machine api.mapbox.com')) {
228
77
  fs.writeFileSync(netrcPath, existingContent + netrcEntry, { mode: 0o600 });
78
+ console.log('[@jacques_gordon/expo-mapbox-navigation] ✅ Wrote Mapbox credentials to ~/.netrc');
229
79
  }
230
-
231
80
  return mod;
232
81
  },
233
82
  ]);
234
83
 
235
- // ── iOS: Add Mapbox Navigation SPM to Xcode project main target ───────────
236
- //
237
- // WHY NOT spm_dependency() IN THE PODSPEC:
238
- // Using spm_dependency() in ExpoMapboxNavigation.podspec causes
239
- // "43029 duplicate symbols" linker errors (React Native GitHub #47344,
240
- // Expo GitHub #37813). The framework ends up linked in both the Pod target
241
- // AND the main app target, so the linker sees every symbol twice.
242
- //
243
- // THE CORRECT APPROACH:
244
- // Add MapboxNavigation as a Swift Package directly to the Xcode project's
245
- // main target via withXcodeProject. This ensures only ONE copy of each
246
- // framework is linked in the final binary — exactly the same technique
247
- // used by rnmapbox/maps itself for MapboxMaps.
248
- config = withXcodeProject(config, (mod) => {
249
- const xcodeProject = mod.modResults;
250
- const MAPBOX_NAV_REPO = 'https://github.com/mapbox/mapbox-navigation-ios.git';
251
- const MAPBOX_NAV_VERSION = '3.25.0';
84
+ return config;
85
+ };
86
+
87
+ // ── Android helpers ───────────────────────────────────────────────────────────
252
88
 
253
- // Check if already added to avoid duplicates on repeated prebuild
254
- const existingPackages = xcodeProject.pbxXCRemoteSwiftPackageReferenceSection
255
- ? Object.values(xcodeProject.pbxXCRemoteSwiftPackageReferenceSection())
256
- : [];
257
- const alreadyAdded = existingPackages.some(
258
- (pkg) => pkg && pkg.repositoryURL && pkg.repositoryURL.includes('mapbox-navigation-ios')
89
+ function addAndroidConfig(mod, mapboxMapsVersion, androidColorOverrides) {
90
+ if (!mod.modResults.contents.includes('abiFilters')) {
91
+ mod.modResults.contents = mod.modResults.contents.replace(
92
+ /defaultConfig {([\s\S]*?)}/,
93
+ `defaultConfig {\n ndk {\n abiFilters "arm64-v8a", "x86_64"\n }\n $1\n }`
259
94
  );
95
+ }
260
96
 
261
- if (!alreadyAdded) {
262
- // Add the SPM package reference to the project
263
- const packageRef = xcodeProject.addSwiftPackage(
264
- MAPBOX_NAV_REPO,
265
- {
266
- repositoryURL: MAPBOX_NAV_REPO,
267
- requirement: {
268
- kind: 'upToNextMajorVersion',
269
- minimumVersion: MAPBOX_NAV_VERSION,
270
- },
271
- },
272
- [
273
- // Products from MapboxNavigation SPM package that our Swift files import
274
- { product: 'MapboxNavigationCore' },
275
- { product: 'MapboxNavigationUIKit' },
276
- ]
277
- );
97
+ const workVersion = '2.8.0';
98
+ mod.modResults.contents = mod.modResults.contents.replace(
99
+ /implementation ['"]androidx.work:work-runtime-ktx:[\d.]+['"]/,
100
+ ''
101
+ );
102
+ if (!mod.modResults.contents.includes('work-runtime')) {
103
+ mod.modResults.contents += `
104
+ dependencies {
105
+ implementation 'androidx.work:work-runtime:${workVersion}'
106
+ implementation 'androidx.work:work-runtime-ktx:${workVersion}'
278
107
  }
108
+ `;
109
+ }
279
110
 
280
- return mod;
281
- });
111
+ if (!mod.modResults.contents.includes('dependencySubstitution')) {
112
+ const MAPS_VER = mapboxMapsVersion || '11.11.0';
113
+ const COMMON_VER = '24.11.3';
114
+ mod.modResults.contents += `
115
+ configurations.all {
116
+ resolutionStrategy {
117
+ dependencySubstitution {
118
+ substitute module('com.mapbox.maps:android') using module('com.mapbox.maps:android-ndk27:${MAPS_VER}')
119
+ substitute module('com.mapbox.maps:android-core') using module('com.mapbox.maps:android-core-ndk27:${MAPS_VER}')
120
+ substitute module('com.mapbox.maps:base') using module('com.mapbox.maps:base-ndk27:${MAPS_VER}')
121
+ substitute module('com.mapbox.common:common') using module('com.mapbox.common:common-ndk27:${COMMON_VER}')
122
+ substitute module('com.mapbox.module:maps-telemetry') using module('com.mapbox.module:maps-telemetry-ndk27:${MAPS_VER}')
123
+ substitute module('com.mapbox.plugin:maps-attribution') using module('com.mapbox.plugin:maps-attribution-ndk27:${MAPS_VER}')
124
+ substitute module('com.mapbox.plugin:maps-scalebar') using module('com.mapbox.plugin:maps-scalebar-ndk27:${MAPS_VER}')
125
+ substitute module('com.mapbox.plugin:maps-gestures') using module('com.mapbox.plugin:maps-gestures-ndk27:${MAPS_VER}')
126
+ substitute module('com.mapbox.plugin:maps-logo') using module('com.mapbox.plugin:maps-logo-ndk27:${MAPS_VER}')
127
+ substitute module('com.mapbox.plugin:maps-compass') using module('com.mapbox.plugin:maps-compass-ndk27:${MAPS_VER}')
128
+ substitute module('com.mapbox.plugin:maps-lifecycle') using module('com.mapbox.plugin:maps-lifecycle-ndk27:${MAPS_VER}')
129
+ substitute module('com.mapbox.plugin:maps-animation') using module('com.mapbox.plugin:maps-animation-ndk27:${MAPS_VER}')
130
+ substitute module('com.mapbox.plugin:maps-overlay') using module('com.mapbox.plugin:maps-overlay-ndk27:${MAPS_VER}')
131
+ substitute module('com.mapbox.plugin:maps-annotation') using module('com.mapbox.plugin:maps-annotation-ndk27:${MAPS_VER}')
132
+ substitute module('com.mapbox.plugin:maps-locationcomponent') using module('com.mapbox.plugin:maps-locationcomponent-ndk27:${MAPS_VER}')
133
+ substitute module('com.mapbox.plugin:maps-viewport') using module('com.mapbox.plugin:maps-viewport-ndk27:${MAPS_VER}')
134
+ substitute module('com.mapbox.extension:maps-localization') using module('com.mapbox.extension:maps-localization-ndk27:${MAPS_VER}')
135
+ substitute module('com.mapbox.extension:maps-style') using module('com.mapbox.extension:maps-style-ndk27:${MAPS_VER}')
136
+ }
137
+ }
138
+ }
139
+ `;
140
+ }
282
141
 
283
- return config;
284
- };
142
+ // Android color overrides for Mapbox resource colors (route line, banner, etc.)
143
+ if (Object.keys(androidColorOverrides).length > 0) {
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
+ }
158
+ }
159
+ }
160
+
161
+ function addAndroidPermissions(mod, accessToken) {
162
+ const manifest = mod.modResults.manifest;
163
+ const application = manifest.application[0];
164
+
165
+ if (!application['meta-data']) application['meta-data'] = [];
166
+ const existingToken = application['meta-data'].find(
167
+ (item) => item['$']['android:name'] === 'com.mapbox.token'
168
+ );
169
+ if (!existingToken) {
170
+ application['meta-data'].push({
171
+ $: { 'android:name': 'com.mapbox.token', 'android:value': accessToken },
172
+ });
173
+ }
174
+
175
+ const requiredPermissions = [
176
+ 'android.permission.ACCESS_FINE_LOCATION',
177
+ 'android.permission.ACCESS_COARSE_LOCATION',
178
+ 'android.permission.FOREGROUND_SERVICE',
179
+ 'android.permission.FOREGROUND_SERVICE_LOCATION',
180
+ 'android.permission.POST_NOTIFICATIONS',
181
+ ];
182
+ if (!manifest['uses-permission']) manifest['uses-permission'] = [];
183
+ for (const perm of requiredPermissions) {
184
+ if (!manifest['uses-permission'].find((p) => p['$']['android:name'] === perm)) {
185
+ manifest['uses-permission'].push({ $: { 'android:name': perm } });
186
+ }
187
+ }
188
+
189
+ if (!manifest.service) manifest.service = [];
190
+ const svcName = 'com.mapbox.navigation.core.trip.service.NavigationNotificationService';
191
+ if (!manifest.service.find((s) => s['$']['android:name'] === svcName)) {
192
+ manifest.service.push({
193
+ $: {
194
+ 'android:name': svcName,
195
+ 'android:foregroundServiceType': 'location',
196
+ 'android:exported': 'false',
197
+ },
198
+ });
199
+ }
200
+ }
285
201
 
286
202
  module.exports = withMapboxNavigation;