react-native-local-network-info 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # react-native-local-network-info
2
+
3
+ Read a React Native device's **own local IPv4 address** and whether it's **connected to WiFi** or **acting as a hotspot host** — with **live network-change events**.
4
+
5
+ > ℹ️ This library only reads **your own device's** network interfaces. It does **not** scan, probe, or connect to any other device on the network, and it needs no location permission.
6
+
7
+ Built with the [Expo Modules API](https://docs.expo.dev/modules/overview/), so it works in **both bare React Native and Expo** apps and supports the New Architecture out of the box.
8
+
9
+ ## Why
10
+
11
+ `expo-network` / `WifiManager.getConnectionInfo()` only return the **WiFi station** IP — they report `0.0.0.0` when the device is the hotspot host. This module reads the device's network interfaces directly, so it returns a usable LAN IP whether the device is a WiFi client **or** the hotspot, and tells you which.
12
+
13
+ ## Features
14
+
15
+ - ✅ Device's own **local IPv4** on the active LAN interface.
16
+ - ✅ **Role**: `wifi`, `hotspot`, or `none`.
17
+ - ✅ **WiFi takes precedence** when both WiFi and hotspot are active at once.
18
+ - ✅ **Hotspot host** detection that handles Android 11+'s **randomized** SoftAP subnet (reads the real interface IP — never hardcodes `192.168.43.1`).
19
+ - ✅ **Predicted client IP range** when the device is a hotspot (iOS: fixed `172.20.10.2–.14`; Android: derived from the live subnet).
20
+ - ✅ **Live listener** that re-fires on every connectivity / WiFi / hotspot change.
21
+ - ✅ A `useLocalIp()` React hook.
22
+ - ✅ No location permission required.
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ npm install react-native-local-network-info
28
+ ```
29
+
30
+ > **Bare React Native:** you must also have the `expo` package installed so Expo Modules autolinking works. If you don't yet:
31
+ > ```sh
32
+ > npx install-expo-modules@latest
33
+ > ```
34
+ > Then `cd ios && pod install`. Expo apps need no extra steps.
35
+
36
+ ### Requirements
37
+
38
+ | | Minimum |
39
+ | --- | --- |
40
+ | Expo SDK | 54+ (New Architecture) |
41
+ | iOS | 15.1 |
42
+ | Android | API 24 (Android 7.0) |
43
+
44
+ ### Permissions
45
+
46
+ Android permissions are merged automatically via the module's manifest:
47
+
48
+ - `ACCESS_NETWORK_STATE` — for the change listener & station gateway.
49
+ - `ACCESS_WIFI_STATE` — for the DHCP gateway fallback.
50
+
51
+ No `ACCESS_FINE_LOCATION` and no iOS `Info.plist` keys are needed — the module only reads its **own** interfaces (it never connects to or scans other devices).
52
+
53
+ ## Usage
54
+
55
+ ```ts
56
+ import {
57
+ getLocalIp,
58
+ addNetworkChangeListener,
59
+ useLocalIp,
60
+ } from 'react-native-local-network-info';
61
+
62
+ // One-shot read
63
+ const info = await getLocalIp();
64
+ console.log(info.ip, info.role); // e.g. "192.168.1.42" "wifi"
65
+
66
+ // Live updates
67
+ const sub = addNetworkChangeListener((info) => {
68
+ console.log('network changed →', info.role, info.ip);
69
+ });
70
+ // later: sub.remove();
71
+ ```
72
+
73
+ ### React hook
74
+
75
+ ```tsx
76
+ import { useLocalIp } from 'react-native-local-network-info';
77
+
78
+ function Status() {
79
+ const info = useLocalIp(); // null until first snapshot, then live
80
+ if (!info) return <Text>Detecting…</Text>;
81
+ return (
82
+ <Text>
83
+ {info.role === 'hotspot' ? 'Hosting hotspot at' : 'Local IP'}: {info.ip}
84
+ </Text>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `getLocalIp(): Promise<LocalIpInfo>`
92
+
93
+ Captures the current snapshot.
94
+
95
+ ### `addNetworkChangeListener(cb): EventSubscription`
96
+
97
+ Subscribes to changes. Call `.remove()` on the returned subscription to unsubscribe.
98
+
99
+ ### `getAllInterfaces(): Promise<NetworkInterfaceInfo[]>`
100
+
101
+ Every up, non-loopback IPv4 interface with a best-effort role — handy for debugging unusual devices.
102
+
103
+ ### `useLocalIp(): LocalIpInfo | null`
104
+
105
+ Hook returning the latest snapshot, kept live and auto-cleaned on unmount.
106
+
107
+ ### `LocalIpInfo`
108
+
109
+ ```ts
110
+ interface LocalIpInfo {
111
+ ip: string | null; // device's own local IPv4 (wifi first, then hotspot)
112
+ role: 'wifi' | 'hotspot' | 'none'; // wifi wins when both are active
113
+ isWifiConnected: boolean;
114
+ isHotspotHost: boolean;
115
+ interfaceName: string | null; // "en0" | "wlan0" | "bridge100" | "ap0" | ...
116
+ netmask: string | null; // "255.255.255.0"
117
+ gateway: string | null; // see notes below
118
+ predictedClientRange: { first: string; last: string } | null; // hotspot only
119
+ platform: 'ios' | 'android' | 'web';
120
+ timestamp: number; // epoch ms
121
+ }
122
+ ```
123
+
124
+ ## How detection works
125
+
126
+ | | iOS | Android |
127
+ | --- | --- | --- |
128
+ | Enumeration | `getifaddrs(3)` | `java.net.NetworkInterface` |
129
+ | WiFi station | `en0` | the interface `ConnectivityManager` reports for the `TRANSPORT_WIFI` network (usually `wlan0`) |
130
+ | Hotspot host | `bridge*` @ `172.20.10.1/28` | any WiFi-family interface (`wlan*`/`ap*`/`swlan*`/`softap*`) that is **not** the confirmed station — reads its live IP |
131
+ | Cellular (ignored for LAN IP) | `pdp_ip0` | `rmnet*` / `ccmni*` |
132
+ | Change events | `NWPathMonitor` | `registerDefaultNetworkCallback` + `WIFI_AP_STATE_CHANGED` |
133
+ | Station gateway | derived from subnet (heuristic) | `LinkProperties` default route (real) |
134
+
135
+ **Precedence:** if a WiFi-station IP exists it is returned with `role: 'wifi'`; otherwise the hotspot-host IP is returned with `role: 'hotspot'`; otherwise `role: 'none'` with `ip: null`.
136
+
137
+ ## Platform notes & limitations
138
+
139
+ - **iOS station gateway is a heuristic** (first host of the subnet). Apple exposes no public default-gateway API; the WiFi IP, netmask, and role are accurate.
140
+ - **iOS hotspot change events:** `NWPathMonitor` reliably reports WiFi/cellular changes, but toggling Personal Hotspot while the default path is unchanged may not fire an event. Call `getLocalIp()` to force a fresh read when you need certainty.
141
+ - **Android hotspot vs. station** is resolved by asking `ConnectivityManager` which interface carries the real `TRANSPORT_WIFI` (station) network, then treating any *other* WiFi-family interface with an IP as the hotspot. This correctly handles phones that host the SoftAP on `wlan0` itself (not just `ap0`/`swlan0`), and devices running WiFi + hotspot concurrently. Use `getAllInterfaces()` to inspect an unusual device.
142
+ - **Web** returns `role: 'none'` / `ip: null` — browsers don't expose the LAN IP.
143
+ - iOS interface-name → role mapping is a documented **heuristic** (Apple exposes no public role API); the hotspot-host `172.20.10.1`/`255.255.255.240` fingerprint and `NetworkInterface` reads are the reliable signals.
144
+
145
+ ## Example app
146
+
147
+ ```sh
148
+ cd example
149
+ npm install
150
+ npx expo prebuild # generates ios/ and android/
151
+ npx expo run:ios # or: npx expo run:android
152
+ ```
153
+
154
+ Then toggle WiFi / your hotspot in Settings and watch the values update live.
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,20 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'com.localnetworkinfo'
7
+ version = '0.1.0'
8
+
9
+ // compileSdk (36), minSdk (24), targetSdk (36), Kotlin (2.0.21) and the
10
+ // JVM toolchain (17) are supplied by the `expo-module-gradle-plugin`.
11
+ android {
12
+ namespace 'com.localnetworkinfo'
13
+ defaultConfig {
14
+ versionCode 1
15
+ versionName '0.1.0'
16
+ }
17
+ lintOptions {
18
+ abortOnError false
19
+ }
20
+ }
@@ -0,0 +1,8 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- Required for ConnectivityManager (network callbacks, LinkProperties). -->
3
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
4
+ <!-- Required for WifiManager (DHCP gateway fallback). -->
5
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
6
+ <!-- NOTE: ACCESS_FINE_LOCATION is intentionally NOT requested: IP/role/gateway
7
+ detection does not need it (only WiFi scanning / reading SSID would). -->
8
+ </manifest>
@@ -0,0 +1,371 @@
1
+ package com.localnetworkinfo
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.net.ConnectivityManager
8
+ import android.net.LinkProperties
9
+ import android.net.Network
10
+ import android.net.NetworkCapabilities
11
+ import android.os.Build
12
+ import android.os.Handler
13
+ import android.os.HandlerThread
14
+ import expo.modules.kotlin.exception.Exceptions
15
+ import expo.modules.kotlin.modules.Module
16
+ import expo.modules.kotlin.modules.ModuleDefinition
17
+ import java.net.Inet4Address
18
+ import java.net.NetworkInterface
19
+
20
+ class LocalNetworkInfoModule : Module() {
21
+
22
+ private val context: Context
23
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
24
+
25
+ private val connectivityManager: ConnectivityManager
26
+ get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
27
+
28
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
29
+ private var apStateReceiver: BroadcastReceiver? = null
30
+ private var handlerThread: HandlerThread? = null
31
+ private var handler: Handler? = null
32
+ private val resampleRunnable = Runnable { doEmit() }
33
+
34
+ override fun definition() = ModuleDefinition {
35
+ Name("LocalNetworkInfo")
36
+
37
+ Events("onNetworkChange")
38
+
39
+ AsyncFunction("getLocalIpAsync") {
40
+ return@AsyncFunction buildInfo()
41
+ }
42
+
43
+ AsyncFunction("getAllInterfacesAsync") {
44
+ return@AsyncFunction enumerateInterfaces().map { it.toMap() }
45
+ }
46
+
47
+ OnStartObserving {
48
+ startMonitoring()
49
+ }
50
+
51
+ OnStopObserving {
52
+ stopMonitoring()
53
+ }
54
+
55
+ OnDestroy {
56
+ stopMonitoring()
57
+ }
58
+ }
59
+
60
+ // region Network change monitoring
61
+
62
+ private fun startMonitoring() {
63
+ if (handlerThread == null) {
64
+ val thread = HandlerThread("expo.localnetworkinfo.emit").apply { start() }
65
+ handler = Handler(thread.looper)
66
+ handlerThread = thread
67
+ }
68
+
69
+ if (networkCallback == null) {
70
+ val callback = object : ConnectivityManager.NetworkCallback() {
71
+ override fun onAvailable(network: Network) = emitChange()
72
+ override fun onLost(network: Network) = emitChange()
73
+ override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) = emitChange()
74
+ override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) = emitChange()
75
+ }
76
+ try {
77
+ // registerDefaultNetworkCallback is API 24+ (our minSdk is 24).
78
+ connectivityManager.registerDefaultNetworkCallback(callback)
79
+ networkCallback = callback
80
+ } catch (_: Exception) {
81
+ // Leave networkCallback null; the AP receiver below may still work.
82
+ }
83
+ }
84
+
85
+ if (apStateReceiver == null) {
86
+ val receiver = object : BroadcastReceiver() {
87
+ override fun onReceive(receivedContext: Context?, intent: Intent?) = emitChange()
88
+ }
89
+ try {
90
+ // Fired by the system whenever the WiFi hotspot is enabled/disabled.
91
+ // NetworkCallback does NOT cover the device's own SoftAP, so we need this.
92
+ val filter = IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED")
93
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
94
+ context.applicationContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
95
+ } else {
96
+ @Suppress("UnspecifiedRegisterReceiverFlag")
97
+ context.applicationContext.registerReceiver(receiver, filter)
98
+ }
99
+ apStateReceiver = receiver
100
+ } catch (_: Exception) {
101
+ // Hotspot-toggle events will be missed, but everything else still works.
102
+ }
103
+ }
104
+ }
105
+
106
+ private fun stopMonitoring() {
107
+ handler?.removeCallbacks(resampleRunnable)
108
+ handlerThread?.quitSafely()
109
+ handlerThread = null
110
+ handler = null
111
+
112
+ networkCallback?.let {
113
+ try {
114
+ connectivityManager.unregisterNetworkCallback(it)
115
+ } catch (_: Exception) {
116
+ }
117
+ }
118
+ networkCallback = null
119
+
120
+ apStateReceiver?.let {
121
+ try {
122
+ context.applicationContext.unregisterReceiver(it)
123
+ } catch (_: Exception) {
124
+ }
125
+ }
126
+ apStateReceiver = null
127
+ }
128
+
129
+ private fun doEmit() {
130
+ try {
131
+ sendEvent("onNetworkChange", buildInfo())
132
+ } catch (_: Exception) {
133
+ }
134
+ }
135
+
136
+ private fun emitChange() {
137
+ // Emit an immediate snapshot, then re-sample over the next few seconds. On
138
+ // Android the hotspot/station interface frequently receives its IPv4 a beat
139
+ // AFTER the change event fires, and no further event follows — so a single
140
+ // immediate snapshot can report "no connection" until the next manual
141
+ // refresh. The trailing re-samples catch the late address assignment.
142
+ doEmit()
143
+ handler?.let { h ->
144
+ h.removeCallbacks(resampleRunnable)
145
+ h.postDelayed(resampleRunnable, 700)
146
+ h.postDelayed(resampleRunnable, 1800)
147
+ h.postDelayed(resampleRunnable, 3500)
148
+ }
149
+ }
150
+
151
+ // endregion
152
+
153
+ // region Interface model
154
+
155
+ private enum class Role(val value: String) {
156
+ WIFI("wifi"),
157
+ HOTSPOT("hotspot"),
158
+ CELLULAR("cellular"),
159
+ ETHERNET("ethernet"),
160
+ OTHER("other")
161
+ }
162
+
163
+ private data class Iface(
164
+ val name: String,
165
+ val ip: String,
166
+ val netmask: String?,
167
+ val prefixLength: Int,
168
+ val role: Role
169
+ ) {
170
+ fun toMap(): Map<String, Any?> = mapOf(
171
+ "name" to name,
172
+ "ip" to ip,
173
+ "netmask" to netmask,
174
+ "role" to role.value
175
+ )
176
+ }
177
+
178
+ /**
179
+ * Classify an interface, using ConnectivityManager to confirm which interface
180
+ * actually carries a WiFi *station* network. This is what lets us tell a
181
+ * hotspot apart from a station even when the device hosts its SoftAP on
182
+ * `wlan0` itself (common on single-radio phones) instead of a separate
183
+ * `ap0` / `wlan1` / `swlan0` / `softap0` interface.
184
+ */
185
+ private fun classify(name: String, stationInterfaces: Set<String>): Role = when {
186
+ // Confirmed by the system to be a real WiFi station (client) connection.
187
+ name in stationInterfaces -> Role.WIFI
188
+ name.startsWith("rmnet") || name.startsWith("ccmni") -> Role.CELLULAR
189
+ name.startsWith("eth") -> Role.ETHERNET
190
+ // Any WiFi-family interface that is NOT a confirmed station is the hotspot/SoftAP.
191
+ name.startsWith("wlan") ||
192
+ name.startsWith("ap") ||
193
+ name.startsWith("swlan") ||
194
+ name.startsWith("softap") -> Role.HOTSPOT
195
+ else -> Role.OTHER
196
+ }
197
+
198
+ /** Interface names ConnectivityManager reports as carrying a WiFi station network. */
199
+ private fun wifiStationInterfaceNames(): Set<String> {
200
+ val names = mutableSetOf<String>()
201
+ try {
202
+ @Suppress("DEPRECATION")
203
+ val networks = connectivityManager.allNetworks
204
+ for (network in networks) {
205
+ val capabilities = connectivityManager.getNetworkCapabilities(network) ?: continue
206
+ if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) continue
207
+ connectivityManager.getLinkProperties(network)?.interfaceName?.let { names.add(it) }
208
+ }
209
+ } catch (_: Exception) {
210
+ }
211
+ return names
212
+ }
213
+
214
+ // endregion
215
+
216
+ // region Detection
217
+
218
+ private fun enumerateInterfaces(): List<Iface> {
219
+ val result = mutableListOf<Iface>()
220
+ val stationInterfaces = wifiStationInterfaceNames()
221
+
222
+ val interfaces = try {
223
+ NetworkInterface.getNetworkInterfaces() ?: return result
224
+ } catch (_: Exception) {
225
+ return result
226
+ }
227
+
228
+ for (nif in interfaces) {
229
+ try {
230
+ if (!nif.isUp || nif.isLoopback) continue
231
+ } catch (_: Exception) {
232
+ continue
233
+ }
234
+
235
+ // interfaceAddresses gives us the prefix length (for netmask + prediction).
236
+ for (interfaceAddress in nif.interfaceAddresses) {
237
+ val address = interfaceAddress.address
238
+ if (address !is Inet4Address) continue
239
+ if (address.isLoopbackAddress || address.isLinkLocalAddress) continue
240
+
241
+ val ip = address.hostAddress ?: continue
242
+ val prefix = interfaceAddress.networkPrefixLength.toInt()
243
+ result.add(Iface(nif.name, ip, prefixToNetmask(prefix), prefix, classify(nif.name, stationInterfaces)))
244
+ }
245
+ }
246
+
247
+ return result
248
+ }
249
+
250
+ private fun buildInfo(): Map<String, Any?> {
251
+ val interfaces = enumerateInterfaces()
252
+
253
+ val station = interfaces.firstOrNull { it.role == Role.WIFI }
254
+ val host = interfaces.firstOrNull { it.role == Role.HOTSPOT }
255
+
256
+ val isWifiConnected = station != null
257
+ val isHotspotHost = host != null
258
+
259
+ var ip: String? = null
260
+ var role = "none"
261
+ var interfaceName: String? = null
262
+ var netmask: String? = null
263
+ var gateway: String? = null
264
+ var predicted: Map<String, String>? = null
265
+
266
+ if (station != null) {
267
+ ip = station.ip
268
+ role = "wifi"
269
+ interfaceName = station.name
270
+ netmask = station.netmask
271
+ // Prefer the real default-route gateway; fall back to a subnet guess.
272
+ gateway = stationGateway() ?: deriveGateway(station.ip, station.prefixLength)
273
+ } else if (host != null) {
274
+ ip = host.ip
275
+ role = "hotspot"
276
+ interfaceName = host.name
277
+ netmask = host.netmask
278
+ gateway = host.ip // the hotspot host is its own gateway
279
+ predicted = predictClientRange(host.ip, host.prefixLength)
280
+ }
281
+
282
+ return mapOf(
283
+ "ip" to ip,
284
+ "role" to role,
285
+ "isWifiConnected" to isWifiConnected,
286
+ "isHotspotHost" to isHotspotHost,
287
+ "interfaceName" to interfaceName,
288
+ "netmask" to netmask,
289
+ "gateway" to gateway,
290
+ "predictedClientRange" to predicted,
291
+ "platform" to "android",
292
+ "timestamp" to System.currentTimeMillis().toDouble()
293
+ )
294
+ }
295
+
296
+ /** Real station gateway via LinkProperties (default route), with a DHCP fallback. */
297
+ private fun stationGateway(): String? {
298
+ return try {
299
+ val network = connectivityManager.activeNetwork ?: return null
300
+ val linkProperties = connectivityManager.getLinkProperties(network) ?: return null
301
+ // Ignore the 0.0.0.0 wildcard (appears as the "gateway" of a directly-connected
302
+ // default route, e.g. when the device is tethering rather than a real station).
303
+ val routeGateway = linkProperties.routes
304
+ .firstOrNull { it.isDefaultRoute && it.gateway?.isAnyLocalAddress == false }
305
+ ?.gateway
306
+ ?.hostAddress
307
+ if (routeGateway != null) return routeGateway
308
+ val dhcpServer = if (Build.VERSION.SDK_INT >= 30) linkProperties.dhcpServerAddress else null
309
+ if (dhcpServer != null && !dhcpServer.isAnyLocalAddress) dhcpServer.hostAddress else null
310
+ } catch (_: Exception) {
311
+ null
312
+ }
313
+ }
314
+
315
+ // endregion
316
+
317
+ // region IPv4 math
318
+
319
+ private fun prefixToNetmask(prefix: Int): String? {
320
+ if (prefix < 0 || prefix > 32) return null
321
+ val mask = if (prefix == 0) 0 else (-1 shl (32 - prefix))
322
+ return "${(mask ushr 24) and 0xff}.${(mask ushr 16) and 0xff}.${(mask ushr 8) and 0xff}.${mask and 0xff}"
323
+ }
324
+
325
+ private fun ipToInt(ip: String): Int? {
326
+ val parts = ip.split(".")
327
+ if (parts.size != 4) return null
328
+ var value = 0
329
+ for (part in parts) {
330
+ val octet = part.toIntOrNull() ?: return null
331
+ if (octet < 0 || octet > 255) return null
332
+ value = (value shl 8) or octet
333
+ }
334
+ return value
335
+ }
336
+
337
+ private fun intToIp(value: Int): String =
338
+ "${(value ushr 24) and 0xff}.${(value ushr 16) and 0xff}.${(value ushr 8) and 0xff}.${value and 0xff}"
339
+
340
+ /** Subnet-guess gateway (first host) — only used as an Android station fallback. */
341
+ private fun deriveGateway(ip: String, prefix: Int): String? {
342
+ val ipInt = ipToInt(ip) ?: return null
343
+ if (prefix <= 0 || prefix >= 32) return null
344
+ val mask = -1 shl (32 - prefix)
345
+ val network = ipInt and mask
346
+ return intToIp(network + 1)
347
+ }
348
+
349
+ /**
350
+ * Predict the usable client IPv4 range of a hotspot subnet (network+1 ..
351
+ * broadcast-1). The host occupies one address within this range unless it is
352
+ * the first host, in which case the range starts at the next address.
353
+ */
354
+ private fun predictClientRange(hostIp: String, prefix: Int): Map<String, String>? {
355
+ val ipInt = ipToInt(hostIp) ?: return null
356
+ if (prefix <= 0 || prefix >= 31) return null
357
+ val mask = -1 shl (32 - prefix)
358
+ val network = ipInt and mask
359
+ val broadcast = network or mask.inv()
360
+
361
+ var first = network + 1
362
+ if (first == ipInt) first += 1 // skip the host itself
363
+ val last = broadcast - 1
364
+ // Compare as unsigned so high-bit subnets (e.g. 192.168/172.16) order correctly.
365
+ if (Integer.compareUnsigned(last, first) < 0) return null
366
+
367
+ return mapOf("first" to intToIp(first), "last" to intToIp(last))
368
+ }
369
+
370
+ // endregion
371
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Which network role is currently providing the device's local IPv4.
3
+ *
4
+ * - `wifi` — the device is connected to a WiFi network as a station/client.
5
+ * - `hotspot` — the device is acting as a WiFi hotspot (SoftAP) host.
6
+ * - `none` — neither WiFi nor hotspot is providing a usable LAN address
7
+ * (e.g. cellular-only, airplane mode, or offline).
8
+ *
9
+ * When both WiFi and hotspot are active simultaneously, `wifi` takes precedence.
10
+ */
11
+ export type NetworkRole = 'wifi' | 'hotspot' | 'none';
12
+ /** The platform that produced a given snapshot. */
13
+ export type DevicePlatform = 'ios' | 'android' | 'web';
14
+ /**
15
+ * The usable host range of the hotspot subnet — i.e. the addresses a client
16
+ * could be assigned. This is the whole subnet's usable span (network+1 ..
17
+ * broadcast-1), so the host/gateway itself is one address *within* this range
18
+ * (it is not a boundary). When scanning for clients, skip your own `ip`.
19
+ *
20
+ * Example: host/gateway `10.121.184.216` on a `/24` → range `10.121.184.1` ..
21
+ * `10.121.184.254` (a client may legitimately be `.215` *or* `.217`).
22
+ */
23
+ export interface ClientIpRange {
24
+ /** First usable client IPv4 in the subnet. */
25
+ first: string;
26
+ /** Last usable client IPv4 in the subnet. */
27
+ last: string;
28
+ }
29
+ /**
30
+ * A point-in-time snapshot of the device's local network position.
31
+ *
32
+ * The same shape is returned by {@link getLocalIp} and delivered to
33
+ * {@link addNetworkChangeListener} / {@link useLocalIp}.
34
+ */
35
+ export interface LocalIpInfo {
36
+ /**
37
+ * The device's own local IPv4 on the active LAN interface, or `null` when
38
+ * there is no WiFi/hotspot LAN address. Follows the precedence:
39
+ * WiFi station IP first, then hotspot host IP.
40
+ */
41
+ ip: string | null;
42
+ /** Which role is providing {@link ip}. WiFi takes precedence over hotspot. */
43
+ role: NetworkRole;
44
+ /** `true` when connected to a WiFi network as a client/station. */
45
+ isWifiConnected: boolean;
46
+ /** `true` when this device is currently acting as a WiFi hotspot (SoftAP) host. */
47
+ isHotspotHost: boolean;
48
+ /**
49
+ * Name of the network interface backing {@link ip}
50
+ * (e.g. `en0`, `wlan0`, `bridge100`, `ap0`), or `null`.
51
+ */
52
+ interfaceName: string | null;
53
+ /** Dotted IPv4 subnet mask for {@link ip} (e.g. `255.255.255.0`), or `null`. */
54
+ netmask: string | null;
55
+ /**
56
+ * The LAN gateway.
57
+ *
58
+ * - As a **hotspot host**, this is the device itself (it *is* the gateway).
59
+ * - As an **Android station**, this is the real default-route gateway
60
+ * (read from `LinkProperties`).
61
+ * - As an **iOS station**, this is a best-effort guess derived from the
62
+ * subnet (the first host address), because Apple exposes no public gateway
63
+ * API. Treat the iOS station gateway as a heuristic.
64
+ */
65
+ gateway: string | null;
66
+ /**
67
+ * When this device is a hotspot host, the predicted inclusive range of IPs
68
+ * its clients can receive, derived from the host IP + subnet:
69
+ *
70
+ * - **iOS** — the fixed Personal Hotspot range `172.20.10.2`–`172.20.10.14`.
71
+ * - **Android** — computed from the (randomized on Android 11+) SoftAP subnet.
72
+ *
73
+ * `null` when the device is not a hotspot host.
74
+ */
75
+ predictedClientRange: ClientIpRange | null;
76
+ /** Platform that captured this snapshot. */
77
+ platform: DevicePlatform;
78
+ /** Epoch milliseconds when this snapshot was captured natively. */
79
+ timestamp: number;
80
+ }
81
+ /** Per-interface detail, returned by {@link getAllInterfaces} for debugging. */
82
+ export interface NetworkInterfaceInfo {
83
+ /** Interface name (e.g. `en0`, `wlan0`, `bridge100`, `ap0`, `pdp_ip0`). */
84
+ name: string;
85
+ /** The interface's IPv4 address. */
86
+ ip: string;
87
+ /** Dotted IPv4 subnet mask, when known. */
88
+ netmask: string | null;
89
+ /** Best-effort role classification of the interface. */
90
+ role: 'wifi' | 'hotspot' | 'cellular' | 'ethernet' | 'other';
91
+ }
92
+ /** Native event map for the module. */
93
+ export type LocalNetworkInfoModuleEvents = {
94
+ /** Fired whenever the device's network position changes. */
95
+ onNetworkChange: (info: LocalIpInfo) => void;
96
+ };
97
+ //# sourceMappingURL=LocalNetworkInfo.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfo.types.d.ts","sourceRoot":"","sources":["../src/LocalNetworkInfo.types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtD,mDAAmD;AACnD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAElB,8EAA8E;IAC9E,IAAI,EAAE,WAAW,CAAC;IAElB,mEAAmE;IACnE,eAAe,EAAE,OAAO,CAAC;IAEzB,mFAAmF;IACnF,aAAa,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7B,gFAAgF;IAChF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvB;;;;;;;;;OASG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAEvB;;;;;;;;OAQG;IACH,oBAAoB,EAAE,aAAa,GAAG,IAAI,CAAC;IAE3C,4CAA4C;IAC5C,QAAQ,EAAE,cAAc,CAAC;IAEzB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;CAC9D;AAED,uCAAuC;AACvC,MAAM,MAAM,4BAA4B,GAAG;IACzC,4DAA4D;IAC5D,eAAe,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=LocalNetworkInfo.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfo.types.js","sourceRoot":"","sources":["../src/LocalNetworkInfo.types.ts"],"names":[],"mappings":""}