@atomiqlab/react-native-mapbox-navigation 1.1.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.
@@ -0,0 +1,296 @@
1
+ package expo.modules.mapboxnavigation
2
+
3
+ import android.content.Intent
4
+ import expo.modules.kotlin.Promise
5
+ import expo.modules.kotlin.modules.Module
6
+ import expo.modules.kotlin.modules.ModuleDefinition
7
+
8
+ class MapboxNavigationModule : Module() {
9
+ private var isNavigating = false
10
+
11
+ override fun definition() = ModuleDefinition {
12
+ Name("MapboxNavigationModule")
13
+
14
+ Events(
15
+ "onLocationChange",
16
+ "onRouteProgressChange",
17
+ "onBannerInstruction",
18
+ "onArrive",
19
+ "onCancelNavigation",
20
+ "onError"
21
+ )
22
+
23
+ OnCreate {
24
+ MapboxNavigationEventBridge.setEmitter { eventName, payload ->
25
+ sendEvent(eventName, payload)
26
+ }
27
+ }
28
+
29
+ OnDestroy {
30
+ MapboxNavigationEventBridge.clearEmitter()
31
+ }
32
+
33
+ AsyncFunction("startNavigation") { options: Map<String, Any?>, promise: Promise ->
34
+ startNavigation(options, promise)
35
+ }
36
+
37
+ AsyncFunction("stopNavigation") { promise: Promise ->
38
+ stopNavigation(promise)
39
+ }
40
+
41
+ AsyncFunction("setMuted") { _: Boolean, promise: Promise ->
42
+ // Voice control is handled by native navigation UI state.
43
+ promise.resolve(null)
44
+ }
45
+
46
+ AsyncFunction("setVoiceVolume") { _: Double, promise: Promise ->
47
+ promise.resolve(null)
48
+ }
49
+
50
+ AsyncFunction("setDistanceUnit") { _: String, promise: Promise ->
51
+ promise.resolve(null)
52
+ }
53
+
54
+ AsyncFunction("setLanguage") { _: String, promise: Promise ->
55
+ promise.resolve(null)
56
+ }
57
+
58
+ AsyncFunction("isNavigating") { promise: Promise ->
59
+ promise.resolve(isNavigating)
60
+ }
61
+
62
+ AsyncFunction("getNavigationSettings") { promise: Promise ->
63
+ promise.resolve(
64
+ mapOf(
65
+ "isNavigating" to isNavigating,
66
+ "mute" to false,
67
+ "voiceVolume" to 1.0,
68
+ "distanceUnit" to "metric",
69
+ "language" to "en"
70
+ )
71
+ )
72
+ }
73
+
74
+ View(MapboxNavigationView::class) {
75
+ Events(
76
+ "onLocationChange",
77
+ "onRouteProgressChange",
78
+ "onBannerInstruction",
79
+ "onArrive",
80
+ "onCancelNavigation",
81
+ "onError"
82
+ )
83
+
84
+ Prop("startOrigin") { view: MapboxNavigationView, origin: Map<String, Double> ->
85
+ view.setStartOrigin(origin)
86
+ }
87
+
88
+ Prop("destination") { view: MapboxNavigationView, destination: Map<String, Any> ->
89
+ view.setDestination(destination)
90
+ }
91
+
92
+ Prop("waypoints") { view: MapboxNavigationView, waypoints: List<Map<String, Any>>? ->
93
+ view.setWaypoints(waypoints)
94
+ }
95
+
96
+ Prop("shouldSimulateRoute") { view: MapboxNavigationView, simulate: Boolean ->
97
+ view.setShouldSimulateRoute(simulate)
98
+ }
99
+
100
+ Prop("showCancelButton") { view: MapboxNavigationView, show: Boolean ->
101
+ view.setShowCancelButton(show)
102
+ }
103
+
104
+ Prop("mute") { view: MapboxNavigationView, mute: Boolean ->
105
+ view.setMute(mute)
106
+ }
107
+
108
+ Prop("voiceVolume") { view: MapboxNavigationView, volume: Double ->
109
+ view.setVoiceVolume(volume)
110
+ }
111
+
112
+ Prop("cameraPitch") { view: MapboxNavigationView, pitch: Double ->
113
+ view.setCameraPitch(pitch)
114
+ }
115
+
116
+ Prop("cameraZoom") { view: MapboxNavigationView, zoom: Double ->
117
+ view.setCameraZoom(zoom)
118
+ }
119
+
120
+ Prop("cameraMode") { view: MapboxNavigationView, mode: String ->
121
+ view.setCameraMode(mode)
122
+ }
123
+
124
+ Prop("mapStyleUri") { view: MapboxNavigationView, styleUri: String ->
125
+ view.setMapStyleUri(styleUri)
126
+ }
127
+
128
+ Prop("mapStyleUriDay") { view: MapboxNavigationView, styleUri: String ->
129
+ view.setMapStyleUriDay(styleUri)
130
+ }
131
+
132
+ Prop("mapStyleUriNight") { view: MapboxNavigationView, styleUri: String ->
133
+ view.setMapStyleUriNight(styleUri)
134
+ }
135
+
136
+ Prop("uiTheme") { view: MapboxNavigationView, theme: String ->
137
+ view.setUiTheme(theme)
138
+ }
139
+
140
+ Prop("routeAlternatives") { view: MapboxNavigationView, routeAlternatives: Boolean ->
141
+ view.setRouteAlternatives(routeAlternatives)
142
+ }
143
+
144
+ Prop("showsSpeedLimits") { view: MapboxNavigationView, showsSpeedLimits: Boolean ->
145
+ view.setShowsSpeedLimits(showsSpeedLimits)
146
+ }
147
+
148
+ Prop("showsWayNameLabel") { view: MapboxNavigationView, showsWayNameLabel: Boolean ->
149
+ view.setShowsWayNameLabel(showsWayNameLabel)
150
+ }
151
+
152
+ Prop("showsTripProgress") { view: MapboxNavigationView, showsTripProgress: Boolean ->
153
+ view.setShowsTripProgress(showsTripProgress)
154
+ }
155
+
156
+ Prop("showsManeuverView") { view: MapboxNavigationView, showsManeuverView: Boolean ->
157
+ view.setShowsManeuverView(showsManeuverView)
158
+ }
159
+
160
+ Prop("showsActionButtons") { view: MapboxNavigationView, showsActionButtons: Boolean ->
161
+ view.setShowsActionButtons(showsActionButtons)
162
+ }
163
+
164
+ Prop("distanceUnit") { view: MapboxNavigationView, unit: String ->
165
+ view.setDistanceUnit(unit)
166
+ }
167
+
168
+ Prop("language") { view: MapboxNavigationView, language: String ->
169
+ view.setLanguage(language)
170
+ }
171
+ }
172
+ }
173
+
174
+ private fun startNavigation(options: Map<String, Any?>, promise: Promise) {
175
+ val activity = appContext.currentActivity
176
+ if (activity == null) {
177
+ promise.reject("NO_ACTIVITY", "No current activity", null)
178
+ return
179
+ }
180
+
181
+ val origin = options["startOrigin"] as? Map<*, *>
182
+ val destination = options["destination"] as? Map<*, *>
183
+
184
+ val originLat = (origin?.get("latitude") as? Number)?.toDouble()
185
+ val originLng = (origin?.get("longitude") as? Number)?.toDouble()
186
+ val destLat = (destination?.get("latitude") as? Number)?.toDouble()
187
+ val destLng = (destination?.get("longitude") as? Number)?.toDouble()
188
+
189
+ if (destLat == null || destLng == null) {
190
+ promise.reject("INVALID_COORDINATES", "Missing or invalid coordinates", null)
191
+ return
192
+ }
193
+
194
+ val shouldSimulate = (options["shouldSimulateRoute"] as? Boolean) ?: false
195
+ val mute = (options["mute"] as? Boolean) ?: false
196
+ val cameraPitch = (options["cameraPitch"] as? Number)?.toDouble()
197
+ val cameraZoom = (options["cameraZoom"] as? Number)?.toDouble()
198
+ val cameraMode = (options["cameraMode"] as? String) ?: "following"
199
+ val mapStyleUri = (options["mapStyleUri"] as? String) ?: ""
200
+ val mapStyleUriDay = (options["mapStyleUriDay"] as? String) ?: ""
201
+ val mapStyleUriNight = (options["mapStyleUriNight"] as? String) ?: ""
202
+ val uiTheme = (options["uiTheme"] as? String) ?: "system"
203
+ val routeAlternatives = (options["routeAlternatives"] as? Boolean) ?: false
204
+ val showsSpeedLimits = (options["showsSpeedLimits"] as? Boolean) ?: true
205
+ val showsWayNameLabel = (options["showsWayNameLabel"] as? Boolean) ?: true
206
+ val showsTripProgress = (options["showsTripProgress"] as? Boolean) ?: true
207
+ val showsManeuverView = (options["showsManeuverView"] as? Boolean) ?: true
208
+ val showsActionButtons = (options["showsActionButtons"] as? Boolean) ?: true
209
+ val waypoints = parseCoordinatesList(options["waypoints"] as? List<*>)
210
+
211
+ activity.runOnUiThread {
212
+ val accessToken = try {
213
+ getMapboxAccessToken(activity.packageName)
214
+ } catch (e: IllegalStateException) {
215
+ promise.reject("MISSING_ACCESS_TOKEN", e.message ?: "Missing mapbox_access_token", e)
216
+ return@runOnUiThread
217
+ }
218
+
219
+ val intent = Intent(activity, MapboxNavigationActivity::class.java).apply {
220
+ putExtra("accessToken", accessToken)
221
+ if (originLat != null && originLng != null) {
222
+ putExtra("originLat", originLat)
223
+ putExtra("originLng", originLng)
224
+ }
225
+ putExtra("destLat", destLat)
226
+ putExtra("destLng", destLng)
227
+ putExtra("shouldSimulate", shouldSimulate)
228
+ putExtra("mute", mute)
229
+ putExtra("cameraPitch", cameraPitch)
230
+ putExtra("cameraZoom", cameraZoom)
231
+ putExtra("cameraMode", cameraMode)
232
+ putExtra("mapStyleUri", mapStyleUri)
233
+ putExtra("mapStyleUriDay", mapStyleUriDay)
234
+ putExtra("mapStyleUriNight", mapStyleUriNight)
235
+ putExtra("uiTheme", uiTheme)
236
+ putExtra("routeAlternatives", routeAlternatives)
237
+ putExtra("showsSpeedLimits", showsSpeedLimits)
238
+ putExtra("showsWayNameLabel", showsWayNameLabel)
239
+ putExtra("showsTripProgress", showsTripProgress)
240
+ putExtra("showsManeuverView", showsManeuverView)
241
+ putExtra("showsActionButtons", showsActionButtons)
242
+ if (waypoints.isNotEmpty()) {
243
+ putExtra("waypointLats", waypoints.map { it.first }.toDoubleArray())
244
+ putExtra("waypointLngs", waypoints.map { it.second }.toDoubleArray())
245
+ }
246
+ }
247
+
248
+ activity.startActivity(intent)
249
+ isNavigating = true
250
+ promise.resolve(null)
251
+ }
252
+ }
253
+
254
+ private fun getMapboxAccessToken(packageName: String): String {
255
+ val context = appContext.reactContext ?: throw IllegalStateException("Missing React context")
256
+ val resourceId = context.resources.getIdentifier(
257
+ "mapbox_access_token",
258
+ "string",
259
+ packageName
260
+ )
261
+
262
+ if (resourceId == 0) {
263
+ throw IllegalStateException("Missing string resource: mapbox_access_token")
264
+ }
265
+
266
+ val token = context.getString(resourceId).trim()
267
+ if (token.isEmpty()) {
268
+ throw IllegalStateException("mapbox_access_token is empty")
269
+ }
270
+
271
+ return token
272
+ }
273
+
274
+ private fun stopNavigation(promise: Promise) {
275
+ val activity = appContext.currentActivity
276
+ activity?.finish()
277
+ isNavigating = false
278
+ promise.resolve(null)
279
+ }
280
+
281
+ private fun parseCoordinatesList(value: List<*>?): List<Pair<Double, Double>> {
282
+ if (value.isNullOrEmpty()) {
283
+ return emptyList()
284
+ }
285
+
286
+ return value.mapNotNull { item ->
287
+ val map = item as? Map<*, *> ?: return@mapNotNull null
288
+ val latitude = (map["latitude"] as? Number)?.toDouble() ?: return@mapNotNull null
289
+ val longitude = (map["longitude"] as? Number)?.toDouble() ?: return@mapNotNull null
290
+ if (latitude < -90.0 || latitude > 90.0 || longitude < -180.0 || longitude > 180.0) {
291
+ return@mapNotNull null
292
+ }
293
+ latitude to longitude
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,143 @@
1
+ package expo.modules.mapboxnavigation
2
+
3
+ import android.content.Context
4
+ import android.view.Gravity
5
+ import android.widget.FrameLayout
6
+ import android.widget.TextView
7
+ import expo.modules.kotlin.AppContext
8
+ import expo.modules.kotlin.viewevent.EventDispatcher
9
+ import expo.modules.kotlin.views.ExpoView
10
+
11
+ class MapboxNavigationView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
12
+ private var startOrigin: Map<String, Double>? = null
13
+ private var destination: Map<String, Any>? = null
14
+ private var waypoints: List<Map<String, Any>>? = null
15
+ private var shouldSimulateRoute = false
16
+ private var showCancelButton = true
17
+ private var mute = false
18
+ private var voiceVolume = 1.0
19
+ private var cameraPitch = 0.0
20
+ private var cameraZoom = 14.0
21
+ private var cameraMode = "following"
22
+ private var mapStyleUri = ""
23
+ private var mapStyleUriDay = ""
24
+ private var mapStyleUriNight = ""
25
+ private var uiTheme = "system"
26
+ private var routeAlternatives = false
27
+ private var showsSpeedLimits = true
28
+ private var showsWayNameLabel = true
29
+ private var showsTripProgress = true
30
+ private var showsManeuverView = true
31
+ private var showsActionButtons = true
32
+ private var distanceUnit = "metric"
33
+ private var language = "en"
34
+
35
+ val onLocationChange by EventDispatcher()
36
+ val onRouteProgressChange by EventDispatcher()
37
+ val onBannerInstruction by EventDispatcher()
38
+ val onArrive by EventDispatcher()
39
+ val onCancelNavigation by EventDispatcher()
40
+ val onError by EventDispatcher()
41
+
42
+ init {
43
+ val label = TextView(context).apply {
44
+ text = "Mapbox native navigation view is initializing..."
45
+ gravity = Gravity.CENTER
46
+ }
47
+ addView(
48
+ label,
49
+ FrameLayout.LayoutParams(
50
+ FrameLayout.LayoutParams.MATCH_PARENT,
51
+ FrameLayout.LayoutParams.MATCH_PARENT
52
+ )
53
+ )
54
+ }
55
+
56
+ fun setStartOrigin(origin: Map<String, Double>) {
57
+ startOrigin = origin
58
+ }
59
+
60
+ fun setDestination(dest: Map<String, Any>) {
61
+ destination = dest
62
+ }
63
+
64
+ fun setWaypoints(wps: List<Map<String, Any>>?) {
65
+ waypoints = wps
66
+ }
67
+
68
+ fun setShouldSimulateRoute(simulate: Boolean) {
69
+ shouldSimulateRoute = simulate
70
+ }
71
+
72
+ fun setShowCancelButton(show: Boolean) {
73
+ showCancelButton = show
74
+ }
75
+
76
+ fun setMute(muted: Boolean) {
77
+ mute = muted
78
+ }
79
+
80
+ fun setVoiceVolume(volume: Double) {
81
+ voiceVolume = volume
82
+ }
83
+
84
+ fun setCameraPitch(pitch: Double) {
85
+ cameraPitch = pitch
86
+ }
87
+
88
+ fun setCameraZoom(zoom: Double) {
89
+ cameraZoom = zoom
90
+ }
91
+
92
+ fun setCameraMode(mode: String) {
93
+ cameraMode = mode
94
+ }
95
+
96
+ fun setMapStyleUri(styleUri: String) {
97
+ mapStyleUri = styleUri
98
+ }
99
+
100
+ fun setMapStyleUriDay(styleUri: String) {
101
+ mapStyleUriDay = styleUri
102
+ }
103
+
104
+ fun setMapStyleUriNight(styleUri: String) {
105
+ mapStyleUriNight = styleUri
106
+ }
107
+
108
+ fun setUiTheme(theme: String) {
109
+ uiTheme = theme
110
+ }
111
+
112
+ fun setRouteAlternatives(enabled: Boolean) {
113
+ routeAlternatives = enabled
114
+ }
115
+
116
+ fun setShowsSpeedLimits(enabled: Boolean) {
117
+ showsSpeedLimits = enabled
118
+ }
119
+
120
+ fun setShowsWayNameLabel(enabled: Boolean) {
121
+ showsWayNameLabel = enabled
122
+ }
123
+
124
+ fun setShowsTripProgress(enabled: Boolean) {
125
+ showsTripProgress = enabled
126
+ }
127
+
128
+ fun setShowsManeuverView(enabled: Boolean) {
129
+ showsManeuverView = enabled
130
+ }
131
+
132
+ fun setShowsActionButtons(enabled: Boolean) {
133
+ showsActionButtons = enabled
134
+ }
135
+
136
+ fun setDistanceUnit(unit: String) {
137
+ distanceUnit = unit
138
+ }
139
+
140
+ fun setLanguage(lang: String) {
141
+ language = lang
142
+ }
143
+ }
package/app.plugin.js ADDED
@@ -0,0 +1,154 @@
1
+ const {
2
+ withProjectBuildGradle,
3
+ withAppBuildGradle,
4
+ withAndroidManifest,
5
+ withInfoPlist,
6
+ createRunOncePlugin,
7
+ } = require("@expo/config-plugins");
8
+
9
+ const MAPBOX_REPO_BLOCK = ` maven {
10
+ url 'https://api.mapbox.com/downloads/v2/releases/maven'
11
+ authentication {
12
+ basic(BasicAuthentication)
13
+ }
14
+ credentials {
15
+ username = "mapbox"
16
+ password = mapboxDownloadsToken
17
+ }
18
+ }`;
19
+
20
+ const MAPBOX_TOKEN_LINES = ` def mapboxPublicToken = project.findProperty("MAPBOX_PUBLIC_TOKEN") ?: System.getenv("EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN") ?: ""
21
+ resValue "string", "mapbox_access_token", mapboxPublicToken`;
22
+
23
+ const REQUIRED_ANDROID_PERMISSIONS = [
24
+ "android.permission.ACCESS_COARSE_LOCATION",
25
+ "android.permission.ACCESS_FINE_LOCATION",
26
+ "android.permission.ACCESS_BACKGROUND_LOCATION",
27
+ "android.permission.FOREGROUND_SERVICE",
28
+ "android.permission.FOREGROUND_SERVICE_LOCATION",
29
+ "android.permission.POST_NOTIFICATIONS",
30
+ ];
31
+
32
+ const DEFAULT_IOS_LOCATION_USAGE =
33
+ "Allow $(PRODUCT_NAME) to access your location for turn-by-turn navigation.";
34
+
35
+ function ensureAndroidPermissions(androidManifest) {
36
+ const manifest = androidManifest.manifest;
37
+ if (!manifest["uses-permission"]) {
38
+ manifest["uses-permission"] = [];
39
+ }
40
+
41
+ const existingPermissions = new Set(
42
+ manifest["uses-permission"]
43
+ .map((entry) => entry?.$?.["android:name"])
44
+ .filter(Boolean)
45
+ );
46
+
47
+ REQUIRED_ANDROID_PERMISSIONS.forEach((permission) => {
48
+ if (!existingPermissions.has(permission)) {
49
+ manifest["uses-permission"].push({
50
+ $: {
51
+ "android:name": permission,
52
+ },
53
+ });
54
+ }
55
+ });
56
+
57
+ return androidManifest;
58
+ }
59
+
60
+ function ensureProjectBuildGradle(src) {
61
+ let out = src;
62
+
63
+ if (!out.includes("def mapboxDownloadsToken =")) {
64
+ out =
65
+ `def mapboxDownloadsToken = (findProperty("MAPBOX_DOWNLOADS_TOKEN") ?: System.getenv("MAPBOX_DOWNLOADS_TOKEN") ?: "")\n` +
66
+ ` .toString()\n` +
67
+ ` .replace('"', '')\n` +
68
+ ` .trim()\n\n` +
69
+ out;
70
+ }
71
+
72
+ if (!out.includes("https://api.mapbox.com/downloads/v2/releases/maven")) {
73
+ out = out.replace(
74
+ /allprojects\s*\{\s*repositories\s*\{\s*google\(\)\s*mavenCentral\(\)/m,
75
+ (match) => `${match}\n${MAPBOX_REPO_BLOCK}`
76
+ );
77
+ }
78
+
79
+ return out;
80
+ }
81
+
82
+ function ensureAppBuildGradle(src) {
83
+ if (src.includes('resValue "string", "mapbox_access_token"')) {
84
+ return src;
85
+ }
86
+
87
+ return src.replace(
88
+ /(versionName\s+"[^"]+"\s*\n)/m,
89
+ `$1${MAPBOX_TOKEN_LINES}\n`
90
+ );
91
+ }
92
+
93
+ function withMapboxNavigationAndroid(config) {
94
+ config = withProjectBuildGradle(config, (config) => {
95
+ config.modResults.contents = ensureProjectBuildGradle(
96
+ config.modResults.contents
97
+ );
98
+ return config;
99
+ });
100
+
101
+ config = withAppBuildGradle(config, (config) => {
102
+ config.modResults.contents = ensureAppBuildGradle(config.modResults.contents);
103
+ return config;
104
+ });
105
+
106
+ config = withAndroidManifest(config, (config) => {
107
+ config.modResults = ensureAndroidPermissions(config.modResults);
108
+ return config;
109
+ });
110
+
111
+ return config;
112
+ }
113
+
114
+ function withMapboxNavigationIos(config) {
115
+ return withInfoPlist(config, (config) => {
116
+ const infoPlist = config.modResults;
117
+ const mapboxPublicToken =
118
+ process.env.EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN ||
119
+ process.env.MAPBOX_PUBLIC_TOKEN ||
120
+ "";
121
+
122
+ if (!infoPlist.MBXAccessToken && mapboxPublicToken) {
123
+ infoPlist.MBXAccessToken = mapboxPublicToken;
124
+ }
125
+
126
+ if (!infoPlist.NSLocationWhenInUseUsageDescription) {
127
+ infoPlist.NSLocationWhenInUseUsageDescription = DEFAULT_IOS_LOCATION_USAGE;
128
+ }
129
+
130
+ if (!infoPlist.NSLocationAlwaysAndWhenInUseUsageDescription) {
131
+ infoPlist.NSLocationAlwaysAndWhenInUseUsageDescription = DEFAULT_IOS_LOCATION_USAGE;
132
+ }
133
+
134
+ const existingModes = Array.isArray(infoPlist.UIBackgroundModes)
135
+ ? infoPlist.UIBackgroundModes
136
+ : [];
137
+ const mergedModes = new Set([...existingModes, "location", "audio"]);
138
+ infoPlist.UIBackgroundModes = Array.from(mergedModes);
139
+
140
+ return config;
141
+ });
142
+ }
143
+
144
+ const withMapboxNavigation = (config) => {
145
+ config = withMapboxNavigationAndroid(config);
146
+ config = withMapboxNavigationIos(config);
147
+ return config;
148
+ };
149
+
150
+ module.exports = createRunOncePlugin(
151
+ withMapboxNavigation,
152
+ "react-native-mapbox-navigation-plugin",
153
+ "1.1.0"
154
+ );
@@ -0,0 +1,97 @@
1
+ # Publishing & CI/CD Guide
2
+
3
+ ## 1. Create the package repository
4
+
5
+ Recommended repo name: `react-native-mapbox-navigation`.
6
+
7
+ Use this module as the repository root (do not keep it under `modules/` after copying).
8
+
9
+ Required top-level items:
10
+ - `src/`, `android/`, `ios/`
11
+ - `app.plugin.js`, `expo-module.config.json`, `package.json`
12
+ - `README.md`, `QUICKSTART.md`, `CHANGELOG.md`, `docs/`
13
+ - `.github/workflows/`
14
+ - `website/` (GitHub Pages)
15
+
16
+ ## 2. Prepare GitHub repo
17
+
18
+ 1. Create GitHub repo `react-native-mapbox-navigation`.
19
+ 2. Copy this folder contents into that repo root.
20
+ 3. Push initial commit:
21
+
22
+ ```bash
23
+ git init
24
+ git add .
25
+ git commit -m "chore: initial release-ready package"
26
+ git branch -M main
27
+ git remote add origin git@github.com:<your-org-or-user>/react-native-mapbox-navigation.git
28
+ git push -u origin main
29
+ ```
30
+
31
+ ## 3. Enable CI
32
+
33
+ This package includes workflow templates you can use directly:
34
+ - `.github/workflows/ci.yml`
35
+ - `.github/workflows/deploy-pages.yml`
36
+
37
+ `ci.yml` validates:
38
+ - TypeScript check (`npx tsc --noEmit`)
39
+ - Android module compile (`./gradlew ...compileDebugKotlin`)
40
+ - npm tarball dry-run (`npm pack --dry-run`)
41
+
42
+ ## 4. Enable GitHub Pages
43
+
44
+ 1. In GitHub repo: `Settings -> Pages`.
45
+ 2. Source: `GitHub Actions`.
46
+ 3. Keep `website/` as docs source for deployed site.
47
+ 4. Push to `main`; workflow will publish automatically.
48
+
49
+ ## 5. Local release verification
50
+
51
+ From package root:
52
+
53
+ ```bash
54
+ npm run verify
55
+ ```
56
+
57
+ This runs:
58
+ 1. TypeScript check
59
+ 2. Android compile check
60
+ 3. `npm pack --dry-run`
61
+
62
+ ## 6. Test before npm publish
63
+
64
+ ### Pack locally
65
+
66
+ ```bash
67
+ npm pack --cache /tmp/npm-cache-react-native-mapbox-navigation
68
+ ```
69
+
70
+ ### Install in a clean test app
71
+
72
+ ```bash
73
+ npm install /absolute/path/to/react-native-mapbox-navigation-<version>.tgz
74
+ ```
75
+
76
+ Then validate both platforms:
77
+
78
+ ```bash
79
+ npx expo prebuild --clean
80
+ npx expo run:android
81
+ npx expo run:ios
82
+ ```
83
+
84
+ ## 7. Publish steps (after testing)
85
+
86
+ ```bash
87
+ npm version patch # or minor / major
88
+ git push --follow-tags
89
+ npm publish --access public
90
+ ```
91
+
92
+ ## 8. Recommended production setup
93
+
94
+ - Protect `main` and require PR checks.
95
+ - Enable npm trusted publishing from GitHub Actions.
96
+ - Create GitHub Releases for each version tag.
97
+ - Keep `CHANGELOG.md` updated per release.