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.
@@ -0,0 +1,11 @@
1
+ import { NativeModule } from 'expo';
2
+ import { LocalNetworkInfoModuleEvents, LocalIpInfo, NetworkInterfaceInfo } from './LocalNetworkInfo.types';
3
+ declare class LocalNetworkInfoModule extends NativeModule<LocalNetworkInfoModuleEvents> {
4
+ /** Capture the current local-IP/network-role snapshot. */
5
+ getLocalIpAsync(): Promise<LocalIpInfo>;
6
+ /** Enumerate all up, non-loopback IPv4 interfaces (for debugging/inspection). */
7
+ getAllInterfacesAsync(): Promise<NetworkInterfaceInfo[]>;
8
+ }
9
+ declare const _default: LocalNetworkInfoModule;
10
+ export default _default;
11
+ //# sourceMappingURL=LocalNetworkInfoModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfoModule.d.ts","sourceRoot":"","sources":["../src/LocalNetworkInfoModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAElC,OAAO,OAAO,sBAAuB,SAAQ,YAAY,CAAC,4BAA4B,CAAC;IACrF,0DAA0D;IAC1D,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC;IACvC,iFAAiF;IACjF,qBAAqB,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;CACzD;;AAGD,wBAA+E"}
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo';
2
+ // The string must match `Name("LocalNetworkInfo")` in the Swift/Kotlin modules.
3
+ export default requireNativeModule('LocalNetworkInfo');
4
+ //# sourceMappingURL=LocalNetworkInfoModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfoModule.js","sourceRoot":"","sources":["../src/LocalNetworkInfoModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAezD,gFAAgF;AAChF,eAAe,mBAAmB,CAAyB,kBAAkB,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { NativeModule } from 'expo';
2
+ import { LocalNetworkInfoModuleEvents, LocalIpInfo, NetworkInterfaceInfo } from './LocalNetworkInfo.types';
3
+ /**
4
+ * Web fallback. Browsers do not expose the device's local LAN IPv4, so this
5
+ * reports `role: 'none'` with a `null` IP. It exists so the package imports
6
+ * cleanly on web (Metro resolves `*.web.ts`) and the API surface stays stable.
7
+ */
8
+ declare class LocalNetworkInfoModule extends NativeModule<LocalNetworkInfoModuleEvents> {
9
+ getLocalIpAsync(): Promise<LocalIpInfo>;
10
+ getAllInterfacesAsync(): Promise<NetworkInterfaceInfo[]>;
11
+ }
12
+ declare const _default: typeof LocalNetworkInfoModule;
13
+ export default _default;
14
+ //# sourceMappingURL=LocalNetworkInfoModule.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfoModule.web.d.ts","sourceRoot":"","sources":["../src/LocalNetworkInfoModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,YAAY,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAElC;;;;GAIG;AACH,cAAM,sBAAuB,SAAQ,YAAY,CAAC,4BAA4B,CAAC;IACvE,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC;IAevC,qBAAqB,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;CAG/D;;AAED,wBAAmF"}
@@ -0,0 +1,27 @@
1
+ import { registerWebModule, NativeModule } from 'expo';
2
+ /**
3
+ * Web fallback. Browsers do not expose the device's local LAN IPv4, so this
4
+ * reports `role: 'none'` with a `null` IP. It exists so the package imports
5
+ * cleanly on web (Metro resolves `*.web.ts`) and the API surface stays stable.
6
+ */
7
+ class LocalNetworkInfoModule extends NativeModule {
8
+ async getLocalIpAsync() {
9
+ return {
10
+ ip: null,
11
+ role: 'none',
12
+ isWifiConnected: false,
13
+ isHotspotHost: false,
14
+ interfaceName: null,
15
+ netmask: null,
16
+ gateway: null,
17
+ predictedClientRange: null,
18
+ platform: 'web',
19
+ timestamp: Date.now(),
20
+ };
21
+ }
22
+ async getAllInterfacesAsync() {
23
+ return [];
24
+ }
25
+ }
26
+ export default registerWebModule(LocalNetworkInfoModule, 'LocalNetworkInfoModule');
27
+ //# sourceMappingURL=LocalNetworkInfoModule.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalNetworkInfoModule.web.js","sourceRoot":"","sources":["../src/LocalNetworkInfoModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAQvD;;;;GAIG;AACH,MAAM,sBAAuB,SAAQ,YAA0C;IAC7E,KAAK,CAAC,eAAe;QACnB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,MAAM;YACZ,eAAe,EAAE,KAAK;YACtB,aAAa,EAAE,KAAK;YACpB,aAAa,EAAE,IAAI;YACnB,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,oBAAoB,EAAE,IAAI;YAC1B,QAAQ,EAAE,KAAK;YACf,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,qBAAqB;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { EventSubscription } from 'expo-modules-core';
2
+ import { LocalIpInfo, NetworkInterfaceInfo } from './LocalNetworkInfo.types';
3
+ /**
4
+ * Capture the device's current local-IP / network-role snapshot.
5
+ *
6
+ * Resolves with WiFi-station info when connected to WiFi; otherwise hotspot-host
7
+ * info when the device is sharing its connection; otherwise `role: 'none'`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const info = await getLocalIp();
12
+ * if (info.role === 'wifi') console.log('On WiFi at', info.ip);
13
+ * if (info.role === 'hotspot') console.log('Hotspot host at', info.ip);
14
+ * ```
15
+ */
16
+ export declare function getLocalIp(): Promise<LocalIpInfo>;
17
+ /**
18
+ * Enumerate every up, non-loopback IPv4 interface with a best-effort role
19
+ * classification. Useful for debugging odd devices/vendors.
20
+ */
21
+ export declare function getAllInterfaces(): Promise<NetworkInterfaceInfo[]>;
22
+ /**
23
+ * Subscribe to network changes. The listener fires with a fresh
24
+ * {@link LocalIpInfo} whenever connectivity, WiFi, or hotspot state changes.
25
+ *
26
+ * Remember to call `.remove()` on the returned subscription to avoid leaks
27
+ * (or use {@link useLocalIp}, which manages this for you).
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const sub = addNetworkChangeListener((info) => {
32
+ * console.log('network changed →', info.role, info.ip);
33
+ * });
34
+ * // later
35
+ * sub.remove();
36
+ * ```
37
+ */
38
+ export declare function addNetworkChangeListener(listener: (info: LocalIpInfo) => void): EventSubscription;
39
+ /** The raw native module instance (advanced use). */
40
+ export { default as LocalNetworkInfoModule } from './LocalNetworkInfoModule';
41
+ export { useLocalIp } from './useLocalIp';
42
+ export * from './LocalNetworkInfo.types';
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAE7E;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,CAEvD;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAExE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GACpC,iBAAiB,CAEnB;AAED,qDAAqD;AACrD,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,cAAc,0BAA0B,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import LocalNetworkInfoModule from './LocalNetworkInfoModule';
2
+ /**
3
+ * Capture the device's current local-IP / network-role snapshot.
4
+ *
5
+ * Resolves with WiFi-station info when connected to WiFi; otherwise hotspot-host
6
+ * info when the device is sharing its connection; otherwise `role: 'none'`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const info = await getLocalIp();
11
+ * if (info.role === 'wifi') console.log('On WiFi at', info.ip);
12
+ * if (info.role === 'hotspot') console.log('Hotspot host at', info.ip);
13
+ * ```
14
+ */
15
+ export async function getLocalIp() {
16
+ return LocalNetworkInfoModule.getLocalIpAsync();
17
+ }
18
+ /**
19
+ * Enumerate every up, non-loopback IPv4 interface with a best-effort role
20
+ * classification. Useful for debugging odd devices/vendors.
21
+ */
22
+ export async function getAllInterfaces() {
23
+ return LocalNetworkInfoModule.getAllInterfacesAsync();
24
+ }
25
+ /**
26
+ * Subscribe to network changes. The listener fires with a fresh
27
+ * {@link LocalIpInfo} whenever connectivity, WiFi, or hotspot state changes.
28
+ *
29
+ * Remember to call `.remove()` on the returned subscription to avoid leaks
30
+ * (or use {@link useLocalIp}, which manages this for you).
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const sub = addNetworkChangeListener((info) => {
35
+ * console.log('network changed →', info.role, info.ip);
36
+ * });
37
+ * // later
38
+ * sub.remove();
39
+ * ```
40
+ */
41
+ export function addNetworkChangeListener(listener) {
42
+ return LocalNetworkInfoModule.addListener('onNetworkChange', listener);
43
+ }
44
+ /** The raw native module instance (advanced use). */
45
+ export { default as LocalNetworkInfoModule } from './LocalNetworkInfoModule';
46
+ export { useLocalIp } from './useLocalIp';
47
+ export * from './LocalNetworkInfo.types';
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAG9D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,OAAO,sBAAsB,CAAC,eAAe,EAAE,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,sBAAsB,CAAC,qBAAqB,EAAE,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAqC;IAErC,OAAO,sBAAsB,CAAC,WAAW,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;AACzE,CAAC;AAED,qDAAqD;AACrD,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { LocalIpInfo } from './LocalNetworkInfo.types';
2
+ /**
3
+ * React hook that returns the latest {@link LocalIpInfo} and keeps it updated as
4
+ * the network changes.
5
+ *
6
+ * It captures an initial snapshot on mount and then subscribes to native
7
+ * `onNetworkChange` events, cleaning up the subscription on unmount.
8
+ *
9
+ * @returns the latest snapshot, or `null` until the first snapshot resolves.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const info = useLocalIp();
14
+ * return <Text>{info?.ip ?? 'detecting…'} ({info?.role})</Text>;
15
+ * ```
16
+ */
17
+ export declare function useLocalIp(): LocalIpInfo | null;
18
+ //# sourceMappingURL=useLocalIp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLocalIp.d.ts","sourceRoot":"","sources":["../src/useLocalIp.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,IAAI,WAAW,GAAG,IAAI,CA6B/C"}
@@ -0,0 +1,43 @@
1
+ import { useEffect, useState } from 'react';
2
+ import LocalNetworkInfoModule from './LocalNetworkInfoModule';
3
+ /**
4
+ * React hook that returns the latest {@link LocalIpInfo} and keeps it updated as
5
+ * the network changes.
6
+ *
7
+ * It captures an initial snapshot on mount and then subscribes to native
8
+ * `onNetworkChange` events, cleaning up the subscription on unmount.
9
+ *
10
+ * @returns the latest snapshot, or `null` until the first snapshot resolves.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const info = useLocalIp();
15
+ * return <Text>{info?.ip ?? 'detecting…'} ({info?.role})</Text>;
16
+ * ```
17
+ */
18
+ export function useLocalIp() {
19
+ const [info, setInfo] = useState(null);
20
+ useEffect(() => {
21
+ let mounted = true;
22
+ LocalNetworkInfoModule.getLocalIpAsync()
23
+ .then((snapshot) => {
24
+ if (mounted) {
25
+ setInfo(snapshot);
26
+ }
27
+ })
28
+ .catch(() => {
29
+ // Swallow — a change event or a later manual refresh will populate it.
30
+ });
31
+ const subscription = LocalNetworkInfoModule.addListener('onNetworkChange', (snapshot) => {
32
+ if (mounted) {
33
+ setInfo(snapshot);
34
+ }
35
+ });
36
+ return () => {
37
+ mounted = false;
38
+ subscription.remove();
39
+ };
40
+ }, []);
41
+ return info;
42
+ }
43
+ //# sourceMappingURL=useLocalIp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLocalIp.js","sourceRoot":"","sources":["../src/useLocalIp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,sBAAsB,MAAM,0BAA0B,CAAC;AAG9D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC,CAAC;IAE3D,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,GAAG,IAAI,CAAC;QAEnB,sBAAsB,CAAC,eAAe,EAAE;aACrC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACjB,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,uEAAuE;QACzE,CAAC,CAAC,CAAC;QAEL,MAAM,YAAY,GAAG,sBAAsB,CAAC,WAAW,CAAC,iBAAiB,EAAE,CAAC,QAAQ,EAAE,EAAE;YACtF,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,QAAQ,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,OAAO,GAAG,KAAK,CAAC;YAChB,YAAY,CAAC,MAAM,EAAE,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android", "web"],
3
+ "apple": {
4
+ "modules": ["LocalNetworkInfoModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["com.localnetworkinfo.LocalNetworkInfoModule"]
8
+ }
9
+ }
@@ -0,0 +1,29 @@
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 = 'LocalNetworkInfo'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.9'
18
+ s.source = { git: 'https://github.com/daboss2003/react-native-local-network-info' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ # Swift/Objective-C compatibility
24
+ s.pod_target_xcconfig = {
25
+ 'DEFINES_MODULE' => 'YES'
26
+ }
27
+
28
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
29
+ end
@@ -0,0 +1,241 @@
1
+ import ExpoModulesCore
2
+ import Network
3
+
4
+ public class LocalNetworkInfoModule: Module {
5
+ private var monitor: NWPathMonitor?
6
+ private let monitorQueue = DispatchQueue(label: "expo.modules.localnetworkinfo.monitor")
7
+
8
+ public func definition() -> ModuleDefinition {
9
+ Name("LocalNetworkInfo")
10
+
11
+ Events("onNetworkChange")
12
+
13
+ AsyncFunction("getLocalIpAsync") { () -> [String: Any?] in
14
+ return LocalNetworkInfoModule.buildInfo()
15
+ }
16
+
17
+ AsyncFunction("getAllInterfacesAsync") { () -> [[String: Any?]] in
18
+ return LocalNetworkInfoModule.enumerateInterfaces().map { $0.toDictionary() }
19
+ }
20
+
21
+ OnStartObserving {
22
+ self.startMonitoring()
23
+ }
24
+
25
+ OnStopObserving {
26
+ self.stopMonitoring()
27
+ }
28
+
29
+ OnDestroy {
30
+ self.stopMonitoring()
31
+ }
32
+ }
33
+
34
+ // MARK: - Network change monitoring
35
+
36
+ private func startMonitoring() {
37
+ guard monitor == nil else { return }
38
+ let pathMonitor = NWPathMonitor()
39
+ pathMonitor.pathUpdateHandler = { [weak self] _ in
40
+ self?.emitChange()
41
+ }
42
+ pathMonitor.start(queue: monitorQueue)
43
+ monitor = pathMonitor
44
+ }
45
+
46
+ private func stopMonitoring() {
47
+ monitor?.cancel()
48
+ monitor = nil
49
+ }
50
+
51
+ /// Emit an immediate snapshot, then re-sample shortly after: the hotspot
52
+ /// (`bridge100`) interface can receive its address a moment after the path
53
+ /// change fires, and no follow-up path event is guaranteed.
54
+ private func emitChange() {
55
+ sendEvent("onNetworkChange", LocalNetworkInfoModule.buildInfo())
56
+ for delay in [0.7, 1.8] {
57
+ monitorQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
58
+ self?.sendEvent("onNetworkChange", LocalNetworkInfoModule.buildInfo())
59
+ }
60
+ }
61
+ }
62
+
63
+ // MARK: - Interface model
64
+
65
+ private enum Role: String {
66
+ case wifi
67
+ case hotspot
68
+ case cellular
69
+ case ethernet
70
+ case other
71
+ }
72
+
73
+ private struct Interface {
74
+ let name: String
75
+ let ipv4: String
76
+ let netmask: String
77
+ let role: Role
78
+
79
+ /// Reliable Personal Hotspot host fingerprint: iOS always assigns the host
80
+ /// 172.20.10.1 / 255.255.255.240 on a `bridge*` interface.
81
+ var isHotspotHost: Bool {
82
+ return name.hasPrefix("bridge")
83
+ && ipv4 == "172.20.10.1"
84
+ && netmask == "255.255.255.240"
85
+ }
86
+
87
+ func toDictionary() -> [String: Any?] {
88
+ return [
89
+ "name": name,
90
+ "ip": ipv4,
91
+ "netmask": netmask.isEmpty ? nil : netmask,
92
+ "role": role.rawValue,
93
+ ]
94
+ }
95
+ }
96
+
97
+ // MARK: - Detection
98
+
99
+ /// Classify an interface by its (de-facto, non-API) name. Heuristic, paired
100
+ /// with the 172.20.10.1 fingerprint for reliable hotspot-host detection.
101
+ private static func classify(_ name: String) -> Role {
102
+ if name.hasPrefix("bridge") { return .hotspot } // bridge100 = Personal Hotspot host
103
+ if name == "en0" { return .wifi } // primary WiFi station
104
+ if name.hasPrefix("pdp_ip") { return .cellular } // pdp_ip0 = cellular
105
+ if name.hasPrefix("en") { return .ethernet } // en1/en2 = wired adapters
106
+ return .other // awdl0, llw0, utun*, ap1, lo0, ...
107
+ }
108
+
109
+ /// Enumerate up, non-loopback IPv4 interfaces using getifaddrs(3).
110
+ private static func enumerateInterfaces() -> [Interface] {
111
+ var result: [Interface] = []
112
+
113
+ var ifaddrPtr: UnsafeMutablePointer<ifaddrs>?
114
+ guard getifaddrs(&ifaddrPtr) == 0, let first = ifaddrPtr else { return result }
115
+ defer { freeifaddrs(ifaddrPtr) }
116
+
117
+ for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
118
+ let ifa = ptr.pointee
119
+
120
+ guard let addr = ifa.ifa_addr else { continue }
121
+ guard addr.pointee.sa_family == UInt8(AF_INET) else { continue }
122
+
123
+ let flags = Int32(ifa.ifa_flags)
124
+ guard (flags & IFF_UP) == IFF_UP, (flags & IFF_LOOPBACK) == 0 else { continue }
125
+
126
+ let name = String(cString: ifa.ifa_name)
127
+
128
+ // Use sa_len so the call is correct for AF_INET (16 bytes).
129
+ var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
130
+ guard getnameinfo(addr, socklen_t(addr.pointee.sa_len),
131
+ &hostBuffer, socklen_t(hostBuffer.count),
132
+ nil, 0, NI_NUMERICHOST) == 0 else { continue }
133
+ let ip = String(cString: hostBuffer)
134
+
135
+ // Skip link-local AirDrop/Continuity addresses (awdl0/llw0 etc).
136
+ if ip.hasPrefix("169.254.") { continue }
137
+
138
+ var netmask = ""
139
+ if let nm = ifa.ifa_netmask {
140
+ var maskBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
141
+ if getnameinfo(nm, socklen_t(nm.pointee.sa_len),
142
+ &maskBuffer, socklen_t(maskBuffer.count),
143
+ nil, 0, NI_NUMERICHOST) == 0 {
144
+ netmask = String(cString: maskBuffer)
145
+ }
146
+ }
147
+
148
+ result.append(Interface(name: name, ipv4: ip, netmask: netmask, role: classify(name)))
149
+ }
150
+
151
+ return result
152
+ }
153
+
154
+ /// Build a `LocalIpInfo`-shaped dictionary, applying WiFi-over-hotspot precedence.
155
+ private static func buildInfo() -> [String: Any?] {
156
+ let interfaces = enumerateInterfaces()
157
+
158
+ let station = interfaces.first { $0.role == .wifi }
159
+ let host = interfaces.first { $0.isHotspotHost } ?? interfaces.first { $0.role == .hotspot }
160
+
161
+ let isWifiConnected = station != nil
162
+ let isHotspotHost = host != nil
163
+
164
+ var ip: String?
165
+ var role = "none"
166
+ var interfaceName: String?
167
+ var netmask: String?
168
+ var gateway: String?
169
+ var predicted: [String: String]?
170
+
171
+ if let station = station {
172
+ ip = station.ipv4
173
+ role = "wifi"
174
+ interfaceName = station.name
175
+ netmask = station.netmask.isEmpty ? nil : station.netmask
176
+ gateway = deriveGateway(ip: station.ipv4, netmask: station.netmask)
177
+ } else if let host = host {
178
+ ip = host.ipv4
179
+ role = "hotspot"
180
+ interfaceName = host.name
181
+ netmask = host.netmask.isEmpty ? nil : host.netmask
182
+ gateway = host.ipv4 // the hotspot host is its own gateway
183
+ predicted = predictClientRange(hostIp: host.ipv4, netmask: host.netmask)
184
+ }
185
+
186
+ return [
187
+ "ip": ip,
188
+ "role": role,
189
+ "isWifiConnected": isWifiConnected,
190
+ "isHotspotHost": isHotspotHost,
191
+ "interfaceName": interfaceName,
192
+ "netmask": netmask,
193
+ "gateway": gateway,
194
+ "predictedClientRange": predicted,
195
+ "platform": "ios",
196
+ "timestamp": Date().timeIntervalSince1970 * 1000,
197
+ ]
198
+ }
199
+
200
+ // MARK: - IPv4 math
201
+
202
+ private static func ipToInt(_ ip: String) -> UInt32? {
203
+ let parts = ip.split(separator: ".")
204
+ guard parts.count == 4 else { return nil }
205
+ var value: UInt32 = 0
206
+ for part in parts {
207
+ guard let octet = UInt32(part), octet <= 255 else { return nil }
208
+ value = (value << 8) | octet
209
+ }
210
+ return value
211
+ }
212
+
213
+ private static func intToIp(_ value: UInt32) -> String {
214
+ return "\((value >> 24) & 0xff).\((value >> 16) & 0xff).\((value >> 8) & 0xff).\(value & 0xff)"
215
+ }
216
+
217
+ /// Best-effort gateway guess for a station: the first host of the subnet.
218
+ /// iOS exposes no public default-gateway API, so this is a heuristic.
219
+ private static func deriveGateway(ip: String, netmask: String) -> String? {
220
+ guard let ipInt = ipToInt(ip), let maskInt = ipToInt(netmask), maskInt != 0 else { return nil }
221
+ let network = ipInt & maskInt
222
+ return intToIp(network + 1)
223
+ }
224
+
225
+ /// Predict the usable client IPv4 range of a hotspot subnet (network+1 ..
226
+ /// broadcast-1). The host occupies one address within this range unless it is
227
+ /// the first host, in which case the range starts at the next address.
228
+ private static func predictClientRange(hostIp: String, netmask: String) -> [String: String]? {
229
+ guard let ipInt = ipToInt(hostIp), let maskInt = ipToInt(netmask), maskInt != 0 else { return nil }
230
+ let network = ipInt & maskInt
231
+ let broadcast = network | ~maskInt
232
+ guard broadcast > network + 1 else { return nil } // needs at least 2 usable hosts
233
+
234
+ var first = network + 1
235
+ if first == ipInt { first += 1 } // skip the host itself
236
+ let last = broadcast - 1
237
+ guard last >= first else { return nil }
238
+
239
+ return ["first": intToIp(first), "last": intToIp(last)]
240
+ }
241
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "react-native-local-network-info",
3
+ "version": "1.0.0",
4
+ "description": "Read a React Native device's own local IPv4 address and whether it is connected to WiFi or acting as a hotspot host, with live network-change events. Built with the Expo Modules API; works in bare React Native and Expo.",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "clean": "rm -rf build",
10
+ "prepare": "tsc",
11
+ "prepublishOnly": "tsc"
12
+ },
13
+ "keywords": [
14
+ "react-native",
15
+ "expo",
16
+ "expo-module",
17
+ "ip",
18
+ "local-ip",
19
+ "ipv4",
20
+ "network",
21
+ "wifi",
22
+ "hotspot",
23
+ "tethering",
24
+ "softap",
25
+ "gateway"
26
+ ],
27
+ "license": "MIT",
28
+ "author": "daboss2003",
29
+ "homepage": "https://github.com/daboss2003/react-native-local-network-info#readme",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/daboss2003/react-native-local-network-info"
33
+ },
34
+ "files": [
35
+ "build",
36
+ "android",
37
+ "ios",
38
+ "src",
39
+ "expo-module.config.json",
40
+ "!android/build",
41
+ "!android/.gradle",
42
+ "!android/.cxx",
43
+ "!ios/build",
44
+ "!ios/Pods",
45
+ "!**/*.xcworkspace",
46
+ "!**/__tests__",
47
+ "!**/__mocks__"
48
+ ],
49
+ "peerDependencies": {
50
+ "expo": "*",
51
+ "react": "*",
52
+ "react-native": "*"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^19.2.0",
56
+ "expo": "^56.0.0",
57
+ "react": "*",
58
+ "react-native": "*",
59
+ "typescript": "^5.9.0"
60
+ }
61
+ }