react-native-navigation-mode 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jairaj Jangle
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NavigationMode"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "11.0" }
14
+ s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm}"
17
+
18
+ install_modules_dependencies(s)
19
+ end
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # react-native-navigation-mode
2
+
3
+ 🧭 Detect Android navigation mode (3-button, 2-button, or gesture navigation) with native precision using Turbo modules.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/react-native-navigation-mode)](https://badge.fury.io/js/react-native-navigation-mode) [![License](https://img.shields.io/github/license/JairajJangle/react-native-navigation-mode)](https://github.com/JairajJangle/react-native-navigation-mode/blob/main/LICENSE) [![Workflow Status](https://github.com/JairajJangle/react-native-navigation-mode/actions/workflows/ci.yml/badge.svg)](https://github.com/JairajJangle/react-native-navigation-mode/actions/workflows/ci.yml) ![Android](https://img.shields.io/badge/-Android-555555?logo=android&logoColor=3DDC84) ![iOS](https://img.shields.io/badge/-iOS-555555?logo=apple&logoColor=white) [![GitHub issues](https://img.shields.io/github/issues/JairajJangle/react-native-navigation-mode)](https://github.com/JairajJangle/react-native-navigation-mode/issues?q=is%3Aopen+is%3Aissue) ![TS](https://img.shields.io/badge/TypeScript-strict_💪-blue) ![Turbo Module](https://img.shields.io/badge/Turbo%20Module-⚡-orange) ![Expo Compatible](https://img.shields.io/badge/Expo-555555?style=flat&logo=expo&logoColor=white) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-native-navigation-mode)
6
+
7
+ <table align="center">
8
+ <tr>
9
+ <td align="center"><img src=".github/assets/buttons.png" alt="Visibility Sensor demo" height="600"></td>
10
+ <td align="center"><img src=".github/assets/gesture.png" alt="Visibility Sensor demo" height="600"></td>
11
+ </tr>
12
+ </table>
13
+
14
+
15
+ <div align="center">
16
+ <table>
17
+ <tr>
18
+ <td align="center">
19
+ <img src="https://img.shields.io/badge/3--Button-Navigation-blue?style=for-the-badge" alt="3-Button Navigation" />
20
+ <br />
21
+ <small>Traditional Android navigation</small>
22
+ </td>
23
+ <td align="center">
24
+ <img src="https://img.shields.io/badge/2--Button-Navigation-green?style=for-the-badge" alt="2-Button Navigation" />
25
+ <br />
26
+ <small>Home + Back buttons</small>
27
+ </td>
28
+ <td align="center">
29
+ <img src="https://img.shields.io/badge/Gesture-Navigation-purple?style=for-the-badge" alt="Gesture Navigation" />
30
+ <br />
31
+ <small>Swipe-based navigation</small>
32
+ </td>
33
+ </tr>
34
+ </table>
35
+ </div>
36
+
37
+ ## ✨ Features
38
+
39
+ - 🎯 **Direct Native Detection** - No hacky workarounds or dimension-based guessing
40
+ - ⚡ **Turbo Module** - Built with the latest React Native architecture
41
+ - 🔄 **Real-time Detection** - Accurate navigation mode identification
42
+ - 📱 **Cross Platform** - Android detection + iOS compatibility
43
+ - 🎣 **React Hooks** - Easy integration with `useNavigationMode()`
44
+ - 🚀 **Expo Compatible** - Works with Expo managed workflow (dev builds)
45
+ - 📦 **Zero Dependencies** - Lightweight and performant
46
+ - 🛡️ **TypeScript** - Full type safety out of the box
47
+
48
+ ## Installation
49
+
50
+ Using yarn:
51
+
52
+ ```sh
53
+ yarn add react-native-navigation-mode
54
+ ```
55
+
56
+ Using npm:
57
+
58
+ ```sh
59
+ npm install react-native-navigation-mode
60
+ ```
61
+
62
+ ### For Expo Managed Workflow
63
+
64
+ ```sh
65
+ npx expo install react-native-navigation-mode
66
+ ```
67
+
68
+ Add the plugin to your `app.config.js`:
69
+
70
+ ```javascript
71
+ export default {
72
+ expo: {
73
+ plugins: ["react-native-navigation-mode"],
74
+ },
75
+ };
76
+ ```
77
+
78
+ Create a development build:
79
+
80
+ ```sh
81
+ npx expo run:android
82
+ ```
83
+
84
+ > ⚠️ **Note:** This library requires a development build and will NOT work in Expo Go due to native code requirements.
85
+
86
+ ### For React Native CLI
87
+
88
+ Auto-linking handles setup automatically for React Native 0.60+.
89
+
90
+ ## Usage
91
+
92
+ ### Quick Check
93
+
94
+ ```typescript
95
+ import { isGestureNavigation } from 'react-native-navigation-mode';
96
+
97
+ // Simple boolean check
98
+ const isGesture = await isGestureNavigation();
99
+ console.log('Gesture navigation:', isGesture); // true/false
100
+ ```
101
+
102
+ ### Detailed Information
103
+
104
+ ```typescript
105
+ import { getNavigationMode } from 'react-native-navigation-mode';
106
+
107
+ // Get comprehensive navigation info
108
+ const navInfo = await getNavigationMode();
109
+ console.log('Navigation type:', navInfo.type); // '3_button', '2_button', 'gesture', or 'unknown'
110
+ console.log('SDK version:', navInfo.sdkVersion);
111
+ console.log('Device model:', navInfo.deviceModel);
112
+ ```
113
+
114
+ ### React Hook (Recommended)
115
+
116
+ ```typescript
117
+ import React from 'react';
118
+ import { View, Text } from 'react-native';
119
+ import { useNavigationMode } from 'react-native-navigation-mode';
120
+
121
+ export default function NavigationInfo() {
122
+ const { navigationMode, loading, error } = useNavigationMode();
123
+
124
+ if (loading) return <Text>Detecting navigation mode...</Text>;
125
+ if (error) return <Text>Error: {error.message}</Text>;
126
+
127
+ return (
128
+ <View>
129
+ <Text>Navigation Type: {navigationMode?.type}</Text>
130
+ <Text>Gesture Navigation: {navigationMode?.isGestureNavigation ? 'Yes' : 'No'}</Text>
131
+ <Text>Has Navigation Bar: {navigationMode?.hasNavigationBar ? 'Yes' : 'No'}</Text>
132
+ <Text>Android SDK: {navigationMode?.sdkVersion}</Text>
133
+ <Text>Device: {navigationMode?.deviceModel}</Text>
134
+ </View>
135
+ );
136
+ }
137
+ ```
138
+
139
+ ### Conditional UI Rendering
140
+
141
+ ```typescript
142
+ import React from 'react';
143
+ import { View } from 'react-native';
144
+ import { useNavigationMode } from 'react-native-navigation-mode';
145
+
146
+ export default function AdaptiveUI() {
147
+ const { navigationMode } = useNavigationMode();
148
+
149
+ return (
150
+ <View
151
+ style={{
152
+ paddingBottom: navigationMode?.isGestureNavigation ? 34 : 48 // Adjust for gesture nav
153
+ }}
154
+ >
155
+ {/* Your content */}
156
+ </View>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ## API Reference
162
+
163
+ ### Functions
164
+
165
+ #### `getNavigationMode(): Promise<NavigationModeInfo>`
166
+
167
+ Returns comprehensive navigation mode information.
168
+
169
+ #### `isGestureNavigation(): Promise<boolean>`
170
+
171
+ Quick check if device is using gesture navigation.
172
+
173
+ ### Hooks
174
+
175
+ #### `useNavigationMode(): { navigationMode, loading, error }`
176
+
177
+ React hook for navigation mode detection with loading and error states.
178
+
179
+ ### Types
180
+
181
+ #### `NavigationModeInfo`
182
+
183
+ | Property | Type | Description |
184
+ | ------------------- | ---------------------------------------------- | ------------------------------------------------ |
185
+ | type | `'3_button' \| '2_button' \| 'gesture' \| 'unknown'` | Navigation mode type |
186
+ | isGestureNavigation | `boolean` | Whether gesture navigation is active |
187
+ | hasNavigationBar | `boolean` | Whether device has a navigation bar |
188
+ | sdkVersion | `number` | Android SDK version (iOS: iOS version) |
189
+ | deviceModel | `string` | Device model name |
190
+ | interactionMode | `number \| undefined` | Raw Android interaction mode (0, 1, 2, or -1) |
191
+
192
+ ## Platform Support
193
+
194
+ | Platform | Support | Notes |
195
+ |----------|---------|-------|
196
+ | Android | ✅ Full | Detects all navigation modes via native Android APIs |
197
+ | iOS | ✅ Compatible | Always returns `gesture` (iOS uses gesture navigation) |
198
+
199
+ ### Android Compatibility
200
+
201
+ - **API 21+** - Basic navigation bar detection
202
+ - **API 29+** - Full navigation mode detection (`config_navBarInteractionMode`)
203
+ - **All versions** - Fallback detection methods included
204
+
205
+ ## How It Works
206
+
207
+ The library uses multiple detection methods for maximum accuracy:
208
+
209
+ 1. **`config_navBarInteractionMode`** - Official Android configuration (API 29+)
210
+ 2. **Settings Provider** - Checks `navigation_mode` system setting
211
+ 3. **Navigation Bar Detection** - Validates navigation bar presence
212
+ 4. **Hardware Key Detection** - Fallback for older devices
213
+
214
+ ### Navigation Mode Values
215
+
216
+ | Android Mode | Type | Description |
217
+ |--------------|------|-------------|
218
+ | 0 | `3_button` | Traditional Android navigation (Back, Home, Recent) |
219
+ | 1 | `2_button` | Two-button navigation (Back, Home) |
220
+ | 2 | `gesture` | Full gesture navigation |
221
+ | -1 | `unknown` | Could not determine navigation mode |
222
+
223
+ ## Notes
224
+
225
+ 1. 🍎 **iOS Behavior** - iOS always returns `isGestureNavigation: true` since iOS doesn't have 3-button navigation
226
+ 2. 📱 **Expo Go** - Not supported due to native module requirements. Use development builds instead
227
+ 3. ⚡ **Performance** - Turbo module ensures minimal performance impact
228
+ 4. 🔄 **Real-time** - Navigation mode is detected at call time, reflecting current device settings
229
+
230
+ ## Troubleshooting
231
+
232
+ ### Common Issues
233
+
234
+ **"TurboModuleRegistry.getEnforcing(...) is not a function"**
235
+ - Ensure you're using React Native 0.68+ with new architecture enabled
236
+ - For older RN versions, the module will fallback gracefully
237
+
238
+ **Expo Go not working**
239
+ - This is expected. Create a development build with `npx expo run:android`
240
+
241
+ **Always returns 'unknown' on Android**
242
+ - Check if your device/emulator supports the navigation mode APIs
243
+ - Some custom ROMs may not expose standard Android navigation settings
244
+
245
+ ## Contributing
246
+
247
+ See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
248
+
249
+ ## License
250
+
251
+ MIT
252
+
253
+ ## Support the project
254
+
255
+ <p align="center" valign="center">
256
+ <a href="https://liberapay.com/FutureJJ/donate">
257
+ <img src="https://liberapay.com/assets/widgets/donate.svg" alt="LiberPay_Donation_Button" height="50" >
258
+ </a>
259
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
260
+ <a href=".github/assets/Jairaj_Jangle_Google_Pay_UPI_QR_Code.jpg">
261
+ <img src=".github/assets/upi.png" alt="UPI_Donation_Button" height="50" >
262
+ </a>
263
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
264
+ <a href="https://www.paypal.com/paypalme/jairajjangle001/usd">
265
+ <img src=".github/assets/paypal_donate.png" alt="Paypal_Donation_Button" height="50" >
266
+ </a>
267
+ </p>
268
+
269
+ ## ❤️ Thanks to
270
+
271
+ - Module built using [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
272
+ - Inspiration from various Android navigation detection attempts in the community
273
+ - React Native team for Turbo Modules architecture
274
+ - Expo team for config plugin support
275
+ - Readme is edited using [Typora](https://typora.io/)
276
+
277
+ ---
278
+
279
+ Made with ❤️ by [JairajJangle](https://github.com/JairajJangle)
@@ -0,0 +1,83 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NavigationMode_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+ apply plugin: "com.facebook.react"
23
+
24
+ def getExtOrIntegerDefault(name) {
25
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NavigationMode_" + name]).toInteger()
26
+ }
27
+
28
+ android {
29
+ namespace "com.navigationmode"
30
+
31
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
32
+
33
+ defaultConfig {
34
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
36
+ }
37
+
38
+ buildFeatures {
39
+ buildConfig true
40
+ }
41
+
42
+ buildTypes {
43
+ release {
44
+ minifyEnabled false
45
+ }
46
+ }
47
+
48
+ lintOptions {
49
+ disable "GradleCompatible"
50
+ }
51
+
52
+ compileOptions {
53
+ sourceCompatibility JavaVersion.VERSION_1_8
54
+ targetCompatibility JavaVersion.VERSION_1_8
55
+ }
56
+
57
+ sourceSets {
58
+ main {
59
+ java.srcDirs += [
60
+ "generated/java",
61
+ "generated/jni"
62
+ ]
63
+ }
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ mavenCentral()
69
+ google()
70
+ }
71
+
72
+ def kotlin_version = getExtOrDefault("kotlinVersion")
73
+
74
+ dependencies {
75
+ implementation "com.facebook.react:react-android"
76
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
+ }
78
+
79
+ react {
80
+ jsRootDir = file("../src/")
81
+ libraryName = "NavigationMode"
82
+ codegenJavaPackageName = "com.navigationmode"
83
+ }
@@ -0,0 +1,5 @@
1
+ NavigationMode_kotlinVersion=2.0.21
2
+ NavigationMode_minSdkVersion=24
3
+ NavigationMode_targetSdkVersion=34
4
+ NavigationMode_compileSdkVersion=35
5
+ NavigationMode_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,117 @@
1
+ package com.navigationmode
2
+
3
+ import android.content.Context
4
+ import android.content.res.Resources
5
+ import android.os.Build
6
+ import android.provider.Settings
7
+ import android.view.ViewConfiguration
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.WritableMap
11
+ import com.facebook.react.bridge.Arguments
12
+ import com.facebook.react.module.annotations.ReactModule
13
+
14
+ @ReactModule(name = NavigationModeModule.NAME)
15
+ class NavigationModeModule(reactContext: ReactApplicationContext) :
16
+ NativeNavigationModeSpec(reactContext) {
17
+
18
+ companion object {
19
+ const val NAME = "NavigationMode"
20
+ }
21
+
22
+ override fun getName(): String = NAME
23
+
24
+ override fun getNavigationMode(promise: Promise) {
25
+ try {
26
+ val result = Arguments.createMap()
27
+ val context = reactApplicationContext
28
+
29
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
30
+ val navBarInteractionMode = getNavBarInteractionMode(context)
31
+ result.putInt("interactionMode", navBarInteractionMode)
32
+ result.putString("type", getNavigationTypeFromInteractionMode(navBarInteractionMode))
33
+ }
34
+
35
+ val gestureNavEnabled = isGestureNavigationEnabled(context)
36
+ result.putBoolean("isGestureNavigation", gestureNavEnabled)
37
+
38
+ val hasNavigationBar = hasNavigationBar(context)
39
+ result.putBoolean("hasNavigationBar", hasNavigationBar)
40
+
41
+ result.putInt("sdkVersion", Build.VERSION.SDK_INT)
42
+ result.putString("deviceModel", Build.MODEL)
43
+
44
+ promise.resolve(result)
45
+ } catch (e: Exception) {
46
+ promise.reject("NAVIGATION_MODE_ERROR", "Failed to get navigation mode: ${e.message}", e)
47
+ }
48
+ }
49
+
50
+ override fun isGestureNavigation(promise: Promise) {
51
+ try {
52
+ val context = reactApplicationContext
53
+
54
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
55
+ val navBarInteractionMode = getNavBarInteractionMode(context)
56
+ promise.resolve(navBarInteractionMode == 2)
57
+ } else {
58
+ val gestureEnabled = isGestureNavigationEnabled(context)
59
+ promise.resolve(gestureEnabled)
60
+ }
61
+ } catch (e: Exception) {
62
+ promise.reject("GESTURE_NAV_ERROR", "Failed to check gesture navigation: ${e.message}", e)
63
+ }
64
+ }
65
+
66
+ private fun getNavBarInteractionMode(context: Context): Int {
67
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
68
+ try {
69
+ val resources = context.resources
70
+ val resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android")
71
+ if (resourceId > 0) {
72
+ resources.getInteger(resourceId)
73
+ } else -1
74
+ } catch (e: Exception) {
75
+ -1
76
+ }
77
+ } else -1
78
+ }
79
+
80
+ private fun getNavigationTypeFromInteractionMode(mode: Int): String {
81
+ return when (mode) {
82
+ 0 -> "3_button"
83
+ 1 -> "2_button"
84
+ 2 -> "gesture"
85
+ else -> "unknown"
86
+ }
87
+ }
88
+
89
+ private fun isGestureNavigationEnabled(context: Context): Boolean {
90
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
91
+ try {
92
+ val navBarMode = Settings.Secure.getString(
93
+ context.contentResolver,
94
+ "navigation_mode"
95
+ )
96
+ "2" == navBarMode
97
+ } catch (e: Exception) {
98
+ false
99
+ }
100
+ } else false
101
+ }
102
+
103
+ private fun hasNavigationBar(context: Context): Boolean {
104
+ return try {
105
+ val resources = context.resources
106
+ val resourceId = resources.getIdentifier("config_showNavigationBar", "bool", "android")
107
+
108
+ if (resourceId > 0) {
109
+ resources.getBoolean(resourceId)
110
+ } else {
111
+ !ViewConfiguration.get(context).hasPermanentMenuKey()
112
+ }
113
+ } catch (e: Exception) {
114
+ true
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,34 @@
1
+ package com.navigationmode
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class NavigationModePackage : TurboReactPackage() {
10
+
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == NavigationModeModule.NAME) {
13
+ NavigationModeModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ mapOf(
22
+ NavigationModeModule.NAME to ReactModuleInfo(
23
+ NavigationModeModule.NAME,
24
+ NavigationModeModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ true, // hasConstants
28
+ false, // isCxxModule
29
+ true // isTurboModule
30
+ )
31
+ )
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,9 @@
1
+ #import <NavigationModeSpec/NavigationModeSpec.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @interface NavigationMode : NSObject <NativeNavigationModeSpec>
6
+
7
+ @end
8
+
9
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,31 @@
1
+ #import "NavigationMode.h"
2
+
3
+ @implementation NavigationMode
4
+
5
+ RCT_EXPORT_MODULE()
6
+
7
+ - (void)getNavigationMode:(RCTPromiseResolveBlock)resolve
8
+ reject:(RCTPromiseRejectBlock)reject {
9
+ // iOS always uses gesture navigation - handled in TypeScript
10
+ NSDictionary *result = @{
11
+ @"type": @"gesture",
12
+ @"isGestureNavigation": @YES,
13
+ @"hasNavigationBar": @NO,
14
+ @"sdkVersion": @([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion),
15
+ @"deviceModel": @"iOS"
16
+ };
17
+ resolve(result);
18
+ }
19
+
20
+ - (void)isGestureNavigation:(RCTPromiseResolveBlock)resolve
21
+ reject:(RCTPromiseRejectBlock)reject {
22
+ // iOS always uses gesture navigation - handled in TypeScript
23
+ resolve(@YES);
24
+ }
25
+
26
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
27
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
28
+ return std::make_shared<facebook::react::NativeNavigationModeSpecJSI>(params);
29
+ }
30
+
31
+ @end
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.getEnforcing('NavigationMode');
5
+ //# sourceMappingURL=NativeNavigationMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeNavigationMode.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAgBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,gBAAgB,CAAC","ignoreList":[]}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ import { Platform } from 'react-native';
4
+ import React from 'react';
5
+ import NavigationModeModule from "./NativeNavigationMode.js";
6
+ /**
7
+ * Get detailed navigation mode information
8
+ * Returns navigation type, interaction mode, and device info
9
+ */
10
+ export function getNavigationMode() {
11
+ if (Platform.OS === 'ios') {
12
+ // iOS always uses gesture navigation (no 3-button navigation exists)
13
+ return Promise.resolve({
14
+ type: 'gesture',
15
+ isGestureNavigation: true,
16
+ hasNavigationBar: false,
17
+ sdkVersion: parseInt(Platform.Version, 10),
18
+ deviceModel: 'iOS'
19
+ });
20
+ }
21
+ return NavigationModeModule.getNavigationMode();
22
+ }
23
+
24
+ /**
25
+ * Quick check if the device is using gesture-based navigation
26
+ * @returns Promise<boolean> - true if gesture navigation is active
27
+ */
28
+ export function isGestureNavigation() {
29
+ if (Platform.OS === 'ios') {
30
+ // iOS always uses gesture navigation
31
+ return Promise.resolve(true);
32
+ }
33
+ return NavigationModeModule.isGestureNavigation();
34
+ }
35
+
36
+ /**
37
+ * Hook for React components to get navigation mode
38
+ */
39
+ export function useNavigationMode() {
40
+ const [navigationMode, setNavigationMode] = React.useState(null);
41
+ const [loading, setLoading] = React.useState(true);
42
+ const [error, setError] = React.useState(null);
43
+ React.useEffect(() => {
44
+ let mounted = true;
45
+ async function fetchNavigationMode() {
46
+ try {
47
+ const mode = await getNavigationMode();
48
+ if (mounted) {
49
+ setNavigationMode(mode);
50
+ setError(null);
51
+ }
52
+ } catch (err) {
53
+ if (mounted) {
54
+ setError(err instanceof Error ? err : new Error('Unknown error'));
55
+ }
56
+ } finally {
57
+ if (mounted) {
58
+ setLoading(false);
59
+ }
60
+ }
61
+ }
62
+ fetchNavigationMode();
63
+ return () => {
64
+ mounted = false;
65
+ };
66
+ }, []);
67
+ return {
68
+ navigationMode,
69
+ loading,
70
+ error
71
+ };
72
+ }
73
+ export default {
74
+ getNavigationMode,
75
+ isGestureNavigation,
76
+ useNavigationMode
77
+ };
78
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["Platform","React","NavigationModeModule","getNavigationMode","OS","Promise","resolve","type","isGestureNavigation","hasNavigationBar","sdkVersion","parseInt","Version","deviceModel","useNavigationMode","navigationMode","setNavigationMode","useState","loading","setLoading","error","setError","useEffect","mounted","fetchNavigationMode","mode","err","Error"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,cAAc;AACvC,OAAOC,KAAK,MAAM,OAAO;AACzB,OAAOC,oBAAoB,MAEpB,2BAAwB;AAI/B;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAAA,EAAgC;EAC/D,IAAIH,QAAQ,CAACI,EAAE,KAAK,KAAK,EAAE;IACzB;IACA,OAAOC,OAAO,CAACC,OAAO,CAAC;MACrBC,IAAI,EAAE,SAAS;MACfC,mBAAmB,EAAE,IAAI;MACzBC,gBAAgB,EAAE,KAAK;MACvBC,UAAU,EAAEC,QAAQ,CAACX,QAAQ,CAACY,OAAO,EAAY,EAAE,CAAC;MACpDC,WAAW,EAAE;IACf,CAAC,CAAC;EACJ;EAEA,OAAOX,oBAAoB,CAACC,iBAAiB,CAAC,CAAC;AACjD;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASK,mBAAmBA,CAAA,EAAqB;EACtD,IAAIR,QAAQ,CAACI,EAAE,KAAK,KAAK,EAAE;IACzB;IACA,OAAOC,OAAO,CAACC,OAAO,CAAC,IAAI,CAAC;EAC9B;EAEA,OAAOJ,oBAAoB,CAACM,mBAAmB,CAAC,CAAC;AACnD;;AAEA;AACA;AACA;AACA,OAAO,SAASM,iBAAiBA,CAAA,EAAG;EAClC,MAAM,CAACC,cAAc,EAAEC,iBAAiB,CAAC,GACvCf,KAAK,CAACgB,QAAQ,CAA4B,IAAI,CAAC;EACjD,MAAM,CAACC,OAAO,EAAEC,UAAU,CAAC,GAAGlB,KAAK,CAACgB,QAAQ,CAAC,IAAI,CAAC;EAClD,MAAM,CAACG,KAAK,EAAEC,QAAQ,CAAC,GAAGpB,KAAK,CAACgB,QAAQ,CAAe,IAAI,CAAC;EAE5DhB,KAAK,CAACqB,SAAS,CAAC,MAAM;IACpB,IAAIC,OAAO,GAAG,IAAI;IAElB,eAAeC,mBAAmBA,CAAA,EAAG;MACnC,IAAI;QACF,MAAMC,IAAI,GAAG,MAAMtB,iBAAiB,CAAC,CAAC;QACtC,IAAIoB,OAAO,EAAE;UACXP,iBAAiB,CAACS,IAAI,CAAC;UACvBJ,QAAQ,CAAC,IAAI,CAAC;QAChB;MACF,CAAC,CAAC,OAAOK,GAAG,EAAE;QACZ,IAAIH,OAAO,EAAE;UACXF,QAAQ,CAACK,GAAG,YAAYC,KAAK,GAAGD,GAAG,GAAG,IAAIC,KAAK,CAAC,eAAe,CAAC,CAAC;QACnE;MACF,CAAC,SAAS;QACR,IAAIJ,OAAO,EAAE;UACXJ,UAAU,CAAC,KAAK,CAAC;QACnB;MACF;IACF;IAEAK,mBAAmB,CAAC,CAAC;IAErB,OAAO,MAAM;MACXD,OAAO,GAAG,KAAK;IACjB,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO;IAAER,cAAc;IAAEG,OAAO;IAAEE;EAAM,CAAC;AAC3C;AAEA,eAAe;EACbjB,iBAAiB;EACjBK,mBAAmB;EACnBM;AACF,CAAC","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,4 @@
1
+ import { type ConfigPlugin } from '@expo/config-plugins';
2
+ declare const _default: ConfigPlugin<void>;
3
+ export default _default;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../plugin/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAuB,MAAM,sBAAsB,CAAC;;AAQ9E,wBAA8E"}
@@ -0,0 +1,16 @@
1
+ import type { TurboModule } from 'react-native';
2
+ export interface NavigationModeInfo {
3
+ interactionMode?: number;
4
+ type: '3_button' | '2_button' | 'gesture' | 'unknown';
5
+ isGestureNavigation: boolean;
6
+ hasNavigationBar: boolean;
7
+ sdkVersion: number;
8
+ deviceModel: string;
9
+ }
10
+ export interface Spec extends TurboModule {
11
+ getNavigationMode(): Promise<NavigationModeInfo>;
12
+ isGestureNavigation(): Promise<boolean>;
13
+ }
14
+ declare const _default: Spec;
15
+ export default _default;
16
+ //# sourceMappingURL=NativeNavigationMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeNavigationMode.d.ts","sourceRoot":"","sources":["../../../src/NativeNavigationMode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,kBAAkB;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IACtD,mBAAmB,EAAE,OAAO,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjD,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACzC;;AAED,wBAAwE"}
@@ -0,0 +1,27 @@
1
+ import { type NavigationModeInfo } from './NativeNavigationMode';
2
+ export type { NavigationModeInfo };
3
+ /**
4
+ * Get detailed navigation mode information
5
+ * Returns navigation type, interaction mode, and device info
6
+ */
7
+ export declare function getNavigationMode(): Promise<NavigationModeInfo>;
8
+ /**
9
+ * Quick check if the device is using gesture-based navigation
10
+ * @returns Promise<boolean> - true if gesture navigation is active
11
+ */
12
+ export declare function isGestureNavigation(): Promise<boolean>;
13
+ /**
14
+ * Hook for React components to get navigation mode
15
+ */
16
+ export declare function useNavigationMode(): {
17
+ navigationMode: NavigationModeInfo | null;
18
+ loading: boolean;
19
+ error: Error | null;
20
+ };
21
+ declare const _default: {
22
+ readonly getNavigationMode: typeof getNavigationMode;
23
+ readonly isGestureNavigation: typeof isGestureNavigation;
24
+ readonly useNavigationMode: typeof useNavigationMode;
25
+ };
26
+ export default _default;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,OAA6B,EAC3B,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAa/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAOtD;AAED;;GAEG;AACH,wBAAgB,iBAAiB;;;;EAmChC;;;;;;AAED,wBAIW"}
package/package.json ADDED
@@ -0,0 +1,174 @@
1
+ {
2
+ "name": "react-native-navigation-mode",
3
+ "version": "0.2.0",
4
+ "description": "Detect Android navigation mode (3-button, 2-button, or gesture)",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "expo": {
16
+ "platforms": [
17
+ "android",
18
+ "ios"
19
+ ],
20
+ "plugin": "./plugin/build/index.js"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "lib",
25
+ "android",
26
+ "ios",
27
+ "cpp",
28
+ "*.podspec",
29
+ "react-native.config.js",
30
+ "!ios/build",
31
+ "!android/build",
32
+ "!android/gradle",
33
+ "!android/gradlew",
34
+ "!android/gradlew.bat",
35
+ "!android/local.properties",
36
+ "!**/__tests__",
37
+ "!**/__fixtures__",
38
+ "!**/__mocks__",
39
+ "!**/.*"
40
+ ],
41
+ "scripts": {
42
+ "example": "yarn workspace react-native-navigation-mode-example",
43
+ "test": "jest",
44
+ "typecheck": "tsc",
45
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
46
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
47
+ "prepare": "bob build",
48
+ "release": "release-it --only-version"
49
+ },
50
+ "keywords": [
51
+ "react-native",
52
+ "android",
53
+ "navigation",
54
+ "gesture",
55
+ "turbo-module"
56
+ ],
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/JairajJangle/react-native-navigation-mode.git"
60
+ },
61
+ "author": "Jairaj Jangle <reachout.jairaj.jangle@gmail.com> (https://github.com/JairajJangle)",
62
+ "license": "MIT",
63
+ "bugs": {
64
+ "url": "https://github.com/JairajJangle/react-native-navigation-mode/issues"
65
+ },
66
+ "homepage": "https://github.com/JairajJangle/react-native-navigation-mode#readme",
67
+ "publishConfig": {
68
+ "registry": "https://registry.npmjs.org/"
69
+ },
70
+ "devDependencies": {
71
+ "@commitlint/config-conventional": "^19.6.0",
72
+ "@eslint/compat": "^1.2.7",
73
+ "@eslint/eslintrc": "^3.3.0",
74
+ "@eslint/js": "^9.22.0",
75
+ "@evilmartians/lefthook": "^1.5.0",
76
+ "@expo/config-plugins": "^10.0.0",
77
+ "@react-native-community/cli": "15.0.0-alpha.2",
78
+ "@react-native/babel-preset": "0.79.2",
79
+ "@react-native/eslint-config": "^0.78.0",
80
+ "@release-it/conventional-changelog": "^9.0.2",
81
+ "@types/jest": "^29.5.5",
82
+ "@types/react": "^19.0.0",
83
+ "commitlint": "^19.6.1",
84
+ "del-cli": "^5.1.0",
85
+ "eslint": "^9.22.0",
86
+ "eslint-config-prettier": "^10.1.1",
87
+ "eslint-plugin-prettier": "^5.2.3",
88
+ "jest": "^29.7.0",
89
+ "prettier": "^3.0.3",
90
+ "react": "19.0.0",
91
+ "react-native": "0.79.2",
92
+ "react-native-builder-bob": "^0.40.11",
93
+ "release-it": "^17.10.0",
94
+ "turbo": "^1.10.7",
95
+ "typescript": "^5.8.3"
96
+ },
97
+ "peerDependencies": {
98
+ "react": "*",
99
+ "react-native": "*"
100
+ },
101
+ "workspaces": [
102
+ "example"
103
+ ],
104
+ "packageManager": "yarn@3.6.1",
105
+ "jest": {
106
+ "preset": "react-native",
107
+ "modulePathIgnorePatterns": [
108
+ "<rootDir>/example/node_modules",
109
+ "<rootDir>/lib/"
110
+ ]
111
+ },
112
+ "commitlint": {
113
+ "extends": [
114
+ "@commitlint/config-conventional"
115
+ ]
116
+ },
117
+ "release-it": {
118
+ "git": {
119
+ "commitMessage": "chore: release ${version}",
120
+ "tagName": "v${version}"
121
+ },
122
+ "npm": {
123
+ "publish": true
124
+ },
125
+ "github": {
126
+ "release": true
127
+ },
128
+ "plugins": {
129
+ "@release-it/conventional-changelog": {
130
+ "preset": {
131
+ "name": "angular"
132
+ }
133
+ }
134
+ }
135
+ },
136
+ "prettier": {
137
+ "quoteProps": "consistent",
138
+ "singleQuote": true,
139
+ "tabWidth": 2,
140
+ "trailingComma": "es5",
141
+ "useTabs": false
142
+ },
143
+ "react-native-builder-bob": {
144
+ "source": "src",
145
+ "output": "lib",
146
+ "targets": [
147
+ [
148
+ "module",
149
+ {
150
+ "esm": true
151
+ }
152
+ ],
153
+ [
154
+ "typescript",
155
+ {
156
+ "project": "tsconfig.build.json"
157
+ }
158
+ ]
159
+ ]
160
+ },
161
+ "codegenConfig": {
162
+ "name": "NavigationModeSpec",
163
+ "type": "modules",
164
+ "jsSrcsDir": "src",
165
+ "android": {
166
+ "javaPackageName": "com.navigationmode"
167
+ }
168
+ },
169
+ "create-react-native-library": {
170
+ "languages": "kotlin-objc",
171
+ "type": "turbo-module",
172
+ "version": "0.50.3"
173
+ }
174
+ }
@@ -0,0 +1,18 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ export interface NavigationModeInfo {
5
+ interactionMode?: number;
6
+ type: '3_button' | '2_button' | 'gesture' | 'unknown';
7
+ isGestureNavigation: boolean;
8
+ hasNavigationBar: boolean;
9
+ sdkVersion: number;
10
+ deviceModel: string;
11
+ }
12
+
13
+ export interface Spec extends TurboModule {
14
+ getNavigationMode(): Promise<NavigationModeInfo>;
15
+ isGestureNavigation(): Promise<boolean>;
16
+ }
17
+
18
+ export default TurboModuleRegistry.getEnforcing<Spec>('NavigationMode');
package/src/index.tsx ADDED
@@ -0,0 +1,85 @@
1
+ import { Platform } from 'react-native';
2
+ import React from 'react';
3
+ import NavigationModeModule, {
4
+ type NavigationModeInfo,
5
+ } from './NativeNavigationMode';
6
+
7
+ export type { NavigationModeInfo };
8
+
9
+ /**
10
+ * Get detailed navigation mode information
11
+ * Returns navigation type, interaction mode, and device info
12
+ */
13
+ export function getNavigationMode(): Promise<NavigationModeInfo> {
14
+ if (Platform.OS === 'ios') {
15
+ // iOS always uses gesture navigation (no 3-button navigation exists)
16
+ return Promise.resolve({
17
+ type: 'gesture',
18
+ isGestureNavigation: true,
19
+ hasNavigationBar: false,
20
+ sdkVersion: parseInt(Platform.Version as string, 10),
21
+ deviceModel: 'iOS',
22
+ });
23
+ }
24
+
25
+ return NavigationModeModule.getNavigationMode();
26
+ }
27
+
28
+ /**
29
+ * Quick check if the device is using gesture-based navigation
30
+ * @returns Promise<boolean> - true if gesture navigation is active
31
+ */
32
+ export function isGestureNavigation(): Promise<boolean> {
33
+ if (Platform.OS === 'ios') {
34
+ // iOS always uses gesture navigation
35
+ return Promise.resolve(true);
36
+ }
37
+
38
+ return NavigationModeModule.isGestureNavigation();
39
+ }
40
+
41
+ /**
42
+ * Hook for React components to get navigation mode
43
+ */
44
+ export function useNavigationMode() {
45
+ const [navigationMode, setNavigationMode] =
46
+ React.useState<NavigationModeInfo | null>(null);
47
+ const [loading, setLoading] = React.useState(true);
48
+ const [error, setError] = React.useState<Error | null>(null);
49
+
50
+ React.useEffect(() => {
51
+ let mounted = true;
52
+
53
+ async function fetchNavigationMode() {
54
+ try {
55
+ const mode = await getNavigationMode();
56
+ if (mounted) {
57
+ setNavigationMode(mode);
58
+ setError(null);
59
+ }
60
+ } catch (err) {
61
+ if (mounted) {
62
+ setError(err instanceof Error ? err : new Error('Unknown error'));
63
+ }
64
+ } finally {
65
+ if (mounted) {
66
+ setLoading(false);
67
+ }
68
+ }
69
+ }
70
+
71
+ fetchNavigationMode();
72
+
73
+ return () => {
74
+ mounted = false;
75
+ };
76
+ }, []);
77
+
78
+ return { navigationMode, loading, error };
79
+ }
80
+
81
+ export default {
82
+ getNavigationMode,
83
+ isGestureNavigation,
84
+ useNavigationMode,
85
+ } as const;