@jacques_gordon/expo-mapbox-navigation 2.2.5 → 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.
@@ -1,15 +1,20 @@
1
- const { withAppBuildGradle, withProjectBuildGradle, withAndroidManifest, withInfoPlist, withDangerousMod, withXcodeProject } = 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,81 +65,303 @@ 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
  }
230
-
231
81
  return mod;
232
82
  },
233
83
  ]);
234
84
 
235
- // ── iOS: Add Mapbox Navigation SPM to Xcode project main target ───────────
85
+ // ── iOS: Inject Mapbox Navigation SPM into project.pbxproj ───────────────
236
86
  //
237
87
  // 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.
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.
242
91
  //
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';
252
-
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')
259
- );
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
+ );
260
113
 
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
- ]
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}`
277
207
  );
278
- }
279
208
 
280
- return mod;
281
- });
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
+ ]);
282
258
 
283
259
  return config;
284
260
  };
285
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
+
286
367
  module.exports = withMapboxNavigation;