@symbiote-native/network 0.0.1

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/network-state.service/index.d.ts +6 -0
  6. package/build/angular/services/network-state.service/index.js +68 -0
  7. package/build/core/index.d.ts +3 -0
  8. package/build/core/index.js +2 -0
  9. package/build/core/native-module.d.ts +9 -0
  10. package/build/core/native-module.js +3 -0
  11. package/build/core/network.d.ts +13 -0
  12. package/build/core/network.js +34 -0
  13. package/build/core/types.d.ts +55 -0
  14. package/build/core/types.js +40 -0
  15. package/build/react/hooks/use-network-state/index.d.ts +2 -0
  16. package/build/react/hooks/use-network-state/index.js +14 -0
  17. package/build/react/index.d.ts +2 -0
  18. package/build/react/index.js +3 -0
  19. package/build/vue/composables/use-network-state/index.d.ts +3 -0
  20. package/build/vue/composables/use-network-state/index.js +21 -0
  21. package/build/vue/index.d.ts +2 -0
  22. package/build/vue/index.js +3 -0
  23. package/build-ngc/angular/index.d.ts +2 -0
  24. package/build-ngc/angular/index.js +5 -0
  25. package/build-ngc/angular/index.js.map +1 -0
  26. package/build-ngc/angular/services/network-state.service/index.d.ts +9 -0
  27. package/build-ngc/angular/services/network-state.service/index.js +27 -0
  28. package/build-ngc/angular/services/network-state.service/index.js.map +1 -0
  29. package/build-ngc/core/index.d.ts +3 -0
  30. package/build-ngc/core/index.js +3 -0
  31. package/build-ngc/core/index.js.map +1 -0
  32. package/build-ngc/core/native-module.d.ts +9 -0
  33. package/build-ngc/core/native-module.js +4 -0
  34. package/build-ngc/core/native-module.js.map +1 -0
  35. package/build-ngc/core/network.d.ts +13 -0
  36. package/build-ngc/core/network.js +35 -0
  37. package/build-ngc/core/network.js.map +1 -0
  38. package/build-ngc/core/types.d.ts +55 -0
  39. package/build-ngc/core/types.js +41 -0
  40. package/build-ngc/core/types.js.map +1 -0
  41. package/native-link.json +12 -0
  42. package/package.json +107 -0
  43. package/src/angular/index.ts +13 -0
  44. package/src/angular/services/network-state.service/index.ts +26 -0
  45. package/src/angular/services/network-state.service/network-state.service.test.ts +103 -0
  46. package/src/core/index.ts +8 -0
  47. package/src/core/native-module.ts +19 -0
  48. package/src/core/network.test.ts +103 -0
  49. package/src/core/network.ts +42 -0
  50. package/src/core/types.ts +60 -0
  51. package/src/react/hooks/use-network-state/index.ts +17 -0
  52. package/src/react/hooks/use-network-state/use-network-state.test.tsx +91 -0
  53. package/src/react/index.ts +13 -0
  54. package/src/vue/composables/use-network-state/index.ts +31 -0
  55. package/src/vue/composables/use-network-state/use-network-state.test.ts +100 -0
  56. package/src/vue/index.ts +13 -0
@@ -0,0 +1,13 @@
1
+ import { type EventSubscription } from 'expo-modules-core';
2
+ import type { NetworkState, NetworkStateEvent } from './types';
3
+ /** Gets the device's current network connection state. */
4
+ export declare function getNetworkStateAsync(): Promise<NetworkState>;
5
+ /** Gets the device's current IPv4 address. Returns `"0.0.0.0"` if it could not be retrieved. */
6
+ export declare function getIpAddressAsync(): Promise<string>;
7
+ /**
8
+ * Tells if the device is in airplane mode.
9
+ * @platform android
10
+ */
11
+ export declare function isAirplaneModeEnabledAsync(): Promise<boolean>;
12
+ /** Subscribe to network state changes (connection type, connected, internet reachable). */
13
+ export declare function addNetworkStateListener(listener: (event: NetworkStateEvent) => void): EventSubscription;
@@ -0,0 +1,35 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-network/src/Network.ts (sdk-57). Unlike
2
+ // battery, upstream Network throws UnavailabilityError when the native method is absent —
3
+ // same convention as packages/haptics/src/core/haptics.ts.
4
+ import { UnavailabilityError } from 'expo-modules-core';
5
+ import { expoNetwork } from './native-module';
6
+ const NATIVE_MODULE_NAME = 'expo-network';
7
+ /** Gets the device's current network connection state. */
8
+ export async function getNetworkStateAsync() {
9
+ if (!expoNetwork.getNetworkStateAsync) {
10
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getNetworkStateAsync');
11
+ }
12
+ return expoNetwork.getNetworkStateAsync();
13
+ }
14
+ /** Gets the device's current IPv4 address. Returns `"0.0.0.0"` if it could not be retrieved. */
15
+ export async function getIpAddressAsync() {
16
+ if (!expoNetwork.getIpAddressAsync) {
17
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getIpAddressAsync');
18
+ }
19
+ return expoNetwork.getIpAddressAsync();
20
+ }
21
+ /**
22
+ * Tells if the device is in airplane mode.
23
+ * @platform android
24
+ */
25
+ export async function isAirplaneModeEnabledAsync() {
26
+ if (!expoNetwork.isAirplaneModeEnabledAsync) {
27
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isAirplaneModeEnabledAsync');
28
+ }
29
+ return expoNetwork.isAirplaneModeEnabledAsync();
30
+ }
31
+ /** Subscribe to network state changes (connection type, connected, internet reachable). */
32
+ export function addNetworkStateListener(listener) {
33
+ return expoNetwork.addListener('onNetworkStateChanged', listener);
34
+ }
35
+ //# sourceMappingURL=network.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/core/network.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,0FAA0F;AAC1F,2DAA2D;AAC3D,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,IAAI,CAAC,WAAW,CAAC,oBAAoB,EAAE,CAAC;QACtC,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,sBAAsB,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,WAAW,CAAC,oBAAoB,EAAE,CAAC;AAC5C,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,CAAC,WAAW,CAAC,iBAAiB,EAAE,CAAC;QACnC,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,mBAAmB,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,WAAW,CAAC,iBAAiB,EAAE,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B;IAC9C,IAAI,CAAC,WAAW,CAAC,0BAA0B,EAAE,CAAC;QAC5C,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,4BAA4B,CAAC,CAAC;IAClF,CAAC;IACD,OAAO,WAAW,CAAC,0BAA0B,EAAE,CAAC;AAClD,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,uBAAuB,CACrC,QAA4C;IAE5C,OAAO,WAAW,CAAC,WAAW,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CAAC;AACpE,CAAC"}
@@ -0,0 +1,55 @@
1
+ export declare enum NetworkStateType {
2
+ /** No active network connection detected. */
3
+ NONE = "NONE",
4
+ /** The connection type could not be determined. */
5
+ UNKNOWN = "UNKNOWN",
6
+ /** Active network connection over mobile data.
7
+ * @platform android
8
+ * @platform ios
9
+ */
10
+ CELLULAR = "CELLULAR",
11
+ /** Active network connection over Wi-Fi.
12
+ * @platform android
13
+ * @platform ios
14
+ */
15
+ WIFI = "WIFI",
16
+ /** Active network connection over Bluetooth.
17
+ * @platform android
18
+ */
19
+ BLUETOOTH = "BLUETOOTH",
20
+ /** Active network connection over Ethernet.
21
+ * @platform android
22
+ * @platform ios
23
+ */
24
+ ETHERNET = "ETHERNET",
25
+ /** Active network connection over WiMAX.
26
+ * @platform android
27
+ */
28
+ WIMAX = "WIMAX",
29
+ /** Active network connection over VPN.
30
+ * @platform android
31
+ */
32
+ VPN = "VPN",
33
+ /** Active network connection over other network connection types.
34
+ * @platform android
35
+ */
36
+ OTHER = "OTHER"
37
+ }
38
+ export type NetworkState = {
39
+ /** The current network connection type. */
40
+ type?: NetworkStateType;
41
+ /**
42
+ * If there is an active network connection. Note that this does not mean internet is
43
+ * reachable. `false` when `type` is `NONE` or `UNKNOWN`, `true` otherwise.
44
+ */
45
+ isConnected?: boolean;
46
+ /**
47
+ * If the internet is reachable with the currently active network connection. On Android this
48
+ * requires internet capability, confirmed internet access, and a usable connection state; VPN
49
+ * connections also require non-zero downstream bandwidth. On iOS this always matches
50
+ * `isConnected`.
51
+ */
52
+ isInternetReachable?: boolean;
53
+ };
54
+ /** Passed as the argument to listeners registered with `addNetworkStateListener()`. */
55
+ export type NetworkStateEvent = NetworkState;
@@ -0,0 +1,41 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-network/src/Network.types.ts (sdk-57) — plain
2
+ // data shapes, no `expo` meta-package import to swap out (unlike local-auth's types.ts).
3
+ export var NetworkStateType;
4
+ (function (NetworkStateType) {
5
+ /** No active network connection detected. */
6
+ NetworkStateType["NONE"] = "NONE";
7
+ /** The connection type could not be determined. */
8
+ NetworkStateType["UNKNOWN"] = "UNKNOWN";
9
+ /** Active network connection over mobile data.
10
+ * @platform android
11
+ * @platform ios
12
+ */
13
+ NetworkStateType["CELLULAR"] = "CELLULAR";
14
+ /** Active network connection over Wi-Fi.
15
+ * @platform android
16
+ * @platform ios
17
+ */
18
+ NetworkStateType["WIFI"] = "WIFI";
19
+ /** Active network connection over Bluetooth.
20
+ * @platform android
21
+ */
22
+ NetworkStateType["BLUETOOTH"] = "BLUETOOTH";
23
+ /** Active network connection over Ethernet.
24
+ * @platform android
25
+ * @platform ios
26
+ */
27
+ NetworkStateType["ETHERNET"] = "ETHERNET";
28
+ /** Active network connection over WiMAX.
29
+ * @platform android
30
+ */
31
+ NetworkStateType["WIMAX"] = "WIMAX";
32
+ /** Active network connection over VPN.
33
+ * @platform android
34
+ */
35
+ NetworkStateType["VPN"] = "VPN";
36
+ /** Active network connection over other network connection types.
37
+ * @platform android
38
+ */
39
+ NetworkStateType["OTHER"] = "OTHER";
40
+ })(NetworkStateType || (NetworkStateType = {}));
41
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,yFAAyF;AAEzF,MAAM,CAAN,IAAY,gBAoCX;AApCD,WAAY,gBAAgB;IAC1B,6CAA6C;IAC7C,iCAAa,CAAA;IACb,mDAAmD;IACnD,uCAAmB,CAAA;IACnB;;;OAGG;IACH,yCAAqB,CAAA;IACrB;;;OAGG;IACH,iCAAa,CAAA;IACb;;OAEG;IACH,2CAAuB,CAAA;IACvB;;;OAGG;IACH,yCAAqB,CAAA;IACrB;;OAEG;IACH,mCAAe,CAAA;IACf;;OAEG;IACH,+BAAW,CAAA;IACX;;OAEG;IACH,mCAAe,CAAA;AACjB,CAAC,EApCW,gBAAgB,KAAhB,gBAAgB,QAoC3B"}
@@ -0,0 +1,12 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-network",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.network.NetworkModule",
7
+ "className": "NetworkModule",
8
+ "nativeName": "ExpoNetwork"
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@symbiote-native/network",
3
+ "version": "0.0.1",
4
+ "description": "expo-network wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Network state (type/connected/internet-reachable) with a change listener, device IP address, and airplane-mode detection.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/network"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/network#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "type": "module",
17
+ "main": "./build/core/index.js",
18
+ "module": "./build/core/index.js",
19
+ "types": "./build/core/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./build/core/index.d.ts",
23
+ "default": "./build/core/index.js"
24
+ },
25
+ "./vue": {
26
+ "types": "./build/vue/index.d.ts",
27
+ "default": "./build/vue/index.js"
28
+ },
29
+ "./react": {
30
+ "types": "./build/react/index.d.ts",
31
+ "default": "./build/react/index.js"
32
+ },
33
+ "./angular": {
34
+ "types": "./build-ngc/angular/index.d.ts",
35
+ "react-native": "./build-ngc/angular/index.js",
36
+ "default": "./src/angular/index.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "src",
41
+ "build",
42
+ "build-ngc",
43
+ "native-link.json"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "expo-network": "57.0.1",
50
+ "expo-modules-core": "57.0.5"
51
+ },
52
+ "peerDependencies": {
53
+ "@symbiote-native/engine": ">=0.1.7",
54
+ "@angular/core": ">=20",
55
+ "@vue/runtime-core": "^3.5.13",
56
+ "react": ">=19.0.0",
57
+ "react-native": ">=0.86",
58
+ "vue": ">=3.5.0",
59
+ "@symbiote-native/angular": "0.6.1",
60
+ "@symbiote-native/react": "0.2.8",
61
+ "@symbiote-native/vue": "0.3.8"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@symbiote-native/angular": {
65
+ "optional": true
66
+ },
67
+ "@symbiote-native/react": {
68
+ "optional": true
69
+ },
70
+ "@symbiote-native/vue": {
71
+ "optional": true
72
+ },
73
+ "@angular/core": {
74
+ "optional": true
75
+ },
76
+ "@vue/runtime-core": {
77
+ "optional": true
78
+ },
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "vue": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "devDependencies": {
87
+ "@angular/compiler": "^22",
88
+ "@angular/compiler-cli": "^22",
89
+ "@angular/core": "^22",
90
+ "@types/node": "^26.0.0",
91
+ "@types/react": "^19.2.0",
92
+ "@vue/runtime-core": "^3.5.13",
93
+ "react": "19.2.3",
94
+ "typescript": "~6.0.0",
95
+ "@symbiote-native/angular": "0.6.1",
96
+ "@symbiote-native/react": "0.2.8",
97
+ "@symbiote-native/engine": "0.1.7",
98
+ "@symbiote-native/vue": "0.3.8",
99
+ "@symbiote-native/test-utils": "0.1.6"
100
+ },
101
+ "scripts": {
102
+ "typecheck": "tsc --build",
103
+ "clean": "rm -rf build-ngc",
104
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
105
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
106
+ }
107
+ }
@@ -0,0 +1,13 @@
1
+ // NetworkStateService is the Angular-only lifecycle half; the free functions and event
2
+ // subscription plumbing all live in core, shared with React/Vue.
3
+ export { NetworkStateService } from './services/network-state.service';
4
+ export {
5
+ getNetworkStateAsync,
6
+ getIpAddressAsync,
7
+ isAirplaneModeEnabledAsync,
8
+ addNetworkStateListener,
9
+ NetworkStateType,
10
+ type NetworkState,
11
+ type NetworkStateEvent,
12
+ type EventSubscription,
13
+ } from '../core';
@@ -0,0 +1,26 @@
1
+ import { effect, inject, Injectable, Injector, signal, type Signal } from '@angular/core';
2
+ import { addNetworkStateListener, getNetworkStateAsync, type NetworkState } from '../../../core';
3
+
4
+ // Angular twin of React's `useNetworkState` hook and Vue's `useNetworkState` composable.
5
+ //
6
+ // readonly networkState = inject(NetworkStateService).connect();
7
+ // // template: {{ networkState().type }}
8
+ @Injectable({ providedIn: 'root' })
9
+ export class NetworkStateService {
10
+ private readonly injector = inject(Injector);
11
+
12
+ connect(): Signal<NetworkState> {
13
+ const networkState = signal<NetworkState>({});
14
+
15
+ effect(
16
+ onCleanup => {
17
+ getNetworkStateAsync().then(state => networkState.set(state));
18
+ const subscription = addNetworkStateListener(event => networkState.set(event));
19
+ onCleanup(() => subscription.remove());
20
+ },
21
+ { injector: this.injector },
22
+ );
23
+
24
+ return networkState.asReadonly();
25
+ }
26
+ }
@@ -0,0 +1,103 @@
1
+ // Co-located Angular-driven test (ADR 0025) for NetworkStateService. See battery's
2
+ // battery-state.service.test.ts for the shared rationale.
3
+
4
+ import '@angular/compiler';
5
+ import { Component, inject, type Signal } from '@angular/core';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { mount, unmount } from '@symbiote-native/angular';
8
+ import { installFabric } from '@symbiote-native/test-utils';
9
+ import { NetworkStateService } from './index';
10
+
11
+ type INetworkState = { type?: string; isConnected?: boolean; isInternetReachable?: boolean };
12
+
13
+ const addListenerMock = vi.fn();
14
+ const removeMock = vi.fn();
15
+ const getNetworkStateAsyncMock = vi.fn(async () => ({
16
+ type: 'WIFI',
17
+ isConnected: true,
18
+ isInternetReachable: true,
19
+ }));
20
+
21
+ vi.mock('../../../core', () => ({
22
+ addNetworkStateListener: (listener: (event: INetworkState) => void) => addListenerMock(listener),
23
+ getNetworkStateAsync: () => getNetworkStateAsyncMock(),
24
+ }));
25
+
26
+ const ROOT_TAG = 973;
27
+ const fabric = installFabric();
28
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
29
+
30
+ let capturedResult: Signal<INetworkState> | undefined;
31
+ let capturedListener: ((event: INetworkState) => void) | undefined;
32
+
33
+ @Component({
34
+ selector: 'symbiote-network-state-host',
35
+ standalone: true,
36
+ template: '',
37
+ })
38
+ class NetworkStateHost {
39
+ readonly networkState = inject(NetworkStateService).connect();
40
+
41
+ constructor() {
42
+ capturedResult = this.networkState;
43
+ }
44
+ }
45
+
46
+ beforeEach(() => {
47
+ capturedResult = undefined;
48
+ capturedListener = undefined;
49
+ getNetworkStateAsyncMock.mockResolvedValue({
50
+ type: 'WIFI',
51
+ isConnected: true,
52
+ isInternetReachable: true,
53
+ });
54
+ addListenerMock.mockImplementation(listener => {
55
+ capturedListener = listener;
56
+ return { remove: removeMock };
57
+ });
58
+ });
59
+
60
+ afterEach(() => {
61
+ unmount(ROOT_TAG);
62
+ fabric.reset();
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ describe('NetworkStateService.connect', () => {
67
+ it('reports an empty object before the initial fetch resolves', async () => {
68
+ mount(ROOT_TAG, NetworkStateHost);
69
+
70
+ expect(capturedResult?.()).toEqual({});
71
+ });
72
+
73
+ it('reports the fetched state once getNetworkStateAsync() resolves', async () => {
74
+ mount(ROOT_TAG, NetworkStateHost);
75
+ await tick();
76
+
77
+ expect(capturedResult?.()).toEqual({
78
+ type: 'WIFI',
79
+ isConnected: true,
80
+ isInternetReachable: true,
81
+ });
82
+ });
83
+
84
+ it('updates the signal when the registered listener fires', async () => {
85
+ mount(ROOT_TAG, NetworkStateHost);
86
+ await tick();
87
+
88
+ if (capturedListener === undefined) throw new Error('addListener callback was not captured');
89
+ capturedListener({ type: 'CELLULAR', isConnected: true, isInternetReachable: false });
90
+
91
+ expect(capturedResult?.()?.type).toBe('CELLULAR');
92
+ });
93
+
94
+ it('removes the subscription when the host component is unmounted', async () => {
95
+ mount(ROOT_TAG, NetworkStateHost);
96
+ await tick();
97
+
98
+ unmount(ROOT_TAG);
99
+ await tick();
100
+
101
+ expect(removeMock).toHaveBeenCalledOnce();
102
+ });
103
+ });
@@ -0,0 +1,8 @@
1
+ export {
2
+ getNetworkStateAsync,
3
+ getIpAddressAsync,
4
+ isAirplaneModeEnabledAsync,
5
+ addNetworkStateListener,
6
+ } from './network';
7
+ export { NetworkStateType, type NetworkState, type NetworkStateEvent } from './types';
8
+ export type { EventSubscription } from 'expo-modules-core';
@@ -0,0 +1,19 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { EventSubscription } from 'expo-modules-core';
3
+ import type { NetworkStateEvent } from './types';
4
+
5
+ const EXPO_NETWORK_MODULE_NAME = 'ExpoNetwork';
6
+
7
+ // Upstream's own event name, verified against .vendors/expo/packages/expo-network/src/Network.ts
8
+ // (sdk-57) — bare `onNetworkStateChanged`, unlike haptics/battery's `Expo.xxx`-namespaced names.
9
+ export type INativeNetworkModule = {
10
+ getNetworkStateAsync?(): Promise<NetworkStateEvent>;
11
+ getIpAddressAsync?(): Promise<string>;
12
+ isAirplaneModeEnabledAsync?(): Promise<boolean>;
13
+ addListener(
14
+ eventName: 'onNetworkStateChanged',
15
+ listener: (event: NetworkStateEvent) => void,
16
+ ): EventSubscription;
17
+ };
18
+
19
+ export const expoNetwork = requireNativeModule<INativeNetworkModule>(EXPO_NETWORK_MODULE_NAME);
@@ -0,0 +1,103 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const FAKE_NATIVE_NETWORK = {
4
+ getNetworkStateAsync: vi.fn(async () => ({
5
+ type: 'WIFI',
6
+ isConnected: true,
7
+ isInternetReachable: true,
8
+ })),
9
+ getIpAddressAsync: vi.fn(async () => '192.168.1.1'),
10
+ isAirplaneModeEnabledAsync: vi.fn(async () => false),
11
+ addListener: vi.fn(() => ({ remove: vi.fn() })),
12
+ };
13
+
14
+ // The real ExpoNetwork native module only exists on device — resolving it via
15
+ // requireNativeModule() at import time would throw in this headless test run, so the
16
+ // module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
17
+ // packages/battery/src/core/battery.test.ts uses for its native-module.
18
+ vi.mock('./native-module', () => ({
19
+ expoNetwork: FAKE_NATIVE_NETWORK,
20
+ }));
21
+
22
+ // expo-modules-core's real entry transitively imports 'react-native' for Platform/
23
+ // TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
24
+ // packages/haptics/src/core/haptics.test.ts uses.
25
+ vi.mock('expo-modules-core', () => ({
26
+ UnavailabilityError: class UnavailabilityError extends Error {
27
+ constructor(moduleName: string, propertyName: string) {
28
+ super(`${propertyName} is not available on ${moduleName}`);
29
+ }
30
+ },
31
+ }));
32
+
33
+ const {
34
+ getNetworkStateAsync,
35
+ getIpAddressAsync,
36
+ isAirplaneModeEnabledAsync,
37
+ addNetworkStateListener,
38
+ } = await import('./network');
39
+
40
+ afterEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ describe('getNetworkStateAsync', () => {
45
+ it('delegates to the native module', async () => {
46
+ await expect(getNetworkStateAsync()).resolves.toEqual({
47
+ type: 'WIFI',
48
+ isConnected: true,
49
+ isInternetReachable: true,
50
+ });
51
+ });
52
+
53
+ it('throws UnavailabilityError when the native method is absent', async () => {
54
+ const { getNetworkStateAsync: native } = FAKE_NATIVE_NETWORK;
55
+ // @ts-expect-error -- simulating a platform where the native module has no such method
56
+ FAKE_NATIVE_NETWORK.getNetworkStateAsync = undefined;
57
+
58
+ await expect(getNetworkStateAsync()).rejects.toThrow();
59
+
60
+ FAKE_NATIVE_NETWORK.getNetworkStateAsync = native;
61
+ });
62
+ });
63
+
64
+ describe('getIpAddressAsync', () => {
65
+ it('delegates to the native module', async () => {
66
+ await expect(getIpAddressAsync()).resolves.toBe('192.168.1.1');
67
+ });
68
+
69
+ it('throws UnavailabilityError when the native method is absent', async () => {
70
+ const { getIpAddressAsync: native } = FAKE_NATIVE_NETWORK;
71
+ // @ts-expect-error -- simulating a platform where the native module has no such method
72
+ FAKE_NATIVE_NETWORK.getIpAddressAsync = undefined;
73
+
74
+ await expect(getIpAddressAsync()).rejects.toThrow();
75
+
76
+ FAKE_NATIVE_NETWORK.getIpAddressAsync = native;
77
+ });
78
+ });
79
+
80
+ describe('isAirplaneModeEnabledAsync', () => {
81
+ it('delegates to the native module', async () => {
82
+ await expect(isAirplaneModeEnabledAsync()).resolves.toBe(false);
83
+ });
84
+
85
+ it('throws UnavailabilityError when the native method is absent', async () => {
86
+ const { isAirplaneModeEnabledAsync: native } = FAKE_NATIVE_NETWORK;
87
+ // @ts-expect-error -- simulating a platform where the native module has no such method
88
+ FAKE_NATIVE_NETWORK.isAirplaneModeEnabledAsync = undefined;
89
+
90
+ await expect(isAirplaneModeEnabledAsync()).rejects.toThrow();
91
+
92
+ FAKE_NATIVE_NETWORK.isAirplaneModeEnabledAsync = native;
93
+ });
94
+ });
95
+
96
+ describe('addNetworkStateListener', () => {
97
+ it('subscribes through the onNetworkStateChanged event name', () => {
98
+ const listener = vi.fn();
99
+ addNetworkStateListener(listener);
100
+
101
+ expect(FAKE_NATIVE_NETWORK.addListener).toHaveBeenCalledWith('onNetworkStateChanged', listener);
102
+ });
103
+ });
@@ -0,0 +1,42 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-network/src/Network.ts (sdk-57). Unlike
2
+ // battery, upstream Network throws UnavailabilityError when the native method is absent —
3
+ // same convention as packages/haptics/src/core/haptics.ts.
4
+ import { UnavailabilityError, type EventSubscription } from 'expo-modules-core';
5
+ import { expoNetwork } from './native-module';
6
+ import type { NetworkState, NetworkStateEvent } from './types';
7
+
8
+ const NATIVE_MODULE_NAME = 'expo-network';
9
+
10
+ /** Gets the device's current network connection state. */
11
+ export async function getNetworkStateAsync(): Promise<NetworkState> {
12
+ if (!expoNetwork.getNetworkStateAsync) {
13
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getNetworkStateAsync');
14
+ }
15
+ return expoNetwork.getNetworkStateAsync();
16
+ }
17
+
18
+ /** Gets the device's current IPv4 address. Returns `"0.0.0.0"` if it could not be retrieved. */
19
+ export async function getIpAddressAsync(): Promise<string> {
20
+ if (!expoNetwork.getIpAddressAsync) {
21
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getIpAddressAsync');
22
+ }
23
+ return expoNetwork.getIpAddressAsync();
24
+ }
25
+
26
+ /**
27
+ * Tells if the device is in airplane mode.
28
+ * @platform android
29
+ */
30
+ export async function isAirplaneModeEnabledAsync(): Promise<boolean> {
31
+ if (!expoNetwork.isAirplaneModeEnabledAsync) {
32
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isAirplaneModeEnabledAsync');
33
+ }
34
+ return expoNetwork.isAirplaneModeEnabledAsync();
35
+ }
36
+
37
+ /** Subscribe to network state changes (connection type, connected, internet reachable). */
38
+ export function addNetworkStateListener(
39
+ listener: (event: NetworkStateEvent) => void,
40
+ ): EventSubscription {
41
+ return expoNetwork.addListener('onNetworkStateChanged', listener);
42
+ }
@@ -0,0 +1,60 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-network/src/Network.types.ts (sdk-57) — plain
2
+ // data shapes, no `expo` meta-package import to swap out (unlike local-auth's types.ts).
3
+
4
+ export enum NetworkStateType {
5
+ /** No active network connection detected. */
6
+ NONE = 'NONE',
7
+ /** The connection type could not be determined. */
8
+ UNKNOWN = 'UNKNOWN',
9
+ /** Active network connection over mobile data.
10
+ * @platform android
11
+ * @platform ios
12
+ */
13
+ CELLULAR = 'CELLULAR',
14
+ /** Active network connection over Wi-Fi.
15
+ * @platform android
16
+ * @platform ios
17
+ */
18
+ WIFI = 'WIFI',
19
+ /** Active network connection over Bluetooth.
20
+ * @platform android
21
+ */
22
+ BLUETOOTH = 'BLUETOOTH',
23
+ /** Active network connection over Ethernet.
24
+ * @platform android
25
+ * @platform ios
26
+ */
27
+ ETHERNET = 'ETHERNET',
28
+ /** Active network connection over WiMAX.
29
+ * @platform android
30
+ */
31
+ WIMAX = 'WIMAX',
32
+ /** Active network connection over VPN.
33
+ * @platform android
34
+ */
35
+ VPN = 'VPN',
36
+ /** Active network connection over other network connection types.
37
+ * @platform android
38
+ */
39
+ OTHER = 'OTHER',
40
+ }
41
+
42
+ export type NetworkState = {
43
+ /** The current network connection type. */
44
+ type?: NetworkStateType;
45
+ /**
46
+ * If there is an active network connection. Note that this does not mean internet is
47
+ * reachable. `false` when `type` is `NONE` or `UNKNOWN`, `true` otherwise.
48
+ */
49
+ isConnected?: boolean;
50
+ /**
51
+ * If the internet is reachable with the currently active network connection. On Android this
52
+ * requires internet capability, confirmed internet access, and a usable connection state; VPN
53
+ * connections also require non-zero downstream bandwidth. On iOS this always matches
54
+ * `isConnected`.
55
+ */
56
+ isInternetReachable?: boolean;
57
+ };
58
+
59
+ /** Passed as the argument to listeners registered with `addNetworkStateListener()`. */
60
+ export type NetworkStateEvent = NetworkState;