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 +21 -0
- package/README.md +158 -0
- package/android/build.gradle +20 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/com/localnetworkinfo/LocalNetworkInfoModule.kt +371 -0
- package/build/LocalNetworkInfo.types.d.ts +97 -0
- package/build/LocalNetworkInfo.types.d.ts.map +1 -0
- package/build/LocalNetworkInfo.types.js +2 -0
- package/build/LocalNetworkInfo.types.js.map +1 -0
- package/build/LocalNetworkInfoModule.d.ts +11 -0
- package/build/LocalNetworkInfoModule.d.ts.map +1 -0
- package/build/LocalNetworkInfoModule.js +4 -0
- package/build/LocalNetworkInfoModule.js.map +1 -0
- package/build/LocalNetworkInfoModule.web.d.ts +14 -0
- package/build/LocalNetworkInfoModule.web.d.ts.map +1 -0
- package/build/LocalNetworkInfoModule.web.js +27 -0
- package/build/LocalNetworkInfoModule.web.js.map +1 -0
- package/build/index.d.ts +43 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +48 -0
- package/build/index.js.map +1 -0
- package/build/useLocalIp.d.ts +18 -0
- package/build/useLocalIp.d.ts.map +1 -0
- package/build/useLocalIp.js +43 -0
- package/build/useLocalIp.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/LocalNetworkInfo.podspec +29 -0
- package/ios/LocalNetworkInfoModule.swift +241 -0
- package/package.json +61 -0
- package/src/LocalNetworkInfo.types.ts +110 -0
- package/src/LocalNetworkInfoModule.ts +17 -0
- package/src/LocalNetworkInfoModule.web.ts +35 -0
- package/src/index.ts +57 -0
- package/src/useLocalIp.ts +50 -0
|
@@ -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 @@
|
|
|
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"}
|
package/build/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|