@jacques_gordon/expo-mapbox-navigation 1.0.0

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.
Files changed (29) hide show
  1. package/README.md +270 -0
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  8. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  9. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  10. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  11. package/android/.gradle/9.2.0/gc.properties +0 -0
  12. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  13. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  14. package/android/.gradle/vcs-1/gc.properties +0 -0
  15. package/android/build.gradle +54 -0
  16. package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationModule.kt +85 -0
  17. package/android/src/main/java/expo/modules/mapboxnavigation/ExpoMapboxNavigationView.kt +184 -0
  18. package/app.plugin.js +1 -0
  19. package/build/MapboxNavigation.types.d.ts +132 -0
  20. package/build/MapboxNavigation.types.js +2 -0
  21. package/build/MapboxNavigationView.d.ts +30 -0
  22. package/build/MapboxNavigationView.js +41 -0
  23. package/build/index.d.ts +2 -0
  24. package/build/index.js +8 -0
  25. package/expo-module.config.json +9 -0
  26. package/ios/ExpoMapboxNavigation.podspec +25 -0
  27. package/ios/ExpoMapboxNavigationModule.swift +81 -0
  28. package/ios/ExpoMapboxNavigationView.swift +236 -0
  29. package/package.json +63 -0
package/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # @jacques_gordon/expo-mapbox-navigation
2
+
3
+ Plugin Expo pour la navigation Mapbox **turn-by-turn**, compatible **Expo SDK 53** avec **Expo Dev Client**.
4
+
5
+ Supporte **iOS** et **Android**, avec waypoints multiples, instructions vocales, reroutage automatique, restrictions de gabarit de véhicule et personnalisation de la carte.
6
+
7
+ > ⚠️ Nécessite **Expo Dev Client** — incompatible avec Expo Go.
8
+
9
+ ---
10
+
11
+ ## Fonctionnalités
12
+
13
+ - ✅ Navigation turn-by-turn complète (iOS & Android)
14
+ - ✅ Waypoints multiples
15
+ - ✅ Instructions vocales
16
+ - ✅ Reroutage automatique
17
+ - ✅ Détection hors-route
18
+ - ✅ Restrictions de gabarit de véhicule (hauteur / largeur)
19
+ - ✅ Map Matching API (trajet explicite selon les coordonnées)
20
+ - ✅ Routes alternatives
21
+ - ✅ Style de carte personnalisable
22
+ - ✅ Couche raster personnalisée
23
+ - ✅ Multilangue
24
+ - ✅ Compatible avec `@rnmapbox/maps`
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ### 1. Installer `@rnmapbox/maps` (dépendance requise)
31
+
32
+ Suivre les [instructions officielles de @rnmapbox/maps](https://rnmapbox.github.io/docs/install).
33
+ La version recommandée est `11.11.0`. **Important : définir explicitement `RNMapboxMapsVersion`.**
34
+
35
+ ### 2. Installer le package
36
+
37
+ ```bash
38
+ npm install @jacques_gordon/expo-mapbox-navigation
39
+ ```
40
+
41
+ ### 3. Configurer le plugin dans `app.json`
42
+
43
+ ```json
44
+ {
45
+ "expo": {
46
+ "plugins": [
47
+ [
48
+ "expo-build-properties",
49
+ {
50
+ "ios": {
51
+ "useFrameworks": "static"
52
+ }
53
+ }
54
+ ],
55
+ [
56
+ "@jacques_gordon/expo-mapbox-navigation",
57
+ {
58
+ "accessToken": "pk.eyJ1IjoiVk9UUkVfVE9LRU4iLCJhIjoiY...",
59
+ "mapboxMapsVersion": "11.11.0",
60
+ "androidColorOverrides": {
61
+ "mapbox_main_maneuver_background_color": "#1E88E5"
62
+ }
63
+ }
64
+ ]
65
+ ]
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### 4. Configurer les tokens secrets
71
+
72
+ #### iOS — `~/.netrc`
73
+
74
+ ```
75
+ machine api.mapbox.com
76
+ login mapbox
77
+ password sk.eyJ1IjoiVk9UUkVfVE9LRU5fU0VDUkVUIiwiYSI6Ii4uLiJ9...
78
+ ```
79
+
80
+ #### Android — `~/.gradle/gradle.properties`
81
+
82
+ ```properties
83
+ MAPBOX_DOWNLOADS_TOKEN=sk.eyJ1IjoiVk9UUkVfVE9LRU5fU0VDUkVUIiwiYSI6Ii4uLiJ9...
84
+ ```
85
+
86
+ ### 5. Prebuild + Dev Client
87
+
88
+ ```bash
89
+ npx expo prebuild --clean
90
+ npx expo run:ios # ou run:android
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Utilisation
96
+
97
+ ### Exemple de base
98
+
99
+ ```tsx
100
+ import { MapboxNavigationView } from '@jacques_gordon/expo-mapbox-navigation';
101
+
102
+ export default function NavigationScreen() {
103
+ return (
104
+ <MapboxNavigationView
105
+ style={{ flex: 1 }}
106
+ coordinates={[
107
+ { latitude: 48.8566, longitude: 2.3522 }, // Paris — départ
108
+ { latitude: 43.2965, longitude: 5.3698 }, // Marseille — destination
109
+ ]}
110
+ locale="fr"
111
+ onFinalDestinationArrival={() => console.log('Arrivé !')}
112
+ onCancelNavigation={() => navigation.goBack()}
113
+ />
114
+ );
115
+ }
116
+ ```
117
+
118
+ ### Exemple complet (compatible avec la page de navigation)
119
+
120
+ ```tsx
121
+ import { MapboxNavigationView } from '@jacques_gordon/expo-mapbox-navigation';
122
+
123
+ export default function NavigationScreen({ navigation, reservation, currentPosition }) {
124
+ const coordinates = [
125
+ currentPosition, // départ (position actuelle)
126
+ { latitude: 45.7640, longitude: 4.8357 }, // Lyon — waypoint intermédiaire
127
+ reservation.address_to_lat_long, // destination finale
128
+ ];
129
+
130
+ return (
131
+ <MapboxNavigationView
132
+ style={{ flex: 1 }}
133
+ coordinates={coordinates}
134
+ locale="fr"
135
+ vehicleMaxHeight={5.0}
136
+ vehicleMaxWidth={2.5}
137
+ showsEndOfRouteFeedback
138
+ onRouteProgressChanged={async (event) => {
139
+ const { distanceRemaining, durationRemaining, distanceTraveled, fractionTraveled } =
140
+ event.nativeEvent;
141
+
142
+ // Notifier le client que le chauffeur approche
143
+ if (distanceRemaining <= 60) {
144
+ AXIOS_POST('reservations/livetracking/drivernear', { reservation });
145
+ }
146
+
147
+ const eta = new Date(Date.now() + durationRemaining * 1000);
148
+ dispatch(setNavigationData({ eta, distanceRemaining, durationRemaining }));
149
+ }}
150
+ onWaypointArrival={(event) => {
151
+ console.log('Waypoint atteint !', event.nativeEvent);
152
+ }}
153
+ onFinalDestinationArrival={async () => {
154
+ await playSound(SOUNDS.ROAD_RECALCULATE);
155
+ showToast({ toast_type: 'validation', title: 'Arrivée', body: 'Destination atteinte !' });
156
+ }}
157
+ onCancelNavigation={() => navigation.goBack()}
158
+ onRouteChanged={() => console.log('Reroutage en cours...')}
159
+ onUserOffRoute={() => console.log('Hors route !')}
160
+ onRoutesLoaded={() => console.log('Routes chargées')}
161
+ />
162
+ );
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Props
169
+
170
+ ### Route
171
+
172
+ | Prop | Type | Requis | Description |
173
+ |------|------|--------|-------------|
174
+ | `coordinates` | `Coordinate[]` | **Oui** | Tableau de `{latitude, longitude}`. Minimum 2 points. |
175
+ | `waypointIndices` | `number[]` | Non | Indices des points considérés comme waypoints. Par défaut: tous. |
176
+
177
+ ### Options de routing
178
+
179
+ | Prop | Type | Défaut | Description |
180
+ |------|------|--------|-------------|
181
+ | `locale` | `string` | locale appareil | Langue des instructions. Ex: `"fr"`, `"en"`, `"de"` |
182
+ | `routeProfile` | `string` | `"mapbox/driving-traffic"` | Profil de routing. Android: sans `"mapbox/"`. |
183
+ | `routeExcludeList` | `string[]` | `[]` | Routes à exclure: `"toll"`, `"ferry"`, `"motorway"` |
184
+ | `useRouteMatchingApi` | `boolean` | `false` | Utilise l'API Map Matching au lieu de l'API Directions |
185
+ | `disableAlternativeRoutes` | `boolean` | `false` | Désactive les routes alternatives |
186
+ | `mute` | `boolean` | `false` | Coupe le son de navigation au démarrage |
187
+ | `vehicleMaxHeight` | `number` | — | Hauteur max du véhicule en mètres |
188
+ | `vehicleMaxWidth` | `number` | — | Largeur max du véhicule en mètres |
189
+
190
+ ### Personnalisation visuelle
191
+
192
+ | Prop | Type | Défaut | Description |
193
+ |------|------|--------|-------------|
194
+ | `mapStyle` | `string` | style par défaut | URL du style Mapbox |
195
+ | `customRasterSourceUrl` | `string` | — | URL template raster personnalisée (ex: OpenStreetMap) |
196
+ | `placeCustomRasterLayerAbove` | `string` | — | ID du layer au-dessus duquel placer la couche raster |
197
+ | `showsEndOfRouteFeedback` | `boolean` | `false` | Affiche l'UI de feedback à l'arrivée |
198
+
199
+ ### Callbacks
200
+
201
+ | Prop | Paramètres | Description |
202
+ |------|-----------|-------------|
203
+ | `onRouteProgressChanged` | `{ nativeEvent: RouteProgress }` | Progression sur la route (appelé fréquemment) |
204
+ | `onWaypointArrival` | `{ nativeEvent: RouteProgress }` (Android only) | Arrivée à un waypoint intermédiaire |
205
+ | `onFinalDestinationArrival` | — | Arrivée à la destination finale |
206
+ | `onCancelNavigation` | — | Annulation de la navigation par l'utilisateur |
207
+ | `onRouteChanged` | — | Route modifiée ou reroutage |
208
+ | `onUserOffRoute` | — | L'utilisateur est sorti de la route |
209
+ | `onRoutesLoaded` | — | Routes chargées et prêtes |
210
+
211
+ ---
212
+
213
+ ## Types TypeScript
214
+
215
+ ```ts
216
+ interface Coordinate {
217
+ latitude: number;
218
+ longitude: number;
219
+ }
220
+
221
+ interface RouteProgress {
222
+ distanceRemaining: number; // mètres restants
223
+ distanceTraveled: number; // mètres parcourus
224
+ durationRemaining: number; // secondes restantes
225
+ fractionTraveled: number; // 0.0 → 1.0
226
+ }
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Plugin `app.json` — Options complètes
232
+
233
+ ```json
234
+ [
235
+ "@jacques_gordon/expo-mapbox-navigation",
236
+ {
237
+ "accessToken": "pk.eyJ1...",
238
+ "mapboxMapsVersion": "11.11.0",
239
+ "androidColorOverrides": {
240
+ "mapbox_main_maneuver_background_color": "#1E88E5",
241
+ "mapbox_sub_maneuver_background_color": "#1565C0",
242
+ "mapbox_banner_background_color": "#FFFFFF"
243
+ }
244
+ }
245
+ ]
246
+ ```
247
+
248
+ ## Notes importantes
249
+
250
+ - Nécessite **Expo Dev Client** (Expo Go non supporté)
251
+ - iOS minimum : **14.0**
252
+ - Android minimum SDK : **24**
253
+ - Le SDK Mapbox Navigation est propriétaire — facturation via votre compte Mapbox
254
+ - Pour iOS, les `.xcframework` du SDK Mapbox Navigation doivent être compilés et inclus dans `ios/Frameworks/`
255
+ (voir [instructions dans le README du repo source](https://github.com/uju777/expo-mapbox-navigation))
256
+
257
+ ---
258
+
259
+ ## Obtenir vos tokens Mapbox
260
+
261
+ 1. Créer un compte sur [mapbox.com](https://www.mapbox.com/)
262
+ 2. Aller dans [Account > Tokens](https://account.mapbox.com/access-tokens/)
263
+ 3. **Token public** (`pk.eyJ1...`) — token par défaut ou nouveau token
264
+ 4. **Token secret** (`sk.eyJ1...`) — nouveau token avec le scope `Downloads:Read`
265
+
266
+ ---
267
+
268
+ ## Licence
269
+
270
+ MIT
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #Sat Jun 27 20:48:59 CEST 2026
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,54 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+ apply plugin: 'maven-publish'
4
+
5
+ buildscript {
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ if (expoModulesCorePlugin.exists()) {
8
+ apply from: expoModulesCorePlugin
9
+ applyKotlinExpoModulesCorePlugin()
10
+ }
11
+ }
12
+
13
+ android {
14
+ compileSdkVersion 34
15
+ namespace "expo.modules.mapboxnavigation"
16
+
17
+ defaultConfig {
18
+ minSdkVersion 24
19
+ targetSdkVersion 34
20
+ }
21
+
22
+ compileOptions {
23
+ sourceCompatibility JavaVersion.VERSION_17
24
+ targetCompatibility JavaVersion.VERSION_17
25
+ }
26
+
27
+ kotlinOptions {
28
+ jvmTarget = "17"
29
+ }
30
+ }
31
+
32
+ repositories {
33
+ maven {
34
+ url 'https://api.mapbox.com/downloads/v2/releases/maven'
35
+ authentication {
36
+ basic(BasicAuthentication)
37
+ }
38
+ credentials {
39
+ username = "mapbox"
40
+ password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv("MAPBOX_DOWNLOADS_TOKEN") ?: ""
41
+ }
42
+ }
43
+ google()
44
+ mavenCentral()
45
+ }
46
+
47
+ dependencies {
48
+ implementation project(':expo-modules-core')
49
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion()}"
50
+
51
+ // Mapbox Navigation SDK v3 + Drop-in UI
52
+ api "com.mapbox.navigation:android:3.3.0"
53
+ api "com.mapbox.navigation:ui-dropin:3.3.0"
54
+ }
@@ -0,0 +1,85 @@
1
+ package expo.modules.mapboxnavigation
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class ExpoMapboxNavigationModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("ExpoMapboxNavigation")
9
+
10
+ View(ExpoMapboxNavigationView::class) {
11
+
12
+ // ─── Props ──────────────────────────────────────────────────────
13
+
14
+ Prop("coordinates") { view: ExpoMapboxNavigationView, value: List<Map<String, Double>> ->
15
+ view.coordinates = value
16
+ view.startNavigationIfReady()
17
+ }
18
+
19
+ Prop("waypointIndices") { view: ExpoMapboxNavigationView, value: List<Int> ->
20
+ view.waypointIndices = value
21
+ }
22
+
23
+ Prop("useRouteMatchingApi") { view: ExpoMapboxNavigationView, value: Boolean ->
24
+ view.useRouteMatchingApi = value
25
+ }
26
+
27
+ Prop("locale") { view: ExpoMapboxNavigationView, value: String ->
28
+ view.locale = value
29
+ }
30
+
31
+ Prop("routeProfile") { view: ExpoMapboxNavigationView, value: String ->
32
+ // Android: sans le préfixe "mapbox/"
33
+ view.routeProfile = value.removePrefix("mapbox/")
34
+ }
35
+
36
+ Prop("routeExcludeList") { view: ExpoMapboxNavigationView, value: List<String> ->
37
+ view.routeExcludeList = value
38
+ }
39
+
40
+ Prop("mapStyle") { view: ExpoMapboxNavigationView, value: String ->
41
+ view.mapStyle = value
42
+ }
43
+
44
+ Prop("mute") { view: ExpoMapboxNavigationView, value: Boolean ->
45
+ view.mute = value
46
+ }
47
+
48
+ Prop("vehicleMaxHeight") { view: ExpoMapboxNavigationView, value: Double ->
49
+ view.vehicleMaxHeight = value
50
+ }
51
+
52
+ Prop("vehicleMaxWidth") { view: ExpoMapboxNavigationView, value: Double ->
53
+ view.vehicleMaxWidth = value
54
+ }
55
+
56
+ Prop("customRasterSourceUrl") { view: ExpoMapboxNavigationView, value: String ->
57
+ view.customRasterSourceUrl = value
58
+ }
59
+
60
+ Prop("placeCustomRasterLayerAbove") { view: ExpoMapboxNavigationView, value: String ->
61
+ view.placeCustomRasterLayerAbove = value
62
+ }
63
+
64
+ Prop("disableAlternativeRoutes") { view: ExpoMapboxNavigationView, value: Boolean ->
65
+ view.disableAlternativeRoutes = value
66
+ }
67
+
68
+ Prop("showsEndOfRouteFeedback") { view: ExpoMapboxNavigationView, value: Boolean ->
69
+ view.showsEndOfRouteFeedback = value
70
+ }
71
+
72
+ // ─── Events ─────────────────────────────────────────────────────
73
+
74
+ Events(
75
+ "onRouteProgressChanged",
76
+ "onWaypointArrival",
77
+ "onFinalDestinationArrival",
78
+ "onCancelNavigation",
79
+ "onRouteChanged",
80
+ "onUserOffRoute",
81
+ "onRoutesLoaded"
82
+ )
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,184 @@
1
+ package expo.modules.mapboxnavigation
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import com.mapbox.api.directions.v5.DirectionsCriteria
6
+ import com.mapbox.api.directions.v5.models.RouteOptions
7
+ import com.mapbox.geojson.Point
8
+ import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
9
+ import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions
10
+ import com.mapbox.navigation.base.route.NavigationRoute
11
+ import com.mapbox.navigation.base.route.NavigationRouterCallback
12
+ import com.mapbox.navigation.base.route.RouterFailure
13
+ import com.mapbox.navigation.base.route.RouterOrigin
14
+ import com.mapbox.navigation.base.trip.model.RouteProgress
15
+ import com.mapbox.navigation.base.trip.model.RouteProgressState
16
+ import com.mapbox.navigation.core.MapboxNavigation
17
+ import com.mapbox.navigation.core.MapboxNavigationProvider
18
+ import com.mapbox.navigation.core.directions.session.RoutesObserver
19
+ import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
20
+ import com.mapbox.navigation.core.trip.session.RouteProgressObserver
21
+ import com.mapbox.navigation.dropin.NavigationView
22
+ import com.mapbox.navigation.dropin.map.MapStyleLoader
23
+ import expo.modules.kotlin.AppContext
24
+ import expo.modules.kotlin.views.ExpoView
25
+ import java.util.Locale
26
+
27
+ class ExpoMapboxNavigationView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
28
+
29
+ // ─── Props ──────────────────────────────────────────────────────────────
30
+ var coordinates: List<Map<String, Double>> = emptyList()
31
+ var waypointIndices: List<Int> = emptyList()
32
+ var useRouteMatchingApi: Boolean = false
33
+ var locale: String = ""
34
+ var routeProfile: String = "driving-traffic"
35
+ var routeExcludeList: List<String> = emptyList()
36
+ var mapStyle: String? = null
37
+ var mute: Boolean = false
38
+ var vehicleMaxHeight: Double? = null
39
+ var vehicleMaxWidth: Double? = null
40
+ var customRasterSourceUrl: String? = null
41
+ var placeCustomRasterLayerAbove: String? = null
42
+ var disableAlternativeRoutes: Boolean = false
43
+ var showsEndOfRouteFeedback: Boolean = false
44
+
45
+ // ─── Events ─────────────────────────────────────────────────────────────
46
+ private val onRouteProgressChanged by lazy { appContext.eventDispatcher("onRouteProgressChanged") }
47
+ private val onWaypointArrival by lazy { appContext.eventDispatcher("onWaypointArrival") }
48
+ private val onFinalDestinationArrival by lazy { appContext.eventDispatcher("onFinalDestinationArrival") }
49
+ private val onCancelNavigation by lazy { appContext.eventDispatcher("onCancelNavigation") }
50
+ private val onRouteChanged by lazy { appContext.eventDispatcher("onRouteChanged") }
51
+ private val onUserOffRoute by lazy { appContext.eventDispatcher("onUserOffRoute") }
52
+ private val onRoutesLoaded by lazy { appContext.eventDispatcher("onRoutesLoaded") }
53
+
54
+ // ─── Internal ───────────────────────────────────────────────────────────
55
+ private var navigationView: NavigationView? = null
56
+ private var isStarted = false
57
+ private var currentWaypointIndex = 0
58
+ private var mapboxNavigation: MapboxNavigation? = null
59
+
60
+ fun startNavigationIfReady() {
61
+ if (coordinates.size >= 2 && !isStarted) {
62
+ isStarted = true
63
+ post { setupNavigationView() }
64
+ }
65
+ }
66
+
67
+ private fun setupNavigationView() {
68
+ val activity = context as? androidx.fragment.app.FragmentActivity ?: return
69
+
70
+ // Créer la NavigationView (drop-in UI Mapbox)
71
+ val navView = NavigationView(context).also {
72
+ it.layoutParams = ViewGroup.LayoutParams(
73
+ ViewGroup.LayoutParams.MATCH_PARENT,
74
+ ViewGroup.LayoutParams.MATCH_PARENT
75
+ )
76
+ }
77
+ navigationView = navView
78
+ addView(navView)
79
+
80
+ // Observer la progression
81
+ navView.registerRouteProgressObserver(object : RouteProgressObserver {
82
+ override fun onRouteProgressChanged(routeProgress: RouteProgress) {
83
+ val progressData = mapOf(
84
+ "distanceRemaining" to routeProgress.distanceRemaining,
85
+ "distanceTraveled" to routeProgress.distanceTraveled,
86
+ "durationRemaining" to routeProgress.durationRemaining,
87
+ "fractionTraveled" to routeProgress.fractionTraveled
88
+ )
89
+
90
+ onRouteProgressChanged.emit(progressData)
91
+
92
+ // Arrivée à un waypoint intermédiaire
93
+ if (routeProgress.currentState == RouteProgressState.COMPLETE) {
94
+ val legIndex = routeProgress.currentLegProgress?.legIndex ?: 0
95
+ val totalLegs = routeProgress.navigationRoute?.directionsRoute?.legs()?.size ?: 1
96
+
97
+ if (legIndex < totalLegs - 1) {
98
+ onWaypointArrival.emit(progressData)
99
+ } else {
100
+ onFinalDestinationArrival.emit(emptyMap<String, Any>())
101
+ }
102
+ }
103
+ }
104
+ })
105
+
106
+ // Observer les changements de route (reroutage)
107
+ navView.registerRoutesObserver(object : RoutesObserver {
108
+ override fun onRoutesChanged(result: RoutesObserver.RoutesUpdatedResult) {
109
+ if (result.navigationRoutes.isNotEmpty()) {
110
+ onRouteChanged.emit(emptyMap<String, Any>())
111
+ }
112
+ }
113
+ })
114
+
115
+ // Construire et lancer la route
116
+ requestRoute(navView)
117
+ }
118
+
119
+ private fun requestRoute(navView: NavigationView) {
120
+ val points = coordinates.mapNotNull { coord ->
121
+ val lat = coord["latitude"] ?: return@mapNotNull null
122
+ val lng = coord["longitude"] ?: return@mapNotNull null
123
+ Point.fromLngLat(lng, lat)
124
+ }
125
+ if (points.size < 2) return
126
+
127
+ val origin = points.first()
128
+ val destination = points.last()
129
+ val waypoints = if (points.size > 2) points.subList(1, points.size - 1) else emptyList()
130
+
131
+ val resolvedLocale = if (locale.isNotEmpty()) Locale(locale) else Locale.getDefault()
132
+
133
+ val profile = when (routeProfile) {
134
+ "driving" -> DirectionsCriteria.PROFILE_DRIVING
135
+ "walking" -> DirectionsCriteria.PROFILE_WALKING
136
+ "cycling" -> DirectionsCriteria.PROFILE_CYCLING
137
+ else -> DirectionsCriteria.PROFILE_DRIVING_TRAFFIC
138
+ }
139
+
140
+ val routeOptionsBuilder = RouteOptions.builder()
141
+ .applyDefaultNavigationOptions(profile)
142
+ .applyLanguageAndVoiceUnitOptions(context)
143
+ .language(resolvedLocale.language)
144
+ .coordinatesList(listOf(origin) + waypoints + listOf(destination))
145
+
146
+ if (!disableAlternativeRoutes) {
147
+ routeOptionsBuilder.alternatives(true)
148
+ }
149
+
150
+ if (routeExcludeList.isNotEmpty()) {
151
+ routeOptionsBuilder.exclude(routeExcludeList.joinToString(","))
152
+ }
153
+
154
+ vehicleMaxHeight?.let { routeOptionsBuilder.maxHeight(it) }
155
+ vehicleMaxWidth?.let { routeOptionsBuilder.maxWidth(it) }
156
+
157
+ MapboxNavigationApp.current()?.requestRoutes(
158
+ routeOptionsBuilder.build(),
159
+ object : NavigationRouterCallback {
160
+ override fun onRoutesReady(routes: List<NavigationRoute>, routerOrigin: RouterOrigin) {
161
+ onRoutesLoaded.emit(emptyMap<String, Any>())
162
+ MapboxNavigationApp.current()?.setNavigationRoutes(routes)
163
+ navView.api.startActiveGuidance(routes)
164
+ }
165
+
166
+ override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
167
+ android.util.Log.e("ExpoMapboxNavigation", "Erreur route: ${reasons.firstOrNull()?.message}")
168
+ }
169
+
170
+ override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {}
171
+ }
172
+ )
173
+
174
+ // Bouton d'annulation
175
+ navView.addOnClickListener {
176
+ onCancelNavigation.emit(emptyMap<String, Any>())
177
+ }
178
+ }
179
+
180
+ override fun onDetachedFromWindow() {
181
+ super.onDetachedFromWindow()
182
+ MapboxNavigationApp.current()?.setNavigationRoutes(emptyList())
183
+ }
184
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build/index').default;
@@ -0,0 +1,132 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+ export interface Coordinate {
3
+ latitude: number;
4
+ longitude: number;
5
+ }
6
+ export interface RouteProgress {
7
+ /** Distance restante en mètres */
8
+ distanceRemaining: number;
9
+ /** Distance parcourue en mètres */
10
+ distanceTraveled: number;
11
+ /** Durée restante en secondes */
12
+ durationRemaining: number;
13
+ /** Fraction parcourue (0.0 → 1.0) */
14
+ fractionTraveled: number;
15
+ }
16
+ export interface NativeRouteProgressEvent {
17
+ nativeEvent: RouteProgress;
18
+ }
19
+ export interface NativeWaypointEvent {
20
+ nativeEvent: RouteProgress;
21
+ }
22
+ export interface MapboxNavigationViewProps {
23
+ /** Style du composant (ex: { flex: 1 }) */
24
+ style?: StyleProp<ViewStyle>;
25
+ /**
26
+ * Tableau de coordonnées { latitude, longitude }.
27
+ * Minimum 2 points (départ + destination).
28
+ * Requis.
29
+ */
30
+ coordinates: Coordinate[];
31
+ /**
32
+ * Indices dans `coordinates` considérés comme des waypoints/destinations intermédiaires.
33
+ * Par défaut, tous les points sont des waypoints.
34
+ * Au moins le premier et le dernier index doivent être inclus.
35
+ */
36
+ waypointIndices?: number[];
37
+ /**
38
+ * Utilise l'API Map Matching de Mapbox au lieu de l'API Directions standard.
39
+ * Utile pour forcer un trajet suivant exactement les coordonnées fournies.
40
+ * Défaut: false
41
+ */
42
+ useRouteMatchingApi?: boolean;
43
+ /**
44
+ * Locale/langue pour les labels de la carte, les directions et la voix.
45
+ * Exemples: "fr", "en", "de", "nl".
46
+ * Par défaut: locale de l'appareil.
47
+ */
48
+ locale?: string;
49
+ /**
50
+ * Profil de routing Mapbox.
51
+ * iOS: "mapbox/driving-traffic" | "mapbox/driving" | "mapbox/walking" | "mapbox/cycling"
52
+ * Android: "driving-traffic" | "driving" | "walking" | "cycling" (sans "mapbox/" prefix)
53
+ * Défaut: "mapbox/driving-traffic"
54
+ */
55
+ routeProfile?: string;
56
+ /**
57
+ * Types de routes à exclure du calcul d'itinéraire.
58
+ * Exemples: ["toll", "ferry", "motorway"]
59
+ */
60
+ routeExcludeList?: string[];
61
+ /**
62
+ * Style de la carte Mapbox.
63
+ * Exemples: "mapbox://styles/mapbox/navigation-day-v1"
64
+ */
65
+ mapStyle?: string;
66
+ /**
67
+ * Coupe le son de navigation au démarrage.
68
+ * Défaut: false
69
+ */
70
+ mute?: boolean;
71
+ /**
72
+ * Hauteur maximale du véhicule en mètres.
73
+ * Permet d'éviter les routes avec restriction de hauteur.
74
+ */
75
+ vehicleMaxHeight?: number;
76
+ /**
77
+ * Largeur maximale du véhicule en mètres.
78
+ * Permet d'éviter les routes avec restriction de largeur.
79
+ */
80
+ vehicleMaxWidth?: number;
81
+ /**
82
+ * URL d'une source raster personnalisée pour la carte.
83
+ * Doit être une URL template avec {x}, {y}, {z}.
84
+ * Exemple: "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
85
+ */
86
+ customRasterSourceUrl?: string;
87
+ /**
88
+ * ID du layer au-dessus duquel placer le layer raster personnalisé.
89
+ */
90
+ placeCustomRasterLayerAbove?: string;
91
+ /**
92
+ * Désactive le calcul et l'affichage des routes alternatives.
93
+ * Défaut: false
94
+ */
95
+ disableAlternativeRoutes?: boolean;
96
+ /**
97
+ * Affiche l'UI de feedback en fin de route.
98
+ * Défaut: false
99
+ */
100
+ showsEndOfRouteFeedback?: boolean;
101
+ /**
102
+ * Appelé régulièrement avec la progression sur la route.
103
+ * L'objet est dans event.nativeEvent.
104
+ */
105
+ onRouteProgressChanged?: (event: NativeRouteProgressEvent) => void;
106
+ /**
107
+ * Appelé quand l'utilisateur arrive à un waypoint intermédiaire.
108
+ * Sur Android uniquement: fournit les données de progression dans event.nativeEvent.
109
+ */
110
+ onWaypointArrival?: (event: NativeWaypointEvent) => void;
111
+ /**
112
+ * Appelé quand l'utilisateur arrive à la destination finale.
113
+ */
114
+ onFinalDestinationArrival?: () => void;
115
+ /**
116
+ * Appelé quand l'utilisateur appuie sur le bouton d'annulation.
117
+ * La navigation ne se ferme pas automatiquement — gérer manuellement (ex: navigation.goBack()).
118
+ */
119
+ onCancelNavigation?: (event?: any) => void;
120
+ /**
121
+ * Appelé quand la route change ou qu'un reroutage est effectué.
122
+ */
123
+ onRouteChanged?: () => void;
124
+ /**
125
+ * Appelé quand l'utilisateur sort de la route.
126
+ */
127
+ onUserOffRoute?: () => void;
128
+ /**
129
+ * Appelé quand les routes sont chargées et prêtes.
130
+ */
131
+ onRoutesLoaded?: () => void;
132
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import type { MapboxNavigationViewProps } from './MapboxNavigation.types';
3
+ /**
4
+ * MapboxNavigationView
5
+ *
6
+ * Composant de navigation Mapbox turn-by-turn pour Expo SDK 53+.
7
+ * Requiert Expo Dev Client (incompatible avec Expo Go).
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * <MapboxNavigationView
12
+ * style={{ flex: 1 }}
13
+ * coordinates={[
14
+ * { latitude: 48.8566, longitude: 2.3522 }, // Paris (départ)
15
+ * { latitude: 45.7640, longitude: 4.8357 }, // Lyon (waypoint)
16
+ * { latitude: 43.2965, longitude: 5.3698 }, // Marseille (destination)
17
+ * ]}
18
+ * locale="fr"
19
+ * vehicleMaxHeight={5.0}
20
+ * vehicleMaxWidth={2.5}
21
+ * onRouteProgressChanged={(event) => {
22
+ * const { distanceRemaining, durationRemaining } = event.nativeEvent;
23
+ * console.log(`${(distanceRemaining / 1000).toFixed(1)} km restants`);
24
+ * }}
25
+ * onFinalDestinationArrival={() => console.log('Arrivé !')}
26
+ * onCancelNavigation={() => navigation.goBack()}
27
+ * />
28
+ * ```
29
+ */
30
+ export default function MapboxNavigationView(props: MapboxNavigationViewProps): React.JSX.Element;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = MapboxNavigationView;
7
+ const react_1 = __importDefault(require("react"));
8
+ const expo_modules_core_1 = require("expo-modules-core");
9
+ const NativeView = (0, expo_modules_core_1.requireNativeViewManager)('ExpoMapboxNavigation');
10
+ /**
11
+ * MapboxNavigationView
12
+ *
13
+ * Composant de navigation Mapbox turn-by-turn pour Expo SDK 53+.
14
+ * Requiert Expo Dev Client (incompatible avec Expo Go).
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <MapboxNavigationView
19
+ * style={{ flex: 1 }}
20
+ * coordinates={[
21
+ * { latitude: 48.8566, longitude: 2.3522 }, // Paris (départ)
22
+ * { latitude: 45.7640, longitude: 4.8357 }, // Lyon (waypoint)
23
+ * { latitude: 43.2965, longitude: 5.3698 }, // Marseille (destination)
24
+ * ]}
25
+ * locale="fr"
26
+ * vehicleMaxHeight={5.0}
27
+ * vehicleMaxWidth={2.5}
28
+ * onRouteProgressChanged={(event) => {
29
+ * const { distanceRemaining, durationRemaining } = event.nativeEvent;
30
+ * console.log(`${(distanceRemaining / 1000).toFixed(1)} km restants`);
31
+ * }}
32
+ * onFinalDestinationArrival={() => console.log('Arrivé !')}
33
+ * onCancelNavigation={() => navigation.goBack()}
34
+ * />
35
+ * ```
36
+ */
37
+ function MapboxNavigationView(props) {
38
+ return (<NativeView
39
+ // Valeurs par défaut
40
+ mute={false} useRouteMatchingApi={false} disableAlternativeRoutes={false} showsEndOfRouteFeedback={false} waypointIndices={[]} routeExcludeList={[]} {...props}/>);
41
+ }
@@ -0,0 +1,2 @@
1
+ export { default as MapboxNavigationView } from './MapboxNavigationView';
2
+ export type { MapboxNavigationViewProps, Coordinate, RouteProgress, NativeRouteProgressEvent, NativeWaypointEvent, } from './MapboxNavigation.types';
package/build/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MapboxNavigationView = void 0;
7
+ var MapboxNavigationView_1 = require("./MapboxNavigationView");
8
+ Object.defineProperty(exports, "MapboxNavigationView", { enumerable: true, get: function () { return __importDefault(MapboxNavigationView_1).default; } });
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "android": {
4
+ "modules": ["expo.modules.mapboxnavigation.ExpoMapboxNavigationModule"]
5
+ },
6
+ "ios": {
7
+ "modules": ["ExpoMapboxNavigationModule"]
8
+ }
9
+ }
@@ -0,0 +1,25 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'ExpoMapboxNavigation'
3
+ s.version = '1.0.0'
4
+ s.summary = 'Expo module for Mapbox turn-by-turn navigation — iOS & Android'
5
+ s.homepage = 'https://github.com/YOUR_GITHUB/expo-mapbox-navigation'
6
+ s.license = { :type => 'MIT' }
7
+ s.authors = { 'Your Name' => 'you@example.com' }
8
+ s.platforms = { :ios => '14.0' }
9
+ s.swift_version = '5.7'
10
+ s.source = { :git => '' }
11
+
12
+ s.static_framework = true
13
+
14
+ s.dependency 'ExpoModulesCore'
15
+ # MapboxNavigationUIKit + MapboxNavigationCore are bundled as .xcframework
16
+ # See README for how to build them from source
17
+
18
+ s.source_files = '*.{swift,h,m,mm,cpp}'
19
+ s.preserve_paths = 'Frameworks/**/*'
20
+
21
+ s.pod_target_xcconfig = {
22
+ 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) $(PODS_TARGET_SRCROOT)/Frameworks/**',
23
+ 'OTHER_LDFLAGS' => '$(inherited) -framework MapboxNavigationUIKit -framework MapboxNavigationCore -framework MapboxNavigationNative -framework MapboxDirections'
24
+ }
25
+ end
@@ -0,0 +1,81 @@
1
+ import ExpoModulesCore
2
+
3
+ public class ExpoMapboxNavigationModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("ExpoMapboxNavigation")
6
+
7
+ View(ExpoMapboxNavigationView.self) {
8
+
9
+ // ─── Props ────────────────────────────────────────────────────────
10
+
11
+ Prop("coordinates") { (view: ExpoMapboxNavigationView, value: [[String: Double]]) in
12
+ view.coordinates = value
13
+ view.startNavigationIfReady()
14
+ }
15
+
16
+ Prop("waypointIndices") { (view: ExpoMapboxNavigationView, value: [Int]) in
17
+ view.waypointIndices = value
18
+ }
19
+
20
+ Prop("useRouteMatchingApi") { (view: ExpoMapboxNavigationView, value: Bool) in
21
+ view.useRouteMatchingApi = value
22
+ }
23
+
24
+ Prop("locale") { (view: ExpoMapboxNavigationView, value: String) in
25
+ view.locale = value
26
+ }
27
+
28
+ Prop("routeProfile") { (view: ExpoMapboxNavigationView, value: String) in
29
+ view.routeProfile = value
30
+ }
31
+
32
+ Prop("routeExcludeList") { (view: ExpoMapboxNavigationView, value: [String]) in
33
+ view.routeExcludeList = value
34
+ }
35
+
36
+ Prop("mapStyle") { (view: ExpoMapboxNavigationView, value: String) in
37
+ view.mapStyle = value
38
+ }
39
+
40
+ Prop("mute") { (view: ExpoMapboxNavigationView, value: Bool) in
41
+ view.mute = value
42
+ }
43
+
44
+ Prop("vehicleMaxHeight") { (view: ExpoMapboxNavigationView, value: Double) in
45
+ view.vehicleMaxHeight = value
46
+ }
47
+
48
+ Prop("vehicleMaxWidth") { (view: ExpoMapboxNavigationView, value: Double) in
49
+ view.vehicleMaxWidth = value
50
+ }
51
+
52
+ Prop("customRasterSourceUrl") { (view: ExpoMapboxNavigationView, value: String) in
53
+ view.customRasterSourceUrl = value
54
+ }
55
+
56
+ Prop("placeCustomRasterLayerAbove") { (view: ExpoMapboxNavigationView, value: String) in
57
+ view.placeCustomRasterLayerAbove = value
58
+ }
59
+
60
+ Prop("disableAlternativeRoutes") { (view: ExpoMapboxNavigationView, value: Bool) in
61
+ view.disableAlternativeRoutes = value
62
+ }
63
+
64
+ Prop("showsEndOfRouteFeedback") { (view: ExpoMapboxNavigationView, value: Bool) in
65
+ view.showsEndOfRouteFeedback = value
66
+ }
67
+
68
+ // ─── Events ───────────────────────────────────────────────────────
69
+
70
+ Events(
71
+ "onRouteProgressChanged",
72
+ "onWaypointArrival",
73
+ "onFinalDestinationArrival",
74
+ "onCancelNavigation",
75
+ "onRouteChanged",
76
+ "onUserOffRoute",
77
+ "onRoutesLoaded"
78
+ )
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,236 @@
1
+ import ExpoModulesCore
2
+ import MapboxNavigationCore
3
+ import MapboxNavigationUIKit
4
+ import MapboxMaps
5
+ import MapboxDirections
6
+
7
+ class ExpoMapboxNavigationView: ExpoView {
8
+
9
+ // ─── Props ──────────────────────────────────────────────────────────────
10
+ var coordinates: [[String: Double]] = []
11
+ var waypointIndices: [Int] = []
12
+ var useRouteMatchingApi: Bool = false
13
+ var locale: String = ""
14
+ var routeProfile: String = "mapbox/driving-traffic"
15
+ var routeExcludeList: [String] = []
16
+ var mapStyle: String? = nil
17
+ var mute: Bool = false
18
+ var vehicleMaxHeight: Double? = nil
19
+ var vehicleMaxWidth: Double? = nil
20
+ var customRasterSourceUrl: String? = nil
21
+ var placeCustomRasterLayerAbove: String? = nil
22
+ var disableAlternativeRoutes: Bool = false
23
+ var showsEndOfRouteFeedback: Bool = false
24
+
25
+ // ─── Events ─────────────────────────────────────────────────────────────
26
+ let onRouteProgressChanged = EventDispatcher()
27
+ let onWaypointArrival = EventDispatcher()
28
+ let onFinalDestinationArrival = EventDispatcher()
29
+ let onCancelNavigation = EventDispatcher()
30
+ let onRouteChanged = EventDispatcher()
31
+ let onUserOffRoute = EventDispatcher()
32
+ let onRoutesLoaded = EventDispatcher()
33
+
34
+ // ─── Internal ───────────────────────────────────────────────────────────
35
+ private var navigationViewController: NavigationViewController?
36
+ private var isNavigationStarted = false
37
+ private var currentWaypointIndex = 0
38
+ private var totalWaypoints = 0
39
+
40
+ required init(appContext: AppContext? = nil) {
41
+ super.init(appContext: appContext)
42
+ }
43
+
44
+ func startNavigationIfReady() {
45
+ guard coordinates.count >= 2, !isNavigationStarted else { return }
46
+ isNavigationStarted = true
47
+ setupNavigation()
48
+ }
49
+
50
+ override func layoutSubviews() {
51
+ super.layoutSubviews()
52
+ startNavigationIfReady()
53
+ }
54
+
55
+ // ─── Navigation Setup ───────────────────────────────────────────────────
56
+
57
+ private func setupNavigation() {
58
+ // Construire les waypoints
59
+ var allWaypoints: [Waypoint] = []
60
+
61
+ for (index, coord) in coordinates.enumerated() {
62
+ guard let lat = coord["latitude"], let lng = coord["longitude"] else { continue }
63
+ let location = CLLocationCoordinate2D(latitude: lat, longitude: lng)
64
+ let wp = Waypoint(coordinate: location)
65
+
66
+ // Marquer les waypoints intermédiaires vs simples pass-through
67
+ if !waypointIndices.isEmpty {
68
+ wp.separatesLegs = waypointIndices.contains(index)
69
+ } else {
70
+ wp.separatesLegs = true
71
+ }
72
+
73
+ allWaypoints.append(wp)
74
+ }
75
+
76
+ totalWaypoints = allWaypoints.filter { $0.separatesLegs }.count
77
+
78
+ // Choisir le profil de routing
79
+ let profile: ProfileIdentifier
80
+ switch routeProfile {
81
+ case "mapbox/driving": profile = .automobile
82
+ case "mapbox/walking": profile = .walking
83
+ case "mapbox/cycling": profile = .cycling
84
+ default: profile = .automobileAvoidingTraffic
85
+ }
86
+
87
+ // Options de route
88
+ let routeOptions = NavigationRouteOptions(waypoints: allWaypoints, profileIdentifier: profile)
89
+
90
+ let resolvedLocale = locale.isEmpty ? Locale.current.identifier : locale
91
+ routeOptions.locale = Locale(identifier: resolvedLocale)
92
+ routeOptions.includesSpokenInstructions = true
93
+ routeOptions.includesVisualInstructions = true
94
+
95
+ // Exclusions
96
+ if !routeExcludeList.isEmpty {
97
+ routeOptions.roadClassesToAvoid = buildRoadClassesToAvoid(routeExcludeList)
98
+ }
99
+
100
+ // Restrictions de véhicule
101
+ if let maxH = vehicleMaxHeight {
102
+ routeOptions.maximumHeight = Measurement(value: maxH, unit: .meters)
103
+ }
104
+ if let maxW = vehicleMaxWidth {
105
+ routeOptions.maximumWidth = Measurement(value: maxW, unit: .meters)
106
+ }
107
+
108
+ if disableAlternativeRoutes {
109
+ routeOptions.includesAlternativeRoutes = false
110
+ }
111
+
112
+ // Calcul de la route
113
+ Directions.shared.calculate(routeOptions) { [weak self] (_, result) in
114
+ guard let self = self else { return }
115
+ switch result {
116
+ case .failure(let error):
117
+ print("[ExpoMapboxNavigation] Erreur calcul route: \(error.localizedDescription)")
118
+ case .success(let response):
119
+ guard let routes = response.routes, !routes.isEmpty else { return }
120
+ self.onRoutesLoaded([:])
121
+ self.presentNavigationUI(routes: routes, options: routeOptions)
122
+ }
123
+ }
124
+ }
125
+
126
+ private func presentNavigationUI(routes: [Route], options: NavigationRouteOptions) {
127
+ DispatchQueue.main.async { [weak self] in
128
+ guard let self = self else { return }
129
+
130
+ let navigationService = MapboxNavigationService(
131
+ routes: routes,
132
+ customRoutingProvider: NavigationSettings.shared.directions,
133
+ credentials: NavigationSettings.shared.directions.credentials,
134
+ simulating: .onPoorGPS
135
+ )
136
+
137
+ let navigationOptions = NavigationOptions(navigationService: navigationService)
138
+ let navVC = NavigationViewController(
139
+ for: routes,
140
+ routeIndex: 0,
141
+ routeOptions: options,
142
+ navigationOptions: navigationOptions
143
+ )
144
+
145
+ navVC.delegate = self
146
+ navVC.showsEndOfRouteFeedback = self.showsEndOfRouteFeedback
147
+ navVC.showsReportFeedback = false
148
+
149
+ if self.mute {
150
+ navVC.navigationService.router.reroutesProactively = false
151
+ }
152
+
153
+ self.navigationViewController = navVC
154
+
155
+ guard let parentVC = self.parentViewController else { return }
156
+ parentVC.addChild(navVC)
157
+ navVC.view.frame = self.bounds
158
+ navVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
159
+ self.addSubview(navVC.view)
160
+ navVC.didMove(toParent: parentVC)
161
+ }
162
+ }
163
+
164
+ private func buildRoadClassesToAvoid(_ list: [String]) -> RoadClasses {
165
+ var classes = RoadClasses()
166
+ for item in list {
167
+ switch item {
168
+ case "toll": classes.insert(.toll)
169
+ case "ferry": classes.insert(.ferry)
170
+ case "motorway": classes.insert(.motorway)
171
+ default: break
172
+ }
173
+ }
174
+ return classes
175
+ }
176
+
177
+ private var parentViewController: UIViewController? {
178
+ var responder: UIResponder? = self
179
+ while let r = responder {
180
+ if let vc = r as? UIViewController { return vc }
181
+ responder = r.next
182
+ }
183
+ return nil
184
+ }
185
+ }
186
+
187
+ // ─── NavigationViewControllerDelegate ───────────────────────────────────────
188
+
189
+ extension ExpoMapboxNavigationView: NavigationViewControllerDelegate {
190
+
191
+ func navigationViewController(
192
+ _ vc: NavigationViewController,
193
+ didUpdate progress: RouteProgress,
194
+ with location: CLLocation,
195
+ rawLocation: CLLocation
196
+ ) {
197
+ onRouteProgressChanged([
198
+ "distanceRemaining": progress.currentLegProgress.distanceRemaining,
199
+ "distanceTraveled": progress.distanceTraveled,
200
+ "durationRemaining": progress.durationRemaining,
201
+ "fractionTraveled": progress.fractionTraveled
202
+ ])
203
+
204
+ // Détection hors route
205
+ if progress.currentLegProgress.userHasArrivedAtWaypoint {
206
+ currentWaypointIndex += 1
207
+ if currentWaypointIndex < totalWaypoints - 1 {
208
+ onWaypointArrival([
209
+ "distanceRemaining": progress.currentLegProgress.distanceRemaining,
210
+ "distanceTraveled": progress.distanceTraveled,
211
+ "durationRemaining": progress.durationRemaining,
212
+ "fractionTraveled": progress.fractionTraveled
213
+ ])
214
+ }
215
+ }
216
+ }
217
+
218
+ func navigationViewControllerDidFinishRouting(_ vc: NavigationViewController) {
219
+ onFinalDestinationArrival([:])
220
+ }
221
+
222
+ func navigationViewControllerDidDismiss(_ vc: NavigationViewController, byCanceling canceled: Bool) {
223
+ if canceled {
224
+ onCancelNavigation([:])
225
+ }
226
+ }
227
+
228
+ func navigationViewController(_ vc: NavigationViewController, didRerouteAlong route: Route) {
229
+ onRouteChanged([:])
230
+ }
231
+
232
+ func navigationViewController(_ vc: NavigationViewController, shouldRerouteFrom location: CLLocation) -> Bool {
233
+ onUserOffRoute([:])
234
+ return true
235
+ }
236
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@jacques_gordon/expo-mapbox-navigation",
3
+ "version": "1.0.0",
4
+ "description": "Expo module for Mapbox turn-by-turn navigation — iOS & Android, compatible with Expo SDK 53 Dev Client",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "files": [
8
+ "build",
9
+ "ios",
10
+ "android",
11
+ "plugin/build",
12
+ "app.plugin.js",
13
+ "expo-module.config.json",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "expo-module build",
18
+ "clean": "expo-module clean",
19
+ "lint": "expo-module lint",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepare": "expo-module prepare",
22
+ "prepublishOnly": "expo-module prepublishOnly"
23
+ },
24
+ "keywords": [
25
+ "expo",
26
+ "mapbox",
27
+ "navigation",
28
+ "turn-by-turn",
29
+ "react-native",
30
+ "expo-module",
31
+ "gps",
32
+ "waypoints"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation/issues"
41
+ },
42
+ "homepage": "https://github.com/jacquesngomeheffa/expo-mapbox-navigation#readme",
43
+ "peerDependencies": {
44
+ "expo": ">=53.0.0",
45
+ "react": "*",
46
+ "react-native": "*"
47
+ },
48
+ "devDependencies": {
49
+ "@expo/config-plugins": "^9.0.0",
50
+ "@types/react": "^19.2.17",
51
+ "expo-module-scripts": "^56.0.3",
52
+ "expo-modules-core": "^2.0.0",
53
+ "typescript": "^5.3.0"
54
+ },
55
+ "expo": {
56
+ "fyi": "https://expo.fyi/",
57
+ "config": "./plugin/build/index.js"
58
+ },
59
+ "dependencies": {
60
+ "react": "^19.2.7",
61
+ "react-native": "^0.86.0"
62
+ }
63
+ }