rn-system-bar 3.0.3 → 3.1.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/src/SystemBar.ts CHANGED
@@ -1,233 +1,310 @@
1
1
  // ─────────────────────────────────────────────
2
- // rn-system-bar · SystemBar.ts
3
- // v4expo-navigation-bar based, zero deprecated APIs
2
+ // rn-system-bar · SystemBar.ts v5
3
+ // All features Android + iOS
4
4
  // ─────────────────────────────────────────────
5
5
 
6
- import * as NavigationBar from "expo-navigation-bar";
7
- import { NativeModules, Platform, StatusBar } from "react-native";
6
+ import { NativeModules, Platform, StatusBar, Vibration } from "react-native";
8
7
 
9
8
  import type {
9
+ BatteryInfo,
10
+ FontScaleInfo,
11
+ HapticPattern,
10
12
  NavigationBarBehavior,
11
13
  NavigationBarButtonStyle,
12
14
  NavigationBarStyle,
13
15
  NavigationBarVisibility,
16
+ NetworkInfo,
14
17
  Orientation,
18
+ ScreencastInfo,
15
19
  StatusBarStyle,
16
20
  VolumeStream,
17
21
  } from "./types";
18
22
 
19
- const { SystemBar: SystemBarNative } = NativeModules;
23
+ // expo-navigation-bar peer dep, loaded at runtime
24
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
25
+ const NavBar = (() => {
26
+ try { return require("expo-navigation-bar"); }
27
+ catch { return null; }
28
+ })();
29
+
30
+ const { SystemBar: Native } = NativeModules;
20
31
 
21
32
  const isAndroid = Platform.OS === "android";
33
+ const isIOS = Platform.OS === "ios";
22
34
 
23
35
  const androidOnly = (name: string): boolean => {
24
36
  if (!isAndroid) {
25
- if (__DEV__) {
26
- console.warn(
27
- `[rn-system-bar] ${name}() is Android-only and is a no-op on iOS.`,
28
- );
29
- }
37
+ if (__DEV__) console.warn(`[rn-system-bar] ${name}() is Android-only, no-op on iOS.`);
30
38
  return false;
31
39
  }
32
40
  return true;
33
41
  };
34
42
 
35
43
  const checkNative = () => {
36
- if (!SystemBarNative) {
37
- throw new Error(
38
- "[rn-system-bar] Native module not found. Rebuild your Android/iOS project.",
39
- );
40
- }
44
+ if (!Native) throw new Error("[rn-system-bar] Native module not found. Rebuild your project.");
41
45
  };
42
46
 
43
47
  // ═══════════════════════════════════════════════
44
- // NAVIGATION BAR — expo-navigation-bar (Android only)
45
- // Uses WindowInsetsController internally on API 30+.
46
- // Zero deprecated APIs.
48
+ // NAVIGATION BAR (Android — expo-navigation-bar)
47
49
  // ═══════════════════════════════════════════════
48
50
 
49
- /**
50
- * Set navigation bar background color.
51
- * @param color Hex string e.g. "#000000"
52
- * @platform android
53
- */
54
51
  export const setNavigationBarColor = (color: string): Promise<void> => {
55
52
  if (!androidOnly("setNavigationBarColor")) return Promise.resolve();
56
- return NavigationBar.setBackgroundColorAsync(color);
53
+ return NavBar?.setBackgroundColorAsync(color) ?? Promise.resolve();
57
54
  };
58
55
 
59
- /**
60
- * Show or hide the navigation bar.
61
- * @platform android
62
- */
63
- export const setNavigationBarVisibility = (
64
- mode: NavigationBarVisibility,
65
- ): Promise<void> => {
56
+ export const setNavigationBarVisibility = (mode: NavigationBarVisibility): Promise<void> => {
66
57
  if (!androidOnly("setNavigationBarVisibility")) return Promise.resolve();
67
- return NavigationBar.setVisibilityAsync(mode);
58
+ return NavBar?.setVisibilityAsync(mode) ?? Promise.resolve();
68
59
  };
69
60
 
70
- /**
71
- * Set navigation bar icon/button style.
72
- * @param style "light" = white icons | "dark" = black icons
73
- * @platform android
74
- */
75
- export const setNavigationBarButtonStyle = (
76
- style: NavigationBarButtonStyle,
77
- ): Promise<void> => {
61
+ export const setNavigationBarButtonStyle = (style: NavigationBarButtonStyle): Promise<void> => {
78
62
  if (!androidOnly("setNavigationBarButtonStyle")) return Promise.resolve();
79
- return NavigationBar.setButtonStyleAsync(style);
63
+ return NavBar?.setButtonStyleAsync(style) ?? Promise.resolve();
80
64
  };
81
65
 
82
- /**
83
- * Set navigation bar visual style.
84
- * @platform android
85
- */
86
- export const setNavigationBarStyle = (
87
- style: NavigationBarStyle,
88
- ): Promise<void> => {
66
+ export const setNavigationBarStyle = (style: NavigationBarStyle): Promise<void> => {
89
67
  if (!androidOnly("setNavigationBarStyle")) return Promise.resolve();
90
- const buttonStyle: NavigationBarButtonStyle =
91
- style === "dark" || style === "auto" ? "dark" : "light";
92
- return NavigationBar.setButtonStyleAsync(buttonStyle);
68
+ const btn: NavigationBarButtonStyle = style === "dark" || style === "auto" ? "dark" : "light";
69
+ return NavBar?.setButtonStyleAsync(btn) ?? Promise.resolve();
93
70
  };
94
71
 
95
- /**
96
- * Set navigation bar gesture behavior.
97
- * @platform android
98
- */
99
- export const setNavigationBarBehavior = (
100
- behavior: NavigationBarBehavior,
101
- ): Promise<void> => {
72
+ export const setNavigationBarBehavior = (behavior: NavigationBarBehavior): Promise<void> => {
102
73
  if (!androidOnly("setNavigationBarBehavior")) return Promise.resolve();
103
- return NavigationBar.setBehaviorAsync(behavior);
74
+ return NavBar?.setBehaviorAsync(behavior) ?? Promise.resolve();
104
75
  };
105
76
 
106
77
  // ═══════════════════════════════════════════════
107
78
  // STATUS BAR
108
- // React Native StatusBar — cross-platform, no deprecated APIs.
109
79
  // ═══════════════════════════════════════════════
110
80
 
111
- /**
112
- * Set status bar background color.
113
- * @platform android
114
- */
115
81
  export const setStatusBarColor = (color: string, animated = false): void => {
116
82
  if (!androidOnly("setStatusBarColor")) return;
117
83
  StatusBar.setBackgroundColor(color, animated);
118
84
  };
119
85
 
120
- /**
121
- * Set status bar icon style.
122
- * "light" = white icons | "dark" = dark icons
123
- */
124
- export const setStatusBarStyle = (
125
- style: StatusBarStyle,
126
- animated = false,
127
- ): void => {
128
- StatusBar.setBarStyle(
129
- style === "light" ? "light-content" : "dark-content",
130
- animated,
131
- );
86
+ export const setStatusBarStyle = (style: StatusBarStyle, animated = false): void => {
87
+ StatusBar.setBarStyle(style === "light" ? "light-content" : "dark-content", animated);
132
88
  };
133
89
 
134
- /**
135
- * Show or hide the status bar.
136
- */
137
- export const setStatusBarVisibility = (
138
- visible: boolean,
139
- animated = false,
140
- ): void => {
90
+ export const setStatusBarVisibility = (visible: boolean, animated = false): void => {
141
91
  StatusBar.setHidden(!visible, animated ? "slide" : "none");
142
92
  };
143
93
 
144
94
  // ═══════════════════════════════════════════════
145
- // BRIGHTNESS — native module
95
+ // BRIGHTNESS
96
+ // ═══════════════════════════════════════════════
97
+
98
+ export const setBrightness = (level: number): void => {
99
+ checkNative();
100
+ Native.setBrightness(Math.max(0, Math.min(1, level)));
101
+ };
102
+
103
+ export const getBrightness = (): Promise<number> => {
104
+ checkNative();
105
+ return Native.getBrightness();
106
+ };
107
+
108
+ // ═══════════════════════════════════════════════
109
+ // VOLUME
110
+ // ═══════════════════════════════════════════════
111
+
112
+ export const setVolume = (level: number, stream: VolumeStream = "music"): void => {
113
+ checkNative();
114
+ Native.setVolume(Math.max(0, Math.min(1, level)), stream);
115
+ };
116
+
117
+ export const getVolume = (stream: VolumeStream = "music"): Promise<number> => {
118
+ checkNative();
119
+ return Native.getVolume(stream);
120
+ };
121
+
122
+ export const setVolumeHUDVisible = (visible: boolean): void => {
123
+ if (!androidOnly("setVolumeHUDVisible")) return;
124
+ checkNative();
125
+ Native.setVolumeHUDVisible(visible);
126
+ };
127
+
128
+ // ═══════════════════════════════════════════════
129
+ // SCREEN
130
+ // ═══════════════════════════════════════════════
131
+
132
+ export const keepScreenOn = (enable: boolean): void => {
133
+ checkNative();
134
+ Native.keepScreenOn(enable);
135
+ };
136
+
137
+ export const immersiveMode = (enable: boolean): void => {
138
+ if (!androidOnly("immersiveMode")) return;
139
+ checkNative();
140
+ Native.immersiveMode(enable);
141
+ };
142
+
143
+ // ═══════════════════════════════════════════════
144
+ // ORIENTATION
145
+ // ═══════════════════════════════════════════════
146
+
147
+ export const setOrientation = (mode: Orientation): void => {
148
+ checkNative();
149
+ Native.setOrientation(mode);
150
+ };
151
+
152
+ // ═══════════════════════════════════════════════
153
+ // 🆕 NETWORK INFO
146
154
  // ═══════════════════════════════════════════════
147
155
 
148
156
  /**
149
- * Set screen brightness (0.0 – 1.0).
157
+ * Get current network connection info.
158
+ * Includes: type, isConnected, isAirplaneMode, ssid, cellularGeneration
150
159
  */
151
- export const setBrightness = (level: number): void => {
160
+ export const getNetworkInfo = (): Promise<NetworkInfo> => {
152
161
  checkNative();
153
- SystemBarNative.setBrightness(Math.max(0, Math.min(1, level)));
162
+ return Native.getNetworkInfo();
154
163
  };
155
164
 
156
165
  /**
157
- * Get current screen brightness.
158
- * @returns Promise<number> 0.0 1.0
166
+ * Subscribe to network changes.
167
+ * @returns unsubscribe function — call it in useEffect cleanup
168
+ * @example
169
+ * const unsub = onNetworkChange((info) => console.log(info));
170
+ * return () => unsub();
159
171
  */
160
- export const getBrightness = (): Promise<number> => {
172
+ export const onNetworkChange = (
173
+ callback: (info: NetworkInfo) => void
174
+ ): (() => void) => {
161
175
  checkNative();
162
- return SystemBarNative.getBrightness();
176
+ // Native side emits "SystemBar_NetworkChange" events
177
+ const { DeviceEventEmitter } = require("react-native");
178
+ Native.startNetworkListener();
179
+ const sub = DeviceEventEmitter.addListener("SystemBar_NetworkChange", callback);
180
+ return () => {
181
+ sub.remove();
182
+ Native.stopNetworkListener();
183
+ };
163
184
  };
164
185
 
165
186
  // ═══════════════════════════════════════════════
166
- // VOLUME — native module
187
+ // 🆕 BATTERY
167
188
  // ═══════════════════════════════════════════════
168
189
 
169
190
  /**
170
- * Set volume level (0.0 1.0).
171
- * @param stream "music" | "ring" | "notification" | "alarm" | "system"
191
+ * Get current battery info: level (0–100), state, isCharging, isLow.
172
192
  */
173
- export const setVolume = (
174
- level: number,
175
- stream: VolumeStream = "music",
176
- ): void => {
193
+ export const getBatteryInfo = (): Promise<BatteryInfo> => {
177
194
  checkNative();
178
- SystemBarNative.setVolume(Math.max(0, Math.min(1, level)), stream);
195
+ return Native.getBatteryInfo();
179
196
  };
180
197
 
181
198
  /**
182
- * Get current volume level (0.0 1.0).
199
+ * Subscribe to battery level / state changes.
200
+ * @returns unsubscribe function
183
201
  */
184
- export const getVolume = (stream: VolumeStream = "music"): Promise<number> => {
202
+ export const onBatteryChange = (
203
+ callback: (info: BatteryInfo) => void
204
+ ): (() => void) => {
185
205
  checkNative();
186
- return SystemBarNative.getVolume(stream);
206
+ const { DeviceEventEmitter } = require("react-native");
207
+ Native.startBatteryListener();
208
+ const sub = DeviceEventEmitter.addListener("SystemBar_BatteryChange", callback);
209
+ return () => {
210
+ sub.remove();
211
+ Native.stopBatteryListener();
212
+ };
187
213
  };
188
214
 
215
+ // ═══════════════════════════════════════════════
216
+ // 🆕 HAPTIC FEEDBACK
217
+ // ═══════════════════════════════════════════════
218
+
189
219
  /**
190
- * Show or hide the system volume popup.
191
- * @platform android
220
+ * Trigger haptic feedback.
221
+ *
222
+ * iOS: Uses UIImpactFeedbackGenerator / UINotificationFeedbackGenerator.
223
+ * Android: Uses Vibrator / VibrationEffect (API 26+).
224
+ *
225
+ * @param pattern "light" | "medium" | "heavy" | "success" | "warning" | "error" | "selection"
192
226
  */
193
- export const setVolumeHUDVisible = (visible: boolean): void => {
194
- if (!androidOnly("setVolumeHUDVisible")) return;
195
- checkNative();
196
- SystemBarNative.setVolumeHUDVisible(visible);
227
+ export const haptic = (pattern: HapticPattern): void => {
228
+ if (isIOS) {
229
+ checkNative();
230
+ Native.haptic(pattern);
231
+ return;
232
+ }
233
+
234
+ // Android fallback using RN's built-in Vibration API
235
+ // when native module is unavailable
236
+ if (isAndroid) {
237
+ checkNative();
238
+ Native.haptic(pattern);
239
+ }
197
240
  };
198
241
 
199
242
  // ═══════════════════════════════════════════════
200
- // SCREEN — native module
243
+ // 🆕 SCREENCAST DETECTION
201
244
  // ═══════════════════════════════════════════════
202
245
 
203
246
  /**
204
- * Prevent the screen from going to sleep.
247
+ * Check if screen is currently being cast or mirrored.
205
248
  */
206
- export const keepScreenOn = (enable: boolean): void => {
249
+ export const getScreencastInfo = (): Promise<ScreencastInfo> => {
250
+ checkNative();
251
+ return Native.getScreencastInfo();
252
+ };
253
+
254
+ /**
255
+ * Subscribe to screencast state changes (started / stopped).
256
+ * @returns unsubscribe function
257
+ */
258
+ export const onScreencastChange = (
259
+ callback: (info: ScreencastInfo) => void
260
+ ): (() => void) => {
207
261
  checkNative();
208
- SystemBarNative.keepScreenOn(enable);
262
+ const { DeviceEventEmitter } = require("react-native");
263
+ Native.startScreencastListener();
264
+ const sub = DeviceEventEmitter.addListener("SystemBar_ScreencastChange", callback);
265
+ return () => {
266
+ sub.remove();
267
+ Native.stopScreencastListener();
268
+ };
209
269
  };
210
270
 
211
271
  /**
212
- * Enable immersive fullscreen (hides status + nav bar).
213
- * Uses WindowInsetsController on API 30+.
272
+ * Prevent screen content from appearing in screenshots / recordings.
273
+ * Useful for sensitive screens (payments, passwords).
214
274
  * @platform android
215
275
  */
216
- export const immersiveMode = (enable: boolean): void => {
217
- if (!androidOnly("immersiveMode")) return;
276
+ export const setSecureScreen = (enable: boolean): void => {
277
+ if (!androidOnly("setSecureScreen")) return;
218
278
  checkNative();
219
- SystemBarNative.immersiveMode(enable);
279
+ Native.setSecureScreen(enable);
220
280
  };
221
281
 
222
282
  // ═══════════════════════════════════════════════
223
- // ORIENTATION — native module
283
+ // 🆕 FONT SCALE / DISPLAY INFO
224
284
  // ═══════════════════════════════════════════════
225
285
 
226
286
  /**
227
- * Lock or unlock screen orientation.
228
- * "portrait" | "landscape" | "landscape-left" | "landscape-right" | "auto"
287
+ * Get current system font scale and display density.
288
+ * Useful for adapting layout to accessibility settings.
229
289
  */
230
- export const setOrientation = (mode: Orientation): void => {
290
+ export const getFontScaleInfo = (): Promise<FontScaleInfo> => {
291
+ checkNative();
292
+ return Native.getFontScaleInfo();
293
+ };
294
+
295
+ /**
296
+ * Subscribe to font scale changes (user changes system text size).
297
+ * @returns unsubscribe function
298
+ */
299
+ export const onFontScaleChange = (
300
+ callback: (info: FontScaleInfo) => void
301
+ ): (() => void) => {
231
302
  checkNative();
232
- SystemBarNative.setOrientation(mode);
303
+ const { DeviceEventEmitter } = require("react-native");
304
+ Native.startFontScaleListener();
305
+ const sub = DeviceEventEmitter.addListener("SystemBar_FontScaleChange", callback);
306
+ return () => {
307
+ sub.remove();
308
+ Native.stopFontScaleListener();
309
+ };
233
310
  };
package/src/types.ts CHANGED
@@ -1,30 +1,21 @@
1
1
  // ─────────────────────────────────────────────
2
- // rn-system-bar · types.ts
2
+ // rn-system-bar · types.ts v5
3
3
  // ─────────────────────────────────────────────
4
4
 
5
+ // ── Navigation Bar ────────────────────────────
5
6
  export type NavigationBarBehavior =
6
- | "overlay-swipe" // Swipe from edge reveals bar briefly (overlay)
7
- | "inset-swipe" // Swipe insets content when bar appears
8
- | "inset-touch"; // Touch insets content when bar appears
7
+ | "overlay-swipe"
8
+ | "inset-swipe"
9
+ | "inset-touch";
9
10
 
10
- export type NavigationBarButtonStyle =
11
- | "light" // White / light-coloured icons
12
- | "dark"; // Black / dark-coloured icons
11
+ export type NavigationBarButtonStyle = "light" | "dark";
12
+ export type NavigationBarStyle = "auto" | "inverted" | "light" | "dark";
13
+ export type NavigationBarVisibility = "visible" | "hidden";
13
14
 
14
- export type NavigationBarStyle =
15
- | "auto" // System decides
16
- | "inverted" // Inverted from system
17
- | "light" // Light background nav bar
18
- | "dark"; // Dark background nav bar
19
-
20
- export type NavigationBarVisibility =
21
- | "visible"
22
- | "hidden";
23
-
24
- export type StatusBarStyle =
25
- | "light" // Light icons (for dark backgrounds)
26
- | "dark"; // Dark icons (for light backgrounds)
15
+ // ── Status Bar ────────────────────────────────
16
+ export type StatusBarStyle = "light" | "dark";
27
17
 
18
+ // ── Orientation ───────────────────────────────
28
19
  export type Orientation =
29
20
  | "portrait"
30
21
  | "landscape"
@@ -32,6 +23,7 @@ export type Orientation =
32
23
  | "landscape-right"
33
24
  | "auto";
34
25
 
26
+ // ── Volume ────────────────────────────────────
35
27
  export type VolumeStream =
36
28
  | "music"
37
29
  | "ring"
@@ -39,32 +31,67 @@ export type VolumeStream =
39
31
  | "alarm"
40
32
  | "system";
41
33
 
42
- export interface SystemBarAPI {
43
- // Navigation Bar
44
- setNavigationBarColor(color: string): void;
45
- setNavigationBarVisibility(mode: NavigationBarVisibility): void;
46
- setNavigationBarButtonStyle(style: NavigationBarButtonStyle): void;
47
- setNavigationBarStyle(style: NavigationBarStyle): void;
48
- setNavigationBarBehavior(behavior: NavigationBarBehavior): void;
34
+ // ── Network ───────────────────────────────────
35
+ export type NetworkType =
36
+ | "wifi"
37
+ | "cellular"
38
+ | "ethernet"
39
+ | "none"
40
+ | "unknown";
41
+
42
+ export interface NetworkInfo {
43
+ /** Current connection type */
44
+ type: NetworkType;
45
+ /** true if any network is reachable */
46
+ isConnected: boolean;
47
+ /** true if in airplane mode */
48
+ isAirplaneMode: boolean;
49
+ /** WiFi SSID — Android only; null on iOS / when not connected */
50
+ ssid: string | null;
51
+ /** Cellular network generation: "2G" | "3G" | "4G" | "5G" | null */
52
+ cellularGeneration: "2G" | "3G" | "4G" | "5G" | null;
53
+ }
49
54
 
50
- // Status Bar
51
- setStatusBarColor(color: string): void;
52
- setStatusBarStyle(style: StatusBarStyle): void;
53
- setStatusBarVisibility(visible: boolean): void;
55
+ // ── Battery ───────────────────────────────────
56
+ export type BatteryState =
57
+ | "charging"
58
+ | "discharging"
59
+ | "full"
60
+ | "unknown";
54
61
 
55
- // Brightness
56
- setBrightness(level: number): void;
57
- getBrightness(): Promise<number>;
62
+ export interface BatteryInfo {
63
+ /** 0–100 */
64
+ level: number;
65
+ /** Current charging state */
66
+ state: BatteryState;
67
+ /** Shorthand: is it plugged in? */
68
+ isCharging: boolean;
69
+ /** Low battery threshold (<= 20%) */
70
+ isLow: boolean;
71
+ }
58
72
 
59
- // Volume
60
- setVolume(level: number, stream?: VolumeStream): void;
61
- getVolume(stream?: VolumeStream): Promise<number>;
62
- setVolumeHUDVisible(visible: boolean): void;
73
+ // ── Haptics ───────────────────────────────────
74
+ export type HapticPattern =
75
+ | "light" // subtle tap
76
+ | "medium" // standard tap
77
+ | "heavy" // strong tap
78
+ | "success" // double bump — success feedback
79
+ | "warning" // single bump — warning feedback
80
+ | "error" // triple bump — error feedback
81
+ | "selection"; // light tick — selection change
63
82
 
64
- // Screen
65
- keepScreenOn(enable: boolean): void;
66
- immersiveMode(enable: boolean): void;
83
+ // ── Screencast ────────────────────────────────
84
+ export interface ScreencastInfo {
85
+ /** true if screen is currently being cast / mirrored */
86
+ isCasting: boolean;
87
+ /** Name of the display being cast to, or null */
88
+ displayName: string | null;
89
+ }
67
90
 
68
- // Orientation
69
- setOrientation(mode: Orientation): void;
91
+ // ── Font Scale ────────────────────────────────
92
+ export interface FontScaleInfo {
93
+ /** System font scale multiplier, e.g. 1.0 = default, 1.3 = large */
94
+ fontScale: number;
95
+ /** Display density (Android: dp ratio, iOS: scale) */
96
+ density: number;
70
97
  }