@jacques_gordon/expo-mapbox-navigation
v5.0.3
Published
Expo module for Mapbox Navigation SDK with 16KB page size support, NDK27, and Mapbox Maps v11.11.0+
Maintainers
Readme
@jacques_gordon/expo-mapbox-navigation
Full-featured Expo module for Mapbox Navigation SDK v3 — Android and iOS.
Features
- Android — Waze-style navigation UI built from scratch: maneuver banner, lane guidance, speed limit, ETA bar, voice instructions, mute/overview/recenter buttons, day/night auto-switch
- iOS — Drop-in
NavigationViewControllerfrom Mapbox Navigation SDK v3 (lane guidance, speed limit, voice, day/night all built-in) - Both platforms — 7 events, 19 props, full feature and API parity
- NDK 27 + 16 KB page size compliant (Android 15+)
✅ Tested on iOS: real EAS production build, real device - launch crash confirmed gone.
✅ Tested on Android: no regressions from the iOS-only changes in 4.0.0 (that release touched iOS exclusively - ios/fetch-xcframeworks.sh, ios/patch-mapbox-library-evolution.sh, README/changelog only).
Installation
npx expo install @jacques_gordon/expo-mapbox-navigation @rnmapbox/maps1. Setup @rnmapbox/maps first
["@rnmapbox/maps", {
"RNMapboxMapsImpl": "mapbox",
"RNMapboxMapsVersion": "11.11.0",-- Android (editable), 11.20.2 -- IOS (fixed)
"RNMapboxMapsDownloadToken": "sk.your_secret_token"
}]2. Add this plugin
["@jacques_gordon/expo-mapbox-navigation", {
"accessToken": "pk.your_public_token",
"downloadsToken": "sk.your_secret_token",
"mapboxMapsVersion": "11.11.0"-- Android (editable), 11.20.2 -- IOS (fixed)
}]3. iOS only — enable static frameworks
["expo-build-properties", {
"ios": {
"useFrameworks": "static"
}
}]On the DYLD 4 Symbol missing: GestureType.singleTap launch crash: this project previously documented forceStaticLinking here as the confirmed fix for that crash. That claim has been retracted — direct binary analysis of a crashing build and a non-crashing build (both real production IPAs) showed identical dynamic linking for MapboxMaps/MapboxCommon/MapboxCoreMaps/Turf in both builds, meaning forceStaticLinking produced no observable difference either way. The actual difference found was that the two builds' MapboxMaps binaries were genuinely different files (different size, different hash) despite both reporting the identical version 11.20.2 — most likely because CocoaPods-trunk-resolved MapboxMaps (installed by @rnmapbox/maps, not vendored by this package) served different binary content at different times under the same version tag. See the changelog's 4.0.3 entry for the full retraction and the 4.0.0 entry (left intact) for the original, now-superseded reasoning. There is currently no confirmed, actionable fix for this crash to document here — if you hit it, treat version pinning/resolution of MapboxMaps via @rnmapbox/maps as the area to investigate, not this package's own config.
Confirmed working version set (interlocked — lift from the matching build-artifacts tag on bump):
nav 3.20.1 / MapboxNavigationNative 324.20.2 (bucket dash-native) /
MapboxCommon 24.20.2 / MapboxMaps 11.20.2 (matches @rnmapbox/[email protected]).Plugin Options
| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| accessToken | ✅ | — | Public Mapbox token (pk.*). Used for map tiles and routing. |
| downloadsToken | ✅ | — | Secret Mapbox token (sk.*) with Downloads:Read scope. Same token as RNMapboxMapsDownloadToken. Kept for backward compatibility with app.json configs from earlier versions — as of 2.3.0 it's no longer used at app-build time (the iOS SDK is vendored as prebuilt binaries; nothing downloads from api.mapbox.com during your pod install/EAS build anymore). |
| mapboxMapsVersion | ✅ | "11.11.0" — iOS fixed at 11.20.2 | Must exactly match RNMapboxMapsVersion in @rnmapbox/maps. Android only as of 2.3.0. As of 2.3.7, this genuinely drives the com.mapbox.maps:android and (indirectly, via mapboxNavigationVersion's auto-calculation) com.mapbox.navigationcore:* Gradle dependency versions — previously this option was silently ignored for that purpose and those versions were hardcoded. iOS SDK version is fixed per npm package release — see iOS Architecture. |
| mapboxNavigationVersion | — | auto-calculated (Android only) | Android only (reactivated in 2.3.7 — was deprecated/unused after 2.3.0's iOS rewrite). If set, used exactly as given for com.mapbox.navigationcore:* Gradle dependencies — no recalculation. If omitted, derived from mapboxMapsVersion via the Phase 1/Phase 2 formula (see iOS Architecture history) — a best-effort approximation, not a guarantee. Has no effect on iOS; the iOS SDK version is fixed by which npm package version you install. |
| androidColorOverrides | — | {} | Override Mapbox native resource colors on Android. |
iOS Architecture
Mapbox Navigation SDK v3 for iOS is distributed via Swift Package Manager only — Mapbox has not shipped CocoaPods support for it. This package works around that by vendoring the SDK's binaries directly. Mapbox officially publishes MapboxNavigationCore/MapboxNavigationUIKit/MapboxDirections (and their transitive binary dependencies) as precompiled .xcframework downloads via a dedicated repository, mapbox-navigation-ios-build-artifacts — ios/fetch-xcframeworks.sh fetches these directly into the consuming app's node_modules on its own first pod install.
What this means for you:
- No network access to
api.mapbox.comneeded during yourpod installor EAS build for the Navigation-specific frameworks themselves — only your owndownloadsTokenat fetch time. - No SPM package resolution happens in your project for this SDK at all.
useFrameworks: "static"(see step 3 of Installation) is required.forceStaticLinking, previously documented here as the fix for theDYLD 4 Symbol missing: GestureType.singleTaplaunch crash, has been retracted as of 4.0.3 — real A/B binary evidence showed it made no observable difference to linking type in either a crashing or non-crashing build. See the4.0.3changelog entry for the retraction and why the crash's real cause is currently unconfirmed.- The iOS SDK version is fixed by which version of this npm package you install (matching a specific
mapboxMapsVersion), not something you configure per-app.
Why MapboxMaps/MapboxCommon/MapboxCoreMaps/Turf are not vendored here: @rnmapbox/maps already installs those via CocoaPods. Vendoring a second copy of the same libraries would cause duplicate-symbol link errors. Only the Navigation-specific frameworks that @rnmapbox/maps doesn't already provide are vendored by this package.
Upgrading the vendored iOS SDK version (maintainers)
The iOS binaries are tied to a specific Navigation SDK version, matched to a specific MapboxMaps version. ios/Frameworks/*.xcframework is not committed to this repo (see Why iOS binaries aren't committed to this repo below) — ios/fetch-xcframeworks.sh fetches the Navigation-specific frameworks fresh each time they're needed. To bump the version:
- Confirm the target Navigation version's matching Maps version from that release's own "Packaging" changelog section at
https://github.com/mapbox/mapbox-navigation-ios/releases/tag/vX.Y.Z— don't assume a pattern holds, verify the exact release notes every time. - Update
MAPBOX_NAV_VERSIONat the top ofios/fetch-xcframeworks.sh. - Update
ExpoMapboxNavigation.podspec'ss.dependency 'MapboxMaps', '...'line to the exact same Maps version — this is intentionally hardcoded here, not read from any option or environment variable (see the comment directly above that line in the podspec for why). - Update
s.platforms/IPHONEOS_DEPLOYMENT_TARGETin the same podspec if the new MapboxMaps version raises its own minimum deployment target. - Also confirm
ios/ExpoMapboxNavigationModule.swift/ExpoMapboxNavigationView.swiftstill compile against the new SDK version's actual API — this project's own custom Swift source was built against a specific API generation once before and broke silently on a version downgrade (see the 3.1.9 changelog entry) without any warning until a real archive build. There is no automated check for this yet. - Run the "Verify Mapbox Navigation xcframeworks fetch" GitHub Actions workflow with the new version tag — this is a CI sanity check only (confirms
ios/fetch-xcframeworks.shstill works end-to-end against the new version), it does not publish or commit anything. npm version+npm publishas usual — the published tarball does not includeios/Frameworks/*.xcframework(see below); consumers fetch it themselves on their own firstpod install.
Why iOS binaries aren't committed to this repo
Mapbox's own Product Terms ("1.10. No Redistribution") prohibit redistributing their SDK binaries to third parties who haven't authenticated with their own Mapbox account/token. Since this repository and npm package are public, committing ios/Frameworks/*.xcframework (fetched using this project's own Mapbox DOWNLOADS:READ token) would make them freely downloadable by anyone via git clone or npm install, without their own token — a real compliance risk, not just a repo-size concern.
Instead, ios/Frameworks/*.xcframework is excluded from both this repo (.gitignore) and the published npm tarball (package.json's files), and ExpoMapboxNavigation.podspec's s.prepare_command fetches these binaries directly into the consuming app's node_modules/@jacques_gordon/expo-mapbox-navigation/ios/Frameworks/ the first time that app runs pod install — using that app's own Mapbox downloadsToken (see the downloadsToken plugin option, required for iOS). This matches the approach used by other public Mapbox Navigation + React Native packages (e.g. pawan-pk/react-native-mapbox-navigation).
Practical implications for consumers of this package:
- The first
pod installafter installing/upgrading this package will be noticeably slower (fetching ~250MB of binaries) — subsequent installs are fast as long asnode_modulesisn't wiped between them. - Requires network access and a valid Mapbox
DOWNLOADS:READtoken (via thedownloadsTokenplugin option) atpod installtime, not just at publish time. If that token is missing or invalid, the consuming app's own build fails with a clear error (not a cryptic downstream one), pointing back to thedownloadsTokenoption.
Usage
import { MapboxNavigationView } from '@jacques_gordon/expo-mapbox-navigation';
export default function Navigation() {
return (
<MapboxNavigationView
style={{ flex: 1 }}
coordinates={[
{ latitude: 50.8503, longitude: 4.3517 }, // Brussels
{ latitude: 51.2194, longitude: 4.4025 }, // Antwerp
]}
voiceUnits="metric"
language="fr"
navigationProfile="driving-traffic"
onRoutesReady={({ nativeEvent }) =>
console.log('Route ready:', nativeEvent.distanceMeters, 'm')
}
onRouteProgressChanged={({ nativeEvent }) =>
console.log('Remaining:', nativeEvent.distanceRemaining, 'm')
}
onManeuverBannerPressed={({ nativeEvent }) => {
// Open a bottom sheet showing all upcoming steps
console.log('Steps:', nativeEvent.steps);
}}
onArrival={() => console.log('Arrived!')}
onNavigationCancelled={() => console.log('Cancelled')}
onRoutesFailed={({ nativeEvent }) =>
//nativeEvent.code === 'same_location' = 'navigation.alreadyAtDestination' (<= 25m)
console.error('Failed:', nativeEvent.code, nativeEvent.message)
}
/>
);
}Props
Navigation
Route recalculation on prop change (5.0.0+, both platforms): changing any of
coordinates,waypointIndices,navigationProfile,language,voiceUnits,excludeTypes,maxHeightormaxWidthautomatically recalculates the route with the new value (multiple changes in the same render batch coalesce into a single request; re-setting an identical value does nothing). Before 5.0.0, onlycoordinatestriggered recalculation — changing e.g.languagemid-session had no effect until the next coordinates change.mapStyleand the color/UI props never trigger recalculation.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| coordinates | { latitude: number; longitude: number }[] | required | Waypoints. Minimum 2. |
| waypointIndices | number[] | all | Which coordinates are true waypoints (vs. route shape points). |
| navigationProfile | string | "driving-traffic" | "driving-traffic", "driving", "walking", "cycling". Android: omit the "mapbox/" prefix. |
| language | string | device locale | BCP-47 tag (e.g. "fr", "nl", "en-US"). |
| voiceUnits | "metric" \| "imperial" | auto by locale | Overrides automatic unit detection. |
| excludeTypes | string[] | — | Road types to avoid (e.g. ["toll", "ferry"]). |
| mapStyle | string | Mapbox Navigation Day | Map style URL. |
| mute | boolean | false | Silence voice instructions. |
| maxHeight | number | — | Max vehicle height in metres. |
| maxWidth | number | — | Max vehicle width in metres. |
| useMapMatching | boolean | false | Use Map Matching API instead of routing. |
| customRasterTileUrl | string | — | Custom raster tile URL with {x}/{y}/{z}. |
| customRasterAboveLayerId | string | — | Layer ID to insert custom raster tiles above. |
| showEndOfRouteFeedback | boolean | true | iOS only (5.0.1+). Whether the built-in end-of-route screen ("You have arrived" + trip rating stars) appears on final arrival. Set false to arrive silently — onArrival fires either way. No-op on Android (its custom UI has no such screen). |
Color Customization (Android)
All color props are optional — defaults are applied when omitted.
| Prop | Default | Description |
|------|---------|-------------|
| maneuverBackgroundColorDay | Mapbox default | Background of the turn-by-turn instruction banner (day/light mode). Sets ManeuverViewOptions.maneuverBackgroundColor at runtime (Android/iOS). Android: has no visible effect from this prop alone — must also be set as a plugin option in app.json for the real on-screen color to change. See the callout right below, and priority vs. androidColorOverrides. |
| maneuverBackgroundColorNight | Mapbox default | Same, for night mode — the component switches automatically based on time of day (6am–8pm = day). Android: same app.json plugin-option requirement as maneuverBackgroundColorDay above. |
| maneuverTurnIconColor | Mapbox default | Color of the turn-direction icon inside the instruction banner. Sets ManeuverViewOptions.turnIconManeuver at runtime. Android: has no visible effect from this prop alone — Mapbox's SDK has no runtime API for icon color, only a build-time XML style chain. Must also be set as a plugin option in app.json. See the callout right below. |
| navigationPuckColor | Mapbox default | Tints Mapbox's own default location puck icon (the arrow showing your position/heading on the map — distinct from maneuverTurnIconColor, which is inside the banner, not on the map). Ignored if navigationPuckImagePath or navigationPuck3DModelPath is set. |
| navigationPuckImagePath | — | Replaces the puck icon entirely with a local image (file:// URI or absolute path). Never tinted, even if navigationPuckColor is also set — avoids any risk of a tint operation failing on an arbitrary custom image. Falls back to the color/default icon if the file can't be loaded. |
| navigationPuck3DModelPath | — | Replaces the 2D puck with a 3D model (.glb/.gltf) — a local path, asset://name.glb (Android's bundled assets), or a full URL. Takes priority over both puck props above when set and valid. Falls back to the 2D puck if the model fails to load. |
| speedLimitPosition | "bottomLeft" | Position of the speed limit panel: "bottomLeft", "bottomRight", "topLeft", or "topRight". "topRight" uses a wider margin to clear the mute/overview/recenter buttons, which occupy that corner already — verify visually on your target devices before relying on it. |
| showEta | true | Whether the ETA/duration/distance bar is shown once navigation starts. Android only. |
| etaBarBackgroundColor | "#1E2433" | Background of the bottom ETA/duration/distance bar. |
| etaTextColor | "#FFFFFF" | Text color for ETA time and duration. |
| iconButtonColor | "#1A73E8" | Color of the mute/overview/recenter buttons (default state). |
| iconButtonMutedColor | "#EA4335" | Color of the mute button when voice is muted. |
<MapboxNavigationView
maneuverBackgroundColorDay="#1E2433"
maneuverBackgroundColorNight="#0B0E14"
maneuverTurnIconColor="#1A73E8"
navigationPuckColor="#1A73E8"
speedLimitPosition="bottomLeft"
etaBarBackgroundColor="#1E2433"
etaTextColor="#FFFFFF"
iconButtonColor="#1A73E8"
iconButtonMutedColor="#EA4335"
/>Android:
maneuverBackgroundColorDay,maneuverBackgroundColorNight, andmaneuverTurnIconColoralone are not enough.All three set a matching
ManeuverViewOptionsfield at runtime — confirmed, via diagnostic logging, to correctly receive and parse the right value every time — but on Android, none of them visibly changes anything on their own. The maneuver banner's actual on-screen appearance (background and icon color) is controlled by a build-time Android resource style (MapboxCustomManeuverStyle/MapboxCustomManeuverTurnIconStyle), generated by this package's config plugin from its ownapp.jsonoptions — not from view props on your React component. This isn't a bug to work around: config plugins run atexpo prebuildtime, before your JS ever executes, so they have no way to read a runtime prop value — and Mapbox's SDK resolves the banner's actual colors from that compiled resource, not from the runtime API. See Mapbox Native Colors below for the full mechanism.You need the same three values in two places: as
app.jsonplugin options (this is what actually changes the color on screen) and, if you like, also as view props on your component (harmless — kept for iOS parity and forward-compatibility, but currently has no visible effect of its own on Android). To avoid maintaining the same hex values twice, put them in one shared file and spread it into both:// maneuverColors.js — single source of truth module.exports = { maneuverBackgroundColorDay: "#1E2433", maneuverBackgroundColorNight: "#0B0E14", maneuverTurnIconColor: "#1A73E8", };// app.config.js — the JS config format, so it can require() the file above // (a static app.json cannot) const maneuverColors = require('./maneuverColors'); module.exports = { expo: { plugins: [ ["@jacques_gordon/expo-mapbox-navigation", { accessToken: "pk.xxx", downloadsToken: "sk.xxx", ...maneuverColors, }], ], }, };// Your component — spread the same object, so there's exactly one place // to ever change one of these three colors import maneuverColors from '../maneuverColors'; <MapboxNavigationView {...maneuverColors} /* ...other props */ />
iOS parity, as of this release:
maneuverBackgroundColorDay/maneuverBackgroundColorNight— implemented via a customStandardDayStyle/StandardNightStylesubclass pair, stylingInstructionsBannerViewthroughUIAppearance(Mapbox's own confirmed, documented v3 mechanism for this). Read once, at the time a route is presented — unlike Android,NavigationOptions.stylesis a construction-time configuration, so changing these colors while navigation is already active takes effect on the next route, not instantly. This is an architectural difference from Android's live-updating maneuver view, not an oversight.navigationPuckColor/navigationPuckImagePath/navigationPuck3DModelPath— implemented viaNavigationMapView.puckType(Puck2DConfiguration/Puck3DConfiguration), the confirmed official v3 API — same precedence as Android (3D model > custom image, never tinted > color tint > default). Unlike the maneuver colors above, this can be updated live while navigation is active.navigationPuckColortints a standard system symbol rather than Mapbox's own default icon — there's no confirmed public API for the exact built-in asset name to tint directly on iOS, unlike Android.maneuverTurnIconColorandspeedLimitPositionare not implemented on iOS and remain no-ops (stored, no effect).maneuverTurnIconColorwould require referencing a specific internal SDK view class for the turn-direction icon that we could not confirm still exists under an unchanged name in the current v3MapboxNavigationUIKit— guessing wrong would fail to compile the entire iOS build, not just silently not work, so this was left out deliberately rather than risked.speedLimitPositionhas no confirmed repositioning API —SpeedLimitView's position is fixed as part ofNavigationViewController's drop-in layout. Both would require building a custom top/bottom banner (see Mapbox's "Build a custom UI" guide) rather than using the drop-inNavigationViewController, a substantially larger undertaking than styling one already outside this package's current architecture.etaBarBackgroundColor,etaTextColor,iconButtonColor,iconButtonMutedColorremain stored-only for the same reason asmaneuverTurnIconColor— no confirmed v3 class name found for these specific sub-elements.
Mapbox Native Colors (Android, via plugin)
Override Mapbox's built-in resource colors (route line, etc.) via androidColorOverrides in app.json:
["@jacques_gordon/expo-mapbox-navigation", {
"accessToken": "pk.xxx",
"downloadsToken": "sk.xxx",
"mapboxMapsVersion": "11.11.0",
"androidColorOverrides": {
"mapbox_primary_route_color": "#0055FF",
"mapbox_main_maneuver_background_color": "#FF5500"
}
}]Priority between androidColorOverrides.mapbox_main_maneuver_background_color and the maneuverBackgroundColorDay prop. These are two independent mechanisms that both affect the maneuver banner's background — androidColorOverrides is a build-time Android resource override (baked into the APK/AAB at expo prebuild), while maneuverBackgroundColorDay is a runtime ManeuverViewOptions API call. Setting both to different values means only one visibly wins, which looks like the other one "isn't working." As of 3.1.2, this package resolves it with a clear, fixed priority:
androidColorOverrides.mapbox_main_maneuver_background_color, if you set it explicitly — always wins, exactly as you set it.- Otherwise,
maneuverBackgroundColorDay, if set — automatically written into the same underlying resource for you. You don't need to set both; setting onlymaneuverBackgroundColorDayis enough. - Otherwise, Mapbox's own default.
In short: if you're only using one of the two, just use maneuverBackgroundColorDay — it's kept in sync automatically. Reach for androidColorOverrides.mapbox_main_maneuver_background_color directly only if you specifically need to pin an exact value regardless of what any prop says (e.g. a design system constant that shouldn't be app-configurable).
maneuverTurnIconColor — plugin option only, no priority conflict. Unlike the background color above, there's no androidColorOverrides key for the turn icon's color, so there's nothing to prioritize against — just set maneuverTurnIconColor as a plugin option and it's used directly to generate MapboxCustomManeuverTurnIconStyle, referenced from MapboxCustomManeuverStyle via maneuverViewIconStyle/laneGuidanceManeuverIconStyle (Mapbox's own official mechanism for this — see their "Change the color of maneuver turn icons" guide). Same single build-time XML style file as the background color, generated in both res/values/ and res/values-night/ — Mapbox ships its own values-night/ default for this too, which would otherwise silently win in dark mode the same way it originally did for the background color.
Events
| Event | Payload | Description |
|-------|---------|-------------|
| onRoutesReady | { routeCount, distanceMeters, durationSeconds } | Fired when routes are calculated and navigation starts. |
| onRouteProgressChanged | { distanceRemaining, durationRemaining, distanceTraveled, fractionTraveled, currentStepDistanceRemaining } | Fired on every GPS update during navigation. |
| onArrival | {} | User reached the destination. |
| onNavigationCancelled | {} | User tapped the cancel (✕) button. |
| onNavigationFinished | {} | Navigation session ended normally. |
| onRoutesFailed | { message: string; code?: string } | Route calculation failed or was rejected. message is English-only debugging text — don't show it to users. When code is present, switch on it for your own localized UI: "same_location" = all waypoints within 25 m of each other (driver already at destination), no route requested. code is absent for ordinary SDK routing failures. |
| onManeuverBannerPressed | { steps: RouteStep[] } | Fired when user taps the instruction banner. Use to open a bottom sheet with the full steps list. |
RouteStep type
interface RouteStep {
instruction: string; // "Turn left onto Main St"
distanceMeters: number;
durationSeconds: number;
maneuverType: string; // "turn", "merge", "roundabout", etc.
maneuverModifier: string; // "left", "right", "straight", etc.
roadName: string;
laneInstructions: {
active: boolean; // true = recommended lane
directions: string[]; // ["straight"], ["left", "straight"]
}[];
}16 KB Page Size (Android 15+)
Google Play requires new apps/updates targeting Android 15+ (API 35+) to support 16 KB memory pages. This package addresses this via two independent mechanisms, covering two different Mapbox artifact groups — worth understanding both, since they're gated differently.
Already enabled by default, no configuration needed:
- NDK 27 (
27.0.12077973) — first NDK version with full 16 KB support jniLibs.useLegacyPackaging = false— prevents.socompression, enables proper alignment- 64-bit ABI filters (
arm64-v8a,x86_64) — the 16 KB requirement applies to 64-bit only
Mechanism 1 — com.mapbox.maps:*/com.mapbox.common:* (Maps/plugin/module/extension groups): always on, no flag.
The config plugin's addAndroidConfig unconditionally substitutes these to their -ndk27 variants via resolutionStrategy.dependencySubstitution (17 modules). MAPS_VER comes from mapboxMapsVersion (default "11.11.0"); COMMON_VER is auto-derived from it (Mapbox's synchronized versioning: 11.x.y ↔ 24.x.y, same minor/patch — verified against Mapbox's own compatibility table). This has been on since before androidUseNdk27 existed and isn't gated by it — it runs every time regardless.
Mechanism 2 — com.mapbox.navigationcore:*: opt-in — androidUseNdk27.
Mapbox publishes a separate -ndk27 artifact variant of each com.mapbox.navigationcore:* dependency (the one this package itself declares in android/build.gradle) — but only starting from Navigation SDK 3.11.0 (this package defaults to 3.8.1, which predates it; confirmed from Mapbox's own changelog). Since switching to a nonexistent -ndk27 artifact fails the build outright (Could not resolve), this one is opt-in rather than default:
["@jacques_gordon/expo-mapbox-navigation", {
"accessToken": "pk.xxx",
"downloadsToken": "sk.xxx",
"mapboxMapsVersion": "11.14.0",
"mapboxNavigationVersion": "3.11.0",
"androidUseNdk27": true
}]Requirements when enabling this:
- Pass both
mapboxMapsVersionandmapboxNavigationVersionexplicitly — don't rely on this package's Phase 1/Phase 2 auto-calculation for a version pair you haven't verified. Mapbox's own docs note that pairing Navigation/Maps versions incorrectly is easy to get wrong for anything below Navigation 3.16.0. - Use a
mapboxNavigationVersionof3.11.0or later (and a correspondingly compatiblemapboxMapsVersion— check Mapbox's Navigation SDK release notes for the exact tested pairing at your target version).
If you enable androidUseNdk27 with an unsupported version pair, Gradle will fail clearly with Could not resolve com.mapbox.navigationcore:...-ndk27:... — a hard failure, not a silent fallback, so you'll know immediately if the pairing is wrong.
In short: leaving everything at defaults gets you Mechanism 1 automatically (Maps/Common covered). Google Play flagging only libnavigator-android.so specifically means Mechanism 2 is what you still need — set androidUseNdk27: true with a verified >= 3.11.0 pairing.
See Android's 16 KB page size guide.
Changelog
5.0.3
Android: fixes the destination flag (and waypoint badges) rendering visibly offset from the point where the route line actually ends — confirmed by a real device screenshot. Two independent causes, both fixed.
- Cause 1 — wrong coordinate source (the big one): markers were placed at the RAW request coordinates (the geocoded address the app passes in), but Mapbox snaps routes to the road network — the drawn route line ends at the SNAPPED point, which can sit tens of meters away from the raw geocode. Markers now use the Directions RESPONSE waypoints (per Mapbox's own docs: "input coordinates snapped to the road and path network, in the order of the input coordinates") — i.e. exactly where the route line starts/ends. Bonus correctness: response waypoints only contain leg-separating stops (the API itself excludes silent via-points), so intermediate badge numbering is right by construction. If a response carries no waypoints, markers fall back to the raw coordinates (the pre-5.0.3 placement) rather than disappearing. iOS needs no change: its markers go through the SDK's own waypoint pipeline, which is already route-derived.
- Cause 2 — bitmap anchor geometry: the flag bitmap drew its pole at the LEFT edge with a 2dp empty margin below, while
IconAnchor.BOTTOManchors the bitmap's bottom-CENTER on the map point — so the pole base landed ~11dp right of and 2dp above the true point even when the coordinate was correct. The pole is now horizontally centered with its base flush against the bitmap's bottom edge (the exact anchor pixel), with the banner waving right of the pole top.
5.0.2
Two changes: (1) BOTH platforms — when the degenerate-route guard fires (driver already at the destination), the view now switches to a live free-drive map centered on the driver, instead of a whole-world view on Android and a permanently black surface on iOS; (2) build-tooling fix — the published build/index.js in 5.0.0/5.0.1 contained raw, uncompiled JSX, restored to proper compiled createElement output, byte-identical to the production-proven 4.0.2 artifact.
The problem, observed on real devices: 5.0.1's guard correctly rejects same-location trips (
onRoutesFailed,code: "same_location") without starting navigation — but then nothing owns the screen. Android's always-present map stayed at world view (no trip session → no enhanced locations → the first-location camera centering never triggers); iOS was worse: its architecture creates NO view at all before a successful route, so the surface stayed permanently black — and honestly, the same was true for ANY route failure on iOS, this case just made it easy to reproduce.The fix — Mapbox free-drive mode (passive session: live map + directional puck + following camera, no turn-by-turn) on both platforms:
- Android: the guard branch immediately centers the camera on the trip's first coordinate (instant feedback, no GPS dependency), then starts a free-drive session (
setNavigationRoutes(emptyList)+startTripSession()— the SDK's documented passive mode). Enhanced locations then animate the puck and the existing first-location logic switches the camera to live following. If a real trip is requested later, the normal route flow transitions free-drive → active guidance seamlessly (the standard SDK flow this package already uses). - iOS: a standalone
NavigationMapViewfed by the shared navigation's Combine publishers, plustripSession().startFreeDrive()and a.followingcamera — the construction pattern is verified verbatim against Mapbox's own v3.20.1 example (Examples/AdditionalExamples/Examples/Custom-Navigation-Camera.swift), and all three APIs are confirmed public at the exact vendored tag (SessionController.startFreeDrive(),NavigationMapView.init(location:routeProgress:...),NavigationController'slocationMatching/routeProgresspublishers). The fallback map is torn down automatically when a real route later presents the full drop-in UI (the SDK's own examples perform this exact free-drive → active-guidance transition), and its passive session is set to idle if the view is removed while the fallback is up — scoped so active-guidance teardown behavior is untouched. Default directional-arrow puck; custom puck props intentionally don't apply to the fallback.
- Android: the guard branch immediately centers the camera on the trip's first coordinate (instant feedback, no GPS dependency), then starts a free-drive session (
onRoutesFailedstill fires exactly as in 5.0.1 (code: "same_location") — the free-drive map is what the USER sees; the event is what the APP reacts to (e.g. its own localized "you have arrived" banner over the map). Reminder: always handleonRoutesFailed— this whole case was initially mistaken for a regression on a real device precisely because no handler was wired.Third change — professional destination and waypoint markers (BOTH platforms): the final destination now shows a checkered "finish" flag on a pole, and intermediate stops show numbered circular badges (1, 2, 3…) — the Waze-style clarity, drawn entirely in code (no bundled image assets, matching how every icon in this package's Android UI is already drawn; same dark-slate/white palette on both platforms).
- iOS: rendered through the SDK's OWN waypoint pipeline via the three
NavigationViewControllerDelegatecustomization hooks (shapeFor waypoints:legIndex:,waypointCircleLayerWithIdentifier:,waypointSymbolLayerWithIdentifier:) — every signature and the whole approach verified verbatim against Mapbox's own v3.20.1Custom-Final-Waypointexample. Using the SDK pipeline means markers automatically follow rerouting, and already-passed stops fade out via the SDK-maintainedlegIndex/completedstate. The flag image is registered with the map style and automatically RE-registered after every style load (style changes wipe registered images — including this package's own optionalmapStyleload), with the image-registration observer cancelled on teardown. Style operations usetry?(the SDK example's owntry!deliberately not copied — this package never crashes on style ops). - Android: rendered with the Maps SDK's annotation plugin (bundled with the existing
com.mapbox.maps:androiddependency — no new artifact): flag bitmap at the final coordinate (bottom-anchored so the pole base sits on the point), numbered badge bitmaps at intermediate leg-separating stops (respectingwaypointIndicessemantics exactly asfetchRoutessends them — silent via-points get no badge). Markers are (re)built when routes become active and fully cleared on navigation teardown; the whole marker path is wrapped so a rendering failure can never break navigation itself (cosmetic-only guarantee).
- iOS: rendered through the SDK's OWN waypoint pipeline via the three
What happened: the
tsconfig.jsonrestored from git history in4.0.3(after being missing since2.1.3, which had silently brokennpm run build) carried"jsx": "react-native"— a mode that PRESERVES JSX syntax in the emitted.js, expecting a later Babel pass. Every rebuild from4.0.3onward therefore shippedbuild/index.jswith a raw<NativeView .../>in it. The original pre-2.1.3artifact had been generated with the classic transform (plainReact.createElementcalls) — meaning the restored tsconfig never matched whatever originally produced the known-good output.Why nothing visibly broke: Metro/babel-preset-expo transpiles JSX in
node_modules.jsfiles, so Expo consumers (including the real iOS device tests that succeeded on 5.0.0/5.0.1) parsed it fine. But shipping preserved JSX in a package's compiled entry point is wrong: any consumer whose bundler does not run a full JSX transform overnode_moduleswould fail to parse the package outright.Fix:
tsconfig.jsonnow uses"jsx": "react"(classiccreateElementtransform;src/index.tsxalready imports React). Verified in the strongest way available: the regeneratedbuild/index.jsis byte-for-byte identical to thebuild/index.jsshipped in4.0.2(the artifact that had been proven in production since2.1.3-era builds) — confirmed by direct file comparison against the immutable npm tarball.No source, native, or API changes of any kind in this release.
5.0.1
iOS: fixes an intermittent hard crash (EXC_BREAKPOINT/SIGTRAP) when the navigation screen is closed and reopened — the Navigation SDK enforces a single MapboxNavigationProvider per process, and this package was creating one per view instance. Found from a real production crash log after 5.0.0's on-device success (the map now renders and navigates — first confirmed working iOS navigation of this package).
- The crash, from a real
.ipscrash report: main-thread stack reads, verbatim:closure #1 in static MapboxNavigationProvider.checkInstanceIsUnique()←Locked.withLock←MapboxNavigationProvider.init(coreConfig:)← this package's init block.checkInstanceIsUnique()is the SDK's own internal guard: it deliberately traps the process when a second provider instance is created while another is still alive. Since this package created a provider per view instance, closing and quickly reopening the navigation screen (remount before the previous view deallocated) crashed the app — intermittently, because it depended on deallocation timing. This is also, in hindsight, exactly why reference implementations keep their provider in astatic let. - Fix: one
static let sharedNavigationProvider, lazily created on first use, shared by every view instance. Teardown (removeFromSuperview) intentionally does NOT touch it — it lives for the process lifetime, which is precisely what the SDK's uniqueness rule wants. - Structural bonus — the 4.0.8 black-screen race fix becomes obsolete by construction: the shared provider is available synchronously, so
mapboxNavigationis assigned ininit()before any prop setter can possibly run. TheDispatchQueue.main.asyncdeferral and its provider-ready catch-up call are removed entirely; 5.0.0's coalesced prop-setter scheduler is now the single trigger path intofetchRoutes(), in every ordering. - Second crash report, honestly triaged as NOT yet attributable: a same-day
SIGSEGV(CFStringFindon a NULL string duringCALayerdrawing,drawLayer:inContext:→_firstCommitBlock) has all its meaningful frames in unsymbolicated app-binary offsets — it cannot be attributed to this package or ruled out without the matching build's binary/dSYM. It may or may not be a downstream effect of the double-provider state this release fixes. To be reassessed after this fix ships: if it recurs, symbolication against that exact build will be needed. - Second fix in this release — navigation arrow instead of a plain blue dot (iOS): during active navigation the location puck appeared as the basic blue location dot with no heading indication (confirmed by a real device screenshot). Root cause, verified verbatim at v3.20.1:
NavigationMapView.puckType's own SDK default is.puck3D(.navigationDefault)— the standard Mapbox directional-arrow puck (PuckConfigurations.swiftdefines thenavigationDefaultstatics) — but this package'sapplyPuckSettings()fallback branch actively REPLACED that default with.puck2D(Puck2DConfiguration()), the plain dot, whenever no custom puck prop was set. The fallback now explicitly sets.puck3D(.navigationDefault)— restoring the SDK's arrow, and also correctly resetting to it when a custom puck prop is later cleared. CustomnavigationPuckColor/navigationPuckImagePath/navigationPuck3DModelPathbehavior is unchanged. - Third change in this release — new prop
showEndOfRouteFeedback(iOS-effective): NavigationViewController's built-in end-of-route screen ("You have arrived" + trip-rating stars) can now be disabled by passingshowEndOfRouteFeedback={false}. Default istrue— the SDK's own behavior, so nothing changes for existing consumers unless they opt out. Verified verbatim at v3.20.1:NavigationViewController.showsEndOfRouteFeedbackis a public settable property (forwards toarrivalController.showsEndOfRoute); note from the same source that assigningshowsReportFeedbackwould overwrite it — this package never assigns that. Applied at controller construction AND live on an already-presented controller (the setter is real, so toggling mid-navigation works).onArrivalfires regardless of this setting. Declared as a stored no-op on Android for cross-platform prop parity (its custom-built UI has no equivalent screen). - Fourth fix in this release — camera now always starts centered on the driver (iOS): a real device test surfaced a case where the map presented over a default world region (mid-ocean) instead of the driver's position: a degenerate route whose destination equals the driver's current location (0 m, instant arrival) — active guidance never entered its follow-the-user camera state.
presentNavigationViewControllernow explicitly callsnavigationCamera.update(cameraState: .following)right after presentation — verified verbatim at v3.20.1 (NavigationCamera.update(cameraState:)is public;.followingis "the camera is following user position"), and it is the exact call the reference implementation uses for its own recenter button, so it is harmless in the normal flow where the SDK enters following on its own. - Fifth change in this release — degenerate-route guard (BOTH platforms): requesting a trip whose destination is where the driver already is produced a 0 m route, a 1-second navigation session, an instant arrival screen, and (on iOS) the mispositioned camera fixed above.
fetchRoutes()on both platforms now detects this case up front and emits a clear, catchableonRoutesFailed(message:"Origin and destination are the same location (all waypoints within 25 m) - nothing to navigate", byte-identical on both platforms) instead of starting navigation — the consuming app can show its own "you are already at your destination" UI from that event. Deliberately checks ALL coordinates against the first, not just origin vs destination: a legitimate round trip (start → distant via point → back to start) also has origin == destination and is NOT blocked. Distance math uses each platform's own standard API (CLLocation.distance(from:)/android.location.Location.distanceBetween) — no new dependency. Localization: the event carries a stable machine-readablecode: "same_location"alongside the English-onlymessage— the consuming app should switch oncodeto display its own localized text (this package's own strings are never translated; thelanguageprop only localizes what the Mapbox API generates). - Sixth change in this release — native upcoming-steps panel on Android: on iOS, tapping the maneuver banner opens the SDK's own built-in list of all upcoming steps; Android's custom UI had no native equivalent (the tap only emitted
onManeuverBannerPressed). Android now shows a native bottom panel (scrim + scrollable list of each step's instruction and distance), toggled by the same banner tap. Coexistence preserved on both platforms: theonManeuverBannerPressedevent still fires on every tap exactly as before — apps rendering their own JS sheet from the payload keep working unchanged. Panel details: hard-capped at 86% of the view height (customonMeasureclamp — wraps short lists, scrolls long ones); dismissed by tapping the scrim, tapping the banner again, or any navigation teardown; all visible text comes from the Directions API response (already localized vialanguage) plus numeric-only distance labels (unit system followsvoiceUnits) — this package still hardcodes no user-facing prose. Built from framework widgets only, no new dependency. iOS's built-in list deliberately NOT capped at 86%: its pin-to-screen-bottom constraint is internal to the SDK and required-priority (verified verbatim at v3.20.1,TopBannerViewController.stepsContainerShowConstraints) — adding a competing external constraint would create an unsatisfiable-constraints conflict that UIKit resolves by breaking an arbitrary constraint. Not safely possible without replacing the SDK's steps UI wholesale; documented rather than risked. - Seventh change —
navigationPuck3DModelPathcrash hardening (Android): a crash was reported with this prop on Android even with a verified-valid.glbfile. Prior hardening (file existence + glb magic-number validation forfile:///absolute paths, try/catch around every SDK call, 2D fallback) already covers the Kotlin-reachable failure modes — butasset://paths were passed through with no validation at all; that gap is now closed (asset existence + the same glb magic-number check, via the standardAssetManagerAPI). Honest status: if the crash persists with a valid model after this, the failure is inside Mapbox's native model loader/renderer (not catchable from Kotlin) and needs the device's crash log (logcat/tombstone) to pinpoint — please capture one. Note that as of this release both platforms show a proper directional-arrow puck by default, with no custom model needed (iOS: SDK's.puck3D(.navigationDefault); Android: SDK's default bearing icon), so the custom 3D model is now purely optional for the arrow use case. - Android otherwise unchanged: the Android SDK's
MapboxNavigationProvider.create()already manages a process-wide instance internally; no equivalent crash exists there, and Android's default puck already shows its own directional icon. - Verification status: crash mechanism and stack are from a real production crash log (strongest possible evidence); the fix pattern is the SDK's own documented usage. As always, no local Swift toolchain — syntax balance checked, and the change uses only constructs already compiled in prior builds (
static let, same initializer call). Real device retest (open/close/reopen the navigation screen repeatedly) is the confirmation step.
5.0.0
Both platforms: route-affecting props now re-trigger route calculation when their value changes. Previously, only coordinates did — changing navigationProfile, language, voiceUnits, excludeTypes, waypointIndices, maxHeight or maxWidth after mount was silently ignored until the next coordinates change. Major version bump because this is a real behavior change consumers can observe (a route may now recalculate in situations where it previously wouldn't).
- Concrete bug this fixes: changing the app language mid-session left voice guidance and banner instructions in the OLD language — the Directions API returns instructions already translated inside the route response itself, so without a new route request, the new
languagevalue was stored but never used. Same mechanism affected all the other route-affecting props (e.g. switchingnavigationProfilefrom driving to walking did nothing). - Props that now re-trigger
fetchRoutes()on change (each one verified to be actually read by both platforms' route-request builders):coordinates(as before),waypointIndices,navigationProfile,language,voiceUnits,excludeTypes,maxHeight,maxWidth. - Props deliberately EXCLUDED from re-triggering (verified to have no link to the route request):
mapStyle(map appearance only — applied at presentation time, unchanged behavior),mute(live-applied already, no route impact),useMapMatching/customRasterTileUrl/customRasterAboveLayerId(stored-but-unused on BOTH platforms since before 4.0.2 — wiring dead props to refetch would be pure noise), and all color/puck/UI props. - Two safeguards against regressions, implemented identically on both platforms:
- Equality guard in every affected setter — re-delivering an identical value never triggers a refetch.
- Coalescing — all triggers within one main-loop turn merge into a single deferred
fetchRoutes()call. This matters most at initial mount, where React Native delivers every prop in one batch: without coalescing, mounting the view would fire up to 8 simultaneous route requests. With it, exactly one. On iOS this also means the initial fetch runs afterinit()'s provider-creation block (queued earlier on the same main queue), closing the 4.0.8 startup-ordering gap from a second, independent angle — and 4.0.8's own provider-ready catch-up call now goes through the same scheduler rather than callingfetchRoutes()directly (caught in this release's own pre-publish audit: a direct call would have fired a first request at mount that the props' scheduled fetch immediately cancelled and re-issued — two network requests per mount, one wasted). On Android the scheduler uses a main-LooperHandlerrather thanView.post()— deliberate:View.post()defers queued work until the view is attached, and props are delivered before attachment.
- Cross-platform parity preserved by construction: the exact same prop list, guards, and coalescing semantics ship on iOS and Android in this same release — at no point does one platform refetch where the other doesn't. (This was the explicit reason this change was previously rejected as an iOS-only fix — see the analysis that led to it.)
- What did NOT change:
fetchRoutes()'s own internal guards (needs ≥2 coordinates and an initialized navigation instance) still make any scheduled fetch a harmless no-op when prerequisites aren't met; Android'smapboxNavigationis still created synchronously ininit{}; iOS keeps the 4.0.8 provider-ready catch-up call. - Verification status: Kotlin/Swift both syntax-checked (brace/paren balance), all call sites of
fetchRoutes()audited on both platforms (it is now reachable only via the scheduler, plus iOS's init catch-up), setter wiring re-verified against both Module definition files, and equality semantics confirmed against both languages' structural-equality rules for the exact declared property types. As always: no local native toolchain available — a real build on each platform is the final confirmation step.
4.0.8
iOS: fixes the black-screen-after-successful-build failure — a startup race condition that silently prevented the route request (and therefore the entire navigation UI) from ever starting. Found via a full line-by-line audit of ios/ExpoMapboxNavigationView.swift prompted by a real on-device test: the 4.0.7 build compiles, launches, and shows a permanently black view where the map should be, with a Console.app capture showing zero Mapbox activity of any kind.
- Root cause (race condition, silent):
init()deferred the creation ofMapboxNavigationProviderto aDispatchQueue.main.asyncblock, but React Native/Fabric delivers initial props — includingcoordinates— synchronously, in the same main-runloop turn that creates the view, i.e. before that async block runs. The initialsetCoordinates()call therefore reachedfetchRoutes()whilemapboxNavigationwas stillnil, andfetchRoutes()'s guard bailed out silently. Nothing ever re-triggered the fetch, so no route request was ever made,NavigationViewControllerwas never constructed, and the view stayed permanently empty. This exactly matches the observed evidence: no crash, no error, no Mapbox log output at all — the SDK was never asked to do anything. Fix: the async init block now callsfetchRoutes()after the provider is ready — a no-op unless valid coordinates already arrived, covering both orderings (props-before-provider and provider-before-props) deterministically. No new SDK API involved; pure sequencing fix. - Second fix (stale-task guard): Swift
Taskcancellation is cooperative —cancel()only sets a flag; the task body keeps running unless it checks.fetchRoutes()cancels any in-flight request before starting a new one (andremoveFromSuperviewcancels too), but the task body never checked the flag — so a superseded/cancelled request could still fire staleonRoutesReady/onRoutesFailedevents and present aNavigationViewControllerfor an outdated route (or on a torn-down view). Now checksTask.isCancelledafter the route result arrives and bails out if cancelled. Confirmed standard Swift concurrency semantics, not SDK-specific. - Third fix (Apple containment contract): child view-controller embedding used a non-canonical call order (
addSubviewbeforeaddChild,didMovelast, containment skipped entirely when no parent VC found at the moment of embedding). Restructured to Apple's documented canonical order from "Creating a custom container view controller":addChild→addSubview→ constraints →didMove(toParent:). Same conditional-parent fallback behavior as before; only the ordering contract changed. - Audit scope, for the record: every remaining SDK call in the file was already source-verified against
mapbox-navigation-iosv3.20.1 during the 4.0.7 pass (RoutingProvider.calculateRoutessignature/return type,NavigationRoutes.mainRoute/alternativeRoutes,NavigationRoute.route,Route.distance/expectedTravelTime,RouteOptions.maximumHeight/maximumWidthtypes,RoadClassesinit semantics, all threeNavigationViewControllerDelegatesignatures). This pass additionally reviewed every function for runtime-safety issues (force unwraps, optional handling, event-thread correctness) — the three items above were the only confirmed defects found. Known non-issues left deliberately untouched:Locale.regionCodeis deprecated (iOS 16+) but functional — cosmetic warning only; oneMapboxNavigationProvideris created per view instance (fine for this package's one-navigation-view-at-a-time usage; would need a shared provider only if multiple simultaneous navigation views ever became a requirement). - Verification status, honestly stated: the race-condition mechanism is established from the code path plus the real device evidence (black screen + zero Mapbox log output), and the fix logic is deterministic — but as with every iOS change in this package, no local Swift compilation is possible (no Mac); brace/paren balance and non-ASCII scans were run, and the changed lines use no API not already compiled successfully in the 4.0.7 build. A real build + on-device test is still the final confirmation step.
- Companion audit of every other iOS-side file, same pass — no confirmed defects found, nothing changed:
ios/ExpoMapboxNavigationModule.swift(all 7Events(...)names match the view'sEventDispatcherproperties exactly; everyPropclosure type matches the TS prop types;Name("ExpoMapboxNavigation")matches bothrequireNativeViewManagerandexpo-module.config.json),ExpoMapboxNavigation.podspec(version/deployment-target/source-file declarations all consistent),ios/patch-mapbox-library-evolution.sh(defensive, isolation-tested per its own 3.4.6 entry; note that after4.0.3's retraction its actual usefulness against the original crash is unconfirmed — kept because it is harmless and was part of the only combination verified crash-free in production), andios/fetch-xcframeworks.sh(production-proven — two theoretical-only smells noted for the record, deliberately NOT changed since the script is verified working in real EAS builds: it assigns toTMPDIR, a variable macOS exports globally, which child processes then inherit — harmless here because the directory exists for the script's whole lifetime; andfound=$(find ... | head -1)underset -o pipefailcould in principle propagate a SIGPIPE exit status — never observed in practice with these small artifact listings).
4.0.7
Two iOS fixes: the one remaining Swift compile error from the 2026-07-15 EAS build (RoadClasses? optional unwrap, line 243), plus a silent-failure bug found by a full-file audit done to make sure no further compile errors are hiding behind it: the didArriveAt delegate method had the v2-era -> Bool signature, which compiles but never fires under v3 — onArrival would never have been emitted.
- Context: the previous build (first with 4.0.6's fixes) failed with exactly ONE remaining error -
value of optional type 'RoadClasses?' must be unwrappedatExpoMapboxNavigationView.swift:243. Notably, this line sits in the samefetchRoutes()function as 4.0.6's six errors, and the compiler only reported it AFTER those were fixed - meaning everything after line 243 in that function had still never been type-checked. Rather than fix one line and risk a third failed build revealing the next hidden error, this release was preceded by a full audit of both Swift files against the exact vendored SDK sources (mapbox-navigation-iostagv3.20.1, which vendors MapboxDirections insideSources/MapboxDirections/- notmain, not assumed). - Fix 1 -
RoadClasses(the compile error):RoadClasses(descriptions:)is a failable initializer -public init?(descriptions: [String]), quoted verbatim from v3.20.1'sRoadClasses.swift, whosedefault:case isreturn nil(one unrecognized string nils the whole thing; recognized values:toll,restricted,motorway,ferry,tunnel,hov2,hov3,hot,unpaved,cash_only_tolls). The 4.0.2-era code assigned it directly to the non-optionalroadClassesToAvoid(open var roadClassesToAvoid: RoadClasses = [], verified at the same tag). Fixed with optional binding: valid list applied, any invalid entry means no exclusions (graceful no-op, never a crash) - consistent with this package's existing bad-prop-input convention. The 4.0.2 changelog claimed this initializer was "confirmed" from source - the initializer's existence was confirmed, its failability was missed. That gap is exactly what this release's audit process was designed to catch. - Fix 2 -
didArriveAt(the silent-failure bug, no compiler diagnostic): the audit's delegate-signature diff against v3.20.1'sNavigationViewControllerDelegate.swiftfound ournavigationViewController(_:didArriveAt:)declared-> Bool(the SDK v2 shape). The v3 requirement returnsVoid(both the protocol requirement and its default implementation, quoted verbatim). A mismatched signature still compiles - it's just an extra unrelated method - but the SDK dispatches to its own default no-op, soonArrivalwould never have fired even once the app built and ran. Removed-> Bool/return true(multi-leg continuation is SDK-handled in v3; the v2 return value has no v3 equivalent). - Audit coverage, stated precisely: everything before line 243 and every other function was already validated by the real compiler in the last build (its single-error output proves the checker got that far). The never-type-checked zone (lines 244-285) was verified API-by-API against v3.20.1 sources:
maximumHeight/maximumWidth(Measurement<UnitLength>?),routingProvider()(@MainActorprotocol, returnsRoutingProvider),calculateRoutes(options:)(-> Task<NavigationRoutes, Error>, acceptsNavigationRouteOptionsvia itsRouteOptionssuperclass - all verified at-tag),NavigationRoutes.mainRoute/.alternativeRoutes/NavigationRoute.route,Route.distance/expectedTravelTime. The remaining two delegate signatures (didUpdate progressandnavigationViewControllerDidDismiss) match v3.20.1 exactly. No other mismatch found. - Still not verified: an actual successful build + on-device render. This is compile-correctness and callback-wiring work verified against source; the map rendering end-to-end remains to be confirmed by the next EAS build.
4.0.6
Fixes 3 real Swift compile errors in ios/ExpoMapboxNavigationView.swift - the first actual Xcode compilation this file has ever gone through (no Mac/Xcode toolchain was available for any of 4.0.0-4.0.5; 4.0.5 fixed autolinking discovery, which let a real archive build reach this file's compiler pass for the first time). Confirmed from a real EAS Build fastlane log, not guessed.
- Context: with
4.0.5installed, the consuming app's iOS archive build got past autolinking (theExpoMapboxNavigationpod was found and CocoaPods started compiling it - direct confirmation the4.0.5fix works) and failed for a new, different reason:** ARCHIVE FAILED **/CompileSwift normal arm64 ... ExpoMapboxNavigationView.swift, with 7 real compiler errors. wp.separatesLegs = ...- "cannot assign to property: 'wp' is a 'let' constant" (line 218, introduced in4.0.2'swaypointIndicesfix): verified againstmapbox-navigation-ios's own current documentation and examples that in Navigation SDK v3 (viaMapboxNavigationCore),Waypointis used as a struct (e.g.var userWaypoint = Waypoint(location:); userWaypoint.heading = ...in Mapbox's own v3 examples) - not theclassfrom the standalonemapbox-directions-swiftpackage that4.0.2's own changelog entry had assumed and never got to compile-check. Fixed by declaringvar wpinstead oflet wpin the waypoint-mapping closure - a one-word fix once the real type's value semantics were confirmed.options.distanceUnit = ... ? .mile : .kilometer- "cannot infer contextual base in reference to member 'mile'" (line 225, also from4.0.2):distanceUnitdoes not exist as a property onRouteOptions/NavigationRouteOptionsat all - confirmed directly frommapbox-directions-swift's ownDirectionsOptions.swiftsource. The real property isdistanceMeasurementSystem: MeasurementSystem, with cases.imperial/.metric(not.mile/.kilometer, which don't exist anywhere on this type). Fixed tooptions.distanceMeasurementSystem = resolveVoiceUnits() == "imperial" ? .imperial : .metric.options.profileIdentifier = .automobileAvoidingTraffic(and.automobile/.walking/.cycling) - "cannot infer contextual base in reference to member '...'" (lines 228-232, also from4.0.2): these 4 errors were not independent bugs - confirmed directly frommapbox-directions-swift's ownProfileIdentifier.swiftsource thatautomobile,automobileAvoidingTraffic,cycling, andwalkingare all real, correctly-named static members (backed by raw values"mapbox/driving","mapbox/driving-traffic","mapbox/cycling","mapbox/walking"). These 4 lines were left completely unchanged - they were cascading/spurious diagnostics produced by the real, precedingdistanceUniterror confusing the type-checker for the rest of that scope, a well-known Swift compiler behavior. Fixing the one real error above was expected to (and, per this same build's compiler behavior inwholemodulemode reporting every real error in one pass, is believed to) resolve all 4.- Audited, not touched: the rest of this file (
mapStyle/StyleURI,RoadClasses,maximumHeight/maximumWidthasMeasurement<UnitLength>,Puck2DConfiguration/Puck3DConfigurationpuck setup) was cross-checked against the same real Mapbox sources and left alone - no errors were reported against any of it in the same compiler pass (SWIFT_COMPILATION_MODE = wholemodulemeans the whole file is type-checked in one pass, so an error there would have surfaced in the same log if one existed). - **Verification method, honestl
