@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.
- package/ExpoMapboxNavigation.podspec +27 -33
- package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +47 -5
- package/app.plugin.js +298 -169
- package/ios/ExpoMapboxNavigationModule.swift +49 -27
- package/ios/ExpoMapboxNavigationView.swift +122 -116
- package/package.json +1 -1
- package/plugin/src/index.js +298 -169
|
@@ -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
|
|
19
|
+
# ── iOS: Mapbox Navigation SDK v3 ───────────────────────────────────────────
|
|
26
20
|
#
|
|
27
|
-
# IMPORTANT
|
|
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
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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'
|
|
52
|
-
'SWIFT_COMPILATION_MODE'
|
|
53
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
939
|
-
fun
|
|
940
|
-
|
|
941
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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;
|