@symbiote-native/brightness 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 +290 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/permissions.service/index.d.ts +8 -0
  6. package/build/angular/services/permissions.service/index.js +78 -0
  7. package/build/core/brightness.d.ts +56 -0
  8. package/build/core/brightness.js +137 -0
  9. package/build/core/index.d.ts +3 -0
  10. package/build/core/index.js +2 -0
  11. package/build/core/native-module.d.ts +16 -0
  12. package/build/core/native-module.js +3 -0
  13. package/build/core/types.d.ts +16 -0
  14. package/build/core/types.js +16 -0
  15. package/build/react/hooks/use-permissions/index.d.ts +6 -0
  16. package/build/react/hooks/use-permissions/index.js +30 -0
  17. package/build/react/index.d.ts +2 -0
  18. package/build/react/index.js +6 -0
  19. package/build/vue/composables/use-permissions/index.d.ts +7 -0
  20. package/build/vue/composables/use-permissions/index.js +23 -0
  21. package/build/vue/index.d.ts +2 -0
  22. package/build/vue/index.js +6 -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/permissions.service/index.d.ts +11 -0
  27. package/build-ngc/angular/services/permissions.service/index.js +37 -0
  28. package/build-ngc/angular/services/permissions.service/index.js.map +1 -0
  29. package/build-ngc/core/brightness.d.ts +56 -0
  30. package/build-ngc/core/brightness.js +138 -0
  31. package/build-ngc/core/brightness.js.map +1 -0
  32. package/build-ngc/core/index.d.ts +3 -0
  33. package/build-ngc/core/index.js +3 -0
  34. package/build-ngc/core/index.js.map +1 -0
  35. package/build-ngc/core/native-module.d.ts +16 -0
  36. package/build-ngc/core/native-module.js +4 -0
  37. package/build-ngc/core/native-module.js.map +1 -0
  38. package/build-ngc/core/types.d.ts +16 -0
  39. package/build-ngc/core/types.js +17 -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 +23 -0
  44. package/src/angular/services/permissions.service/index.ts +37 -0
  45. package/src/angular/services/permissions.service/permissions.service.test.ts +107 -0
  46. package/src/core/brightness.test.ts +334 -0
  47. package/src/core/brightness.ts +155 -0
  48. package/src/core/index.ts +22 -0
  49. package/src/core/native-module.ts +32 -0
  50. package/src/core/types.ts +27 -0
  51. package/src/react/hooks/use-permissions/index.ts +41 -0
  52. package/src/react/hooks/use-permissions/use-permissions.test.tsx +100 -0
  53. package/src/react/index.ts +26 -0
  54. package/src/vue/composables/use-permissions/index.ts +36 -0
  55. package/src/vue/composables/use-permissions/use-permissions.test.ts +99 -0
  56. package/src/vue/index.ts +26 -0
@@ -0,0 +1,37 @@
1
+ // Angular twin of React's usePermissions hook and Vue's usePermissions composable.
2
+ // `connect()` auto-fetches once (no ongoing subscription to clean up, so no Injector/effect
3
+ // needed, unlike BatteryStateService) and returns a readonly signal; get()/request() are
4
+ // imperative one-shot methods.
5
+ //
6
+ // readonly status = inject(PermissionsService).connect();
7
+ // // template: {{ status()?.granted }}
8
+ import { Injectable, signal, type Signal } from '@angular/core';
9
+ import {
10
+ getPermissionsAsync,
11
+ requestPermissionsAsync,
12
+ type PermissionResponse,
13
+ } from '../../../core';
14
+
15
+ @Injectable({ providedIn: 'root' })
16
+ export class PermissionsService {
17
+ private readonly status = signal<PermissionResponse | null>(null);
18
+
19
+ connect(): Signal<PermissionResponse | null> {
20
+ if (this.status() === null) {
21
+ void this.get();
22
+ }
23
+ return this.status.asReadonly();
24
+ }
25
+
26
+ async get(): Promise<PermissionResponse> {
27
+ const response = await getPermissionsAsync();
28
+ this.status.set(response);
29
+ return response;
30
+ }
31
+
32
+ async request(): Promise<PermissionResponse> {
33
+ const response = await requestPermissionsAsync();
34
+ this.status.set(response);
35
+ return response;
36
+ }
37
+ }
@@ -0,0 +1,107 @@
1
+ // Co-located Angular-driven test (ADR 0025) for PermissionsService. 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 { PermissionsService } from './index';
10
+ import { PermissionStatus, type PermissionResponse } from '../../../core';
11
+
12
+ const { getPermissionsAsync, requestPermissionsAsync } = vi.hoisted(() => ({
13
+ getPermissionsAsync: vi.fn(),
14
+ requestPermissionsAsync: vi.fn(),
15
+ }));
16
+
17
+ // Same enum-shaped-object mock trick packages/battery/src/angular/services/battery-state.service's
18
+ // test uses for BatteryState.
19
+ vi.mock('../../../core', () => ({
20
+ getPermissionsAsync,
21
+ requestPermissionsAsync,
22
+ PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
23
+ }));
24
+
25
+ const GRANTED: PermissionResponse = {
26
+ status: PermissionStatus.GRANTED,
27
+ granted: true,
28
+ canAskAgain: true,
29
+ expires: 'never',
30
+ };
31
+ const DENIED: PermissionResponse = {
32
+ status: PermissionStatus.DENIED,
33
+ granted: false,
34
+ canAskAgain: true,
35
+ expires: 'never',
36
+ };
37
+
38
+ const ROOT_TAG = 973;
39
+ const fabric = installFabric();
40
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
41
+
42
+ let capturedStatus: Signal<PermissionResponse | null> | undefined;
43
+ let capturedService: PermissionsService | undefined;
44
+
45
+ @Component({
46
+ selector: 'symbiote-permissions-host',
47
+ standalone: true,
48
+ template: '',
49
+ })
50
+ class PermissionsHost {
51
+ readonly service = inject(PermissionsService);
52
+ readonly status = this.service.connect();
53
+
54
+ constructor() {
55
+ capturedStatus = this.status;
56
+ capturedService = this.service;
57
+ }
58
+ }
59
+
60
+ beforeEach(() => {
61
+ capturedStatus = undefined;
62
+ capturedService = undefined;
63
+ vi.clearAllMocks();
64
+ getPermissionsAsync.mockResolvedValue(GRANTED);
65
+ requestPermissionsAsync.mockResolvedValue(GRANTED);
66
+ });
67
+
68
+ afterEach(() => {
69
+ unmount(ROOT_TAG);
70
+ fabric.reset();
71
+ });
72
+
73
+ describe('PermissionsService.connect', () => {
74
+ it('reports null before the initial fetch resolves', () => {
75
+ mount(ROOT_TAG, PermissionsHost);
76
+
77
+ expect(capturedStatus?.()).toBe(null);
78
+ });
79
+
80
+ it('reports the fetched status once getPermissionsAsync() resolves', async () => {
81
+ mount(ROOT_TAG, PermissionsHost);
82
+ await tick();
83
+
84
+ expect(capturedStatus?.()).toEqual(GRANTED);
85
+ expect(getPermissionsAsync).toHaveBeenCalledTimes(1);
86
+ });
87
+
88
+ it('request() delegates to requestPermissionsAsync and updates the signal', async () => {
89
+ mount(ROOT_TAG, PermissionsHost);
90
+ await tick();
91
+
92
+ requestPermissionsAsync.mockResolvedValueOnce(DENIED);
93
+ await capturedService?.request();
94
+
95
+ expect(capturedStatus?.()).toEqual(DENIED);
96
+ });
97
+
98
+ it('get() re-fetches and updates the signal', async () => {
99
+ mount(ROOT_TAG, PermissionsHost);
100
+ await tick();
101
+
102
+ getPermissionsAsync.mockResolvedValueOnce(DENIED);
103
+ await capturedService?.get();
104
+
105
+ expect(capturedStatus?.()).toEqual(DENIED);
106
+ });
107
+ });
@@ -0,0 +1,334 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const FAKE_NATIVE_BRIGHTNESS = {
4
+ getBrightnessAsync: vi.fn(async () => 0.5),
5
+ setBrightnessAsync: vi.fn(async () => undefined),
6
+ getSystemBrightnessAsync: vi.fn(async () => 0.6),
7
+ setSystemBrightnessAsync: vi.fn(async () => undefined),
8
+ restoreSystemBrightnessAsync: vi.fn(async () => undefined),
9
+ isUsingSystemBrightnessAsync: vi.fn(async () => true),
10
+ getSystemBrightnessModeAsync: vi.fn(async () => 1),
11
+ setSystemBrightnessModeAsync: vi.fn(async () => undefined),
12
+ getPermissionsAsync: vi.fn(async () => ({ status: 'granted', granted: true })),
13
+ requestPermissionsAsync: vi.fn(async () => ({ status: 'granted', granted: true })),
14
+ addListener: vi.fn(() => ({ remove: vi.fn() })),
15
+ };
16
+
17
+ // The real ExpoBrightness native module only exists on device — resolving it via
18
+ // requireNativeModule() at import time would throw in this headless test run, so the
19
+ // module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
20
+ // packages/battery/src/core/battery.test.ts and packages/haptics/src/core/haptics.test.ts use.
21
+ vi.mock('./native-module', () => ({
22
+ expoBrightness: FAKE_NATIVE_BRIGHTNESS,
23
+ }));
24
+
25
+ // expo-modules-core's real entry transitively imports 'react-native' for Platform/
26
+ // TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
27
+ // packages/haptics/src/core/haptics.test.ts uses, since Platform.OS gates every
28
+ // system-brightness branch here.
29
+ const fakePlatform = { OS: 'ios' as 'ios' | 'android' };
30
+
31
+ vi.mock('expo-modules-core', () => ({
32
+ Platform: fakePlatform,
33
+ PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
34
+ UnavailabilityError: class UnavailabilityError extends Error {
35
+ constructor(moduleName: string, propertyName: string) {
36
+ super(`${propertyName} is not available on ${moduleName}`);
37
+ }
38
+ },
39
+ }));
40
+
41
+ const {
42
+ isAvailableAsync,
43
+ getBrightnessAsync,
44
+ setBrightnessAsync,
45
+ getSystemBrightnessAsync,
46
+ setSystemBrightnessAsync,
47
+ restoreSystemBrightnessAsync,
48
+ isUsingSystemBrightnessAsync,
49
+ getSystemBrightnessModeAsync,
50
+ setSystemBrightnessModeAsync,
51
+ getPermissionsAsync,
52
+ requestPermissionsAsync,
53
+ addBrightnessListener,
54
+ } = await import('./brightness');
55
+ const { BrightnessMode } = await import('./types');
56
+
57
+ afterEach(() => {
58
+ fakePlatform.OS = 'ios';
59
+ vi.clearAllMocks();
60
+ });
61
+
62
+ describe('isAvailableAsync', () => {
63
+ it('resolves true when the native module implements getBrightnessAsync', async () => {
64
+ await expect(isAvailableAsync()).resolves.toBe(true);
65
+ });
66
+
67
+ it('resolves false when the native method is absent', async () => {
68
+ const { getBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
69
+ // @ts-expect-error -- simulating a platform where the native module has no such method
70
+ FAKE_NATIVE_BRIGHTNESS.getBrightnessAsync = undefined;
71
+
72
+ await expect(isAvailableAsync()).resolves.toBe(false);
73
+
74
+ FAKE_NATIVE_BRIGHTNESS.getBrightnessAsync = native;
75
+ });
76
+ });
77
+
78
+ describe('getBrightnessAsync', () => {
79
+ it('delegates to the native module', async () => {
80
+ await expect(getBrightnessAsync()).resolves.toBe(0.5);
81
+ });
82
+
83
+ it('throws an UnavailabilityError-shaped error when the native method is absent', async () => {
84
+ const { getBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
85
+ // @ts-expect-error -- simulating a platform where the native module has no such method
86
+ FAKE_NATIVE_BRIGHTNESS.getBrightnessAsync = undefined;
87
+
88
+ await expect(getBrightnessAsync()).rejects.toThrow(
89
+ 'getBrightnessAsync is not available on expo-brightness',
90
+ );
91
+
92
+ FAKE_NATIVE_BRIGHTNESS.getBrightnessAsync = native;
93
+ });
94
+ });
95
+
96
+ describe('setBrightnessAsync', () => {
97
+ it('delegates to the native module', async () => {
98
+ await setBrightnessAsync(0.4);
99
+ expect(FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync).toHaveBeenCalledWith(0.4);
100
+ });
101
+
102
+ it('clamps a value above 1 down to 1', async () => {
103
+ await setBrightnessAsync(1.5);
104
+ expect(FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync).toHaveBeenCalledWith(1);
105
+ });
106
+
107
+ it('clamps a value below 0 up to 0', async () => {
108
+ await setBrightnessAsync(-0.5);
109
+ expect(FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync).toHaveBeenCalledWith(0);
110
+ });
111
+
112
+ it('throws a TypeError when the clamped value is NaN', async () => {
113
+ await expect(setBrightnessAsync(NaN)).rejects.toThrow(
114
+ 'setBrightnessAsync cannot be called with NaN',
115
+ );
116
+ expect(FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it('throws an UnavailabilityError-shaped error when the native method is absent', async () => {
120
+ const { setBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
121
+ // @ts-expect-error -- simulating a platform where the native module has no such method
122
+ FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync = undefined;
123
+
124
+ await expect(setBrightnessAsync(0.5)).rejects.toThrow(
125
+ 'setBrightnessAsync is not available on expo-brightness',
126
+ );
127
+
128
+ FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync = native;
129
+ });
130
+ });
131
+
132
+ describe('getSystemBrightnessAsync', () => {
133
+ it('delegates to getBrightnessAsync on non-Android platforms', async () => {
134
+ fakePlatform.OS = 'ios';
135
+ await expect(getSystemBrightnessAsync()).resolves.toBe(0.5);
136
+ expect(FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessAsync).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it('delegates to the native module on Android', async () => {
140
+ fakePlatform.OS = 'android';
141
+ await expect(getSystemBrightnessAsync()).resolves.toBe(0.6);
142
+ });
143
+
144
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
145
+ fakePlatform.OS = 'android';
146
+ const { getSystemBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
147
+ // @ts-expect-error -- simulating an Android build missing this native method
148
+ FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessAsync = undefined;
149
+
150
+ await expect(getSystemBrightnessAsync()).rejects.toThrow(
151
+ 'getSystemBrightnessAsync is not available on expo-brightness',
152
+ );
153
+
154
+ FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessAsync = native;
155
+ });
156
+ });
157
+
158
+ describe('setSystemBrightnessAsync', () => {
159
+ it('delegates to setBrightnessAsync on non-Android platforms', async () => {
160
+ fakePlatform.OS = 'ios';
161
+ await setSystemBrightnessAsync(0.3);
162
+ expect(FAKE_NATIVE_BRIGHTNESS.setBrightnessAsync).toHaveBeenCalledWith(0.3);
163
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessAsync).not.toHaveBeenCalled();
164
+ });
165
+
166
+ it('delegates to the native module on Android', async () => {
167
+ fakePlatform.OS = 'android';
168
+ await setSystemBrightnessAsync(0.3);
169
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessAsync).toHaveBeenCalledWith(0.3);
170
+ });
171
+
172
+ it('clamps out-of-range values before delegating', async () => {
173
+ fakePlatform.OS = 'android';
174
+ await setSystemBrightnessAsync(2);
175
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessAsync).toHaveBeenCalledWith(1);
176
+ });
177
+
178
+ it('throws a TypeError when the clamped value is NaN', async () => {
179
+ await expect(setSystemBrightnessAsync(NaN)).rejects.toThrow(
180
+ 'setSystemBrightnessAsync cannot be called with NaN',
181
+ );
182
+ });
183
+
184
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
185
+ fakePlatform.OS = 'android';
186
+ const { setSystemBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
187
+ // @ts-expect-error -- simulating an Android build missing this native method
188
+ FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessAsync = undefined;
189
+
190
+ await expect(setSystemBrightnessAsync(0.5)).rejects.toThrow(
191
+ 'setSystemBrightnessAsync is not available on expo-brightness',
192
+ );
193
+
194
+ FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessAsync = native;
195
+ });
196
+ });
197
+
198
+ describe('restoreSystemBrightnessAsync', () => {
199
+ it('no-ops on non-Android platforms without calling the native module', async () => {
200
+ fakePlatform.OS = 'ios';
201
+ await restoreSystemBrightnessAsync();
202
+ expect(FAKE_NATIVE_BRIGHTNESS.restoreSystemBrightnessAsync).not.toHaveBeenCalled();
203
+ });
204
+
205
+ it('delegates to the native module on Android', async () => {
206
+ fakePlatform.OS = 'android';
207
+ await restoreSystemBrightnessAsync();
208
+ expect(FAKE_NATIVE_BRIGHTNESS.restoreSystemBrightnessAsync).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
212
+ fakePlatform.OS = 'android';
213
+ const { restoreSystemBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
214
+ // @ts-expect-error -- simulating an Android build missing this native method
215
+ FAKE_NATIVE_BRIGHTNESS.restoreSystemBrightnessAsync = undefined;
216
+
217
+ await expect(restoreSystemBrightnessAsync()).rejects.toThrow(
218
+ 'restoreSystemBrightnessAsync is not available on expo-brightness',
219
+ );
220
+
221
+ FAKE_NATIVE_BRIGHTNESS.restoreSystemBrightnessAsync = native;
222
+ });
223
+ });
224
+
225
+ describe('isUsingSystemBrightnessAsync', () => {
226
+ it('resolves false on non-Android platforms without calling the native module', async () => {
227
+ fakePlatform.OS = 'ios';
228
+ await expect(isUsingSystemBrightnessAsync()).resolves.toBe(false);
229
+ expect(FAKE_NATIVE_BRIGHTNESS.isUsingSystemBrightnessAsync).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it('delegates to the native module on Android', async () => {
233
+ fakePlatform.OS = 'android';
234
+ await expect(isUsingSystemBrightnessAsync()).resolves.toBe(true);
235
+ });
236
+
237
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
238
+ fakePlatform.OS = 'android';
239
+ const { isUsingSystemBrightnessAsync: native } = FAKE_NATIVE_BRIGHTNESS;
240
+ // @ts-expect-error -- simulating an Android build missing this native method
241
+ FAKE_NATIVE_BRIGHTNESS.isUsingSystemBrightnessAsync = undefined;
242
+
243
+ await expect(isUsingSystemBrightnessAsync()).rejects.toThrow(
244
+ 'isUsingSystemBrightnessAsync is not available on expo-brightness',
245
+ );
246
+
247
+ FAKE_NATIVE_BRIGHTNESS.isUsingSystemBrightnessAsync = native;
248
+ });
249
+ });
250
+
251
+ describe('getSystemBrightnessModeAsync', () => {
252
+ it('resolves UNKNOWN on non-Android platforms without calling the native module', async () => {
253
+ fakePlatform.OS = 'ios';
254
+ await expect(getSystemBrightnessModeAsync()).resolves.toBe(BrightnessMode.UNKNOWN);
255
+ expect(FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessModeAsync).not.toHaveBeenCalled();
256
+ });
257
+
258
+ it('delegates to the native module on Android', async () => {
259
+ fakePlatform.OS = 'android';
260
+ await expect(getSystemBrightnessModeAsync()).resolves.toBe(1);
261
+ });
262
+
263
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
264
+ fakePlatform.OS = 'android';
265
+ const { getSystemBrightnessModeAsync: native } = FAKE_NATIVE_BRIGHTNESS;
266
+ // @ts-expect-error -- simulating an Android build missing this native method
267
+ FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessModeAsync = undefined;
268
+
269
+ await expect(getSystemBrightnessModeAsync()).rejects.toThrow(
270
+ 'getSystemBrightnessModeAsync is not available on expo-brightness',
271
+ );
272
+
273
+ FAKE_NATIVE_BRIGHTNESS.getSystemBrightnessModeAsync = native;
274
+ });
275
+ });
276
+
277
+ describe('setSystemBrightnessModeAsync', () => {
278
+ it('no-ops on non-Android platforms without calling the native module', async () => {
279
+ fakePlatform.OS = 'ios';
280
+ await setSystemBrightnessModeAsync(BrightnessMode.AUTOMATIC);
281
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessModeAsync).not.toHaveBeenCalled();
282
+ });
283
+
284
+ it('no-ops on Android when passed UNKNOWN', async () => {
285
+ fakePlatform.OS = 'android';
286
+ await setSystemBrightnessModeAsync(BrightnessMode.UNKNOWN);
287
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessModeAsync).not.toHaveBeenCalled();
288
+ });
289
+
290
+ it('delegates to the native module on Android with a concrete mode', async () => {
291
+ fakePlatform.OS = 'android';
292
+ await setSystemBrightnessModeAsync(BrightnessMode.MANUAL);
293
+ expect(FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessModeAsync).toHaveBeenCalledWith(
294
+ BrightnessMode.MANUAL,
295
+ );
296
+ });
297
+
298
+ it('throws an UnavailabilityError-shaped error on Android when the native method is absent', async () => {
299
+ fakePlatform.OS = 'android';
300
+ const { setSystemBrightnessModeAsync: native } = FAKE_NATIVE_BRIGHTNESS;
301
+ // @ts-expect-error -- simulating an Android build missing this native method
302
+ FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessModeAsync = undefined;
303
+
304
+ await expect(setSystemBrightnessModeAsync(BrightnessMode.MANUAL)).rejects.toThrow(
305
+ 'setSystemBrightnessModeAsync is not available on expo-brightness',
306
+ );
307
+
308
+ FAKE_NATIVE_BRIGHTNESS.setSystemBrightnessModeAsync = native;
309
+ });
310
+ });
311
+
312
+ describe('getPermissionsAsync', () => {
313
+ it('passes straight through to the native module', async () => {
314
+ await expect(getPermissionsAsync()).resolves.toEqual({ status: 'granted', granted: true });
315
+ });
316
+ });
317
+
318
+ describe('requestPermissionsAsync', () => {
319
+ it('passes straight through to the native module', async () => {
320
+ await expect(requestPermissionsAsync()).resolves.toEqual({ status: 'granted', granted: true });
321
+ });
322
+ });
323
+
324
+ describe('addBrightnessListener', () => {
325
+ it('subscribes through the Expo.brightnessDidChange event name', () => {
326
+ const listener = vi.fn();
327
+ addBrightnessListener(listener);
328
+
329
+ expect(FAKE_NATIVE_BRIGHTNESS.addListener).toHaveBeenCalledWith(
330
+ 'Expo.brightnessDidChange',
331
+ listener,
332
+ );
333
+ });
334
+ });
@@ -0,0 +1,155 @@
1
+ // Hand-ported from .vendors/expo/packages/expo-brightness/src/Brightness.ts (sdk-57). The
2
+ // system-brightness surface (mode, "using system brightness", restore) is Android-only —
3
+ // upstream branches every one of those on `Platform.OS !== 'android'` before ever reaching the
4
+ // native module, so those branches are preserved verbatim rather than guessed.
5
+ import { Platform, UnavailabilityError } from 'expo-modules-core';
6
+ import type { EventSubscription } from 'expo-modules-core';
7
+
8
+ import { expoBrightness } from './native-module';
9
+ import { BrightnessMode, type BrightnessEvent, type PermissionResponse } from './types';
10
+
11
+ const NATIVE_MODULE_NAME = 'expo-brightness';
12
+
13
+ function clampBrightness(brightnessValue: number, callerName: string): number {
14
+ const clamped = Math.max(0, Math.min(brightnessValue, 1));
15
+ if (Number.isNaN(clamped)) {
16
+ throw new TypeError(`${callerName} cannot be called with ${brightnessValue}`);
17
+ }
18
+ return clamped;
19
+ }
20
+
21
+ /** Whether the brightness API is available on the current device. */
22
+ export async function isAvailableAsync(): Promise<boolean> {
23
+ return !!expoBrightness.getBrightnessAsync;
24
+ }
25
+
26
+ /** Gets the current brightness level of the device's main screen, between `0` and `1`. */
27
+ export async function getBrightnessAsync(): Promise<number> {
28
+ if (!expoBrightness.getBrightnessAsync) {
29
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getBrightnessAsync');
30
+ }
31
+ return expoBrightness.getBrightnessAsync();
32
+ }
33
+
34
+ /**
35
+ * Sets the current screen brightness. On iOS this only affects the screen while the app is
36
+ * foregrounded; on Android it persists until changed again.
37
+ */
38
+ export async function setBrightnessAsync(brightnessValue: number): Promise<void> {
39
+ const clamped = clampBrightness(brightnessValue, 'setBrightnessAsync');
40
+ if (!expoBrightness.setBrightnessAsync) {
41
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setBrightnessAsync');
42
+ }
43
+ await expoBrightness.setBrightnessAsync(clamped);
44
+ }
45
+
46
+ /**
47
+ * Gets the system-wide brightness. On iOS the app-local and system brightness are the same
48
+ * value, so this delegates to `getBrightnessAsync`.
49
+ * @platform android
50
+ */
51
+ export async function getSystemBrightnessAsync(): Promise<number> {
52
+ if (Platform.OS !== 'android') {
53
+ return getBrightnessAsync();
54
+ }
55
+ if (!expoBrightness.getSystemBrightnessAsync) {
56
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getSystemBrightnessAsync');
57
+ }
58
+ return expoBrightness.getSystemBrightnessAsync();
59
+ }
60
+
61
+ /**
62
+ * Sets the system-wide brightness. On iOS this delegates to `setBrightnessAsync`, since there is
63
+ * no separate system-level brightness to set.
64
+ * @platform android
65
+ */
66
+ export async function setSystemBrightnessAsync(brightnessValue: number): Promise<void> {
67
+ const clamped = clampBrightness(brightnessValue, 'setSystemBrightnessAsync');
68
+ if (Platform.OS !== 'android') {
69
+ return setBrightnessAsync(clamped);
70
+ }
71
+ if (!expoBrightness.setSystemBrightnessAsync) {
72
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setSystemBrightnessAsync');
73
+ }
74
+ await expoBrightness.setSystemBrightnessAsync(clamped);
75
+ }
76
+
77
+ /**
78
+ * Resets the system brightness to the value it had before this app started controlling it. A
79
+ * no-op on every platform except Android.
80
+ * @platform android
81
+ */
82
+ export async function restoreSystemBrightnessAsync(): Promise<void> {
83
+ if (Platform.OS !== 'android') {
84
+ return;
85
+ }
86
+ if (!expoBrightness.restoreSystemBrightnessAsync) {
87
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'restoreSystemBrightnessAsync');
88
+ }
89
+ await expoBrightness.restoreSystemBrightnessAsync();
90
+ }
91
+
92
+ /**
93
+ * Whether the app is currently controlling the system brightness (as opposed to only its own
94
+ * window's brightness). Always `false` on every platform except Android.
95
+ * @platform android
96
+ */
97
+ export async function isUsingSystemBrightnessAsync(): Promise<boolean> {
98
+ if (Platform.OS !== 'android') {
99
+ return false;
100
+ }
101
+ if (!expoBrightness.isUsingSystemBrightnessAsync) {
102
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'isUsingSystemBrightnessAsync');
103
+ }
104
+ return expoBrightness.isUsingSystemBrightnessAsync();
105
+ }
106
+
107
+ /**
108
+ * Gets the system brightness mode (`AUTOMATIC` / `MANUAL`). Always resolves `UNKNOWN` on every
109
+ * platform except Android.
110
+ * @platform android
111
+ */
112
+ export async function getSystemBrightnessModeAsync(): Promise<BrightnessMode> {
113
+ if (Platform.OS !== 'android') {
114
+ return BrightnessMode.UNKNOWN;
115
+ }
116
+ if (!expoBrightness.getSystemBrightnessModeAsync) {
117
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getSystemBrightnessModeAsync');
118
+ }
119
+ return expoBrightness.getSystemBrightnessModeAsync();
120
+ }
121
+
122
+ /**
123
+ * Sets the system brightness mode. A no-op on every platform except Android, and also a no-op
124
+ * when passed `BrightnessMode.UNKNOWN` (there is nothing meaningful to set it to).
125
+ * @platform android
126
+ */
127
+ export async function setSystemBrightnessModeAsync(brightnessMode: BrightnessMode): Promise<void> {
128
+ if (Platform.OS !== 'android' || brightnessMode === BrightnessMode.UNKNOWN) {
129
+ return;
130
+ }
131
+ if (!expoBrightness.setSystemBrightnessModeAsync) {
132
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setSystemBrightnessModeAsync');
133
+ }
134
+ await expoBrightness.setSystemBrightnessModeAsync(brightnessMode);
135
+ }
136
+
137
+ /** Checks user's permissions for accessing the system brightness. */
138
+ export async function getPermissionsAsync(): Promise<PermissionResponse> {
139
+ return expoBrightness.getPermissionsAsync();
140
+ }
141
+
142
+ /** Asks the user for permission to access the system brightness. */
143
+ export async function requestPermissionsAsync(): Promise<PermissionResponse> {
144
+ return expoBrightness.requestPermissionsAsync();
145
+ }
146
+
147
+ /**
148
+ * Subscribes to brightness changes. Only fires on iOS — never on Android or web.
149
+ * @platform ios
150
+ */
151
+ export function addBrightnessListener(
152
+ listener: (event: BrightnessEvent) => void,
153
+ ): EventSubscription {
154
+ return expoBrightness.addListener('Expo.brightnessDidChange', listener);
155
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ isAvailableAsync,
3
+ getBrightnessAsync,
4
+ setBrightnessAsync,
5
+ getSystemBrightnessAsync,
6
+ setSystemBrightnessAsync,
7
+ restoreSystemBrightnessAsync,
8
+ isUsingSystemBrightnessAsync,
9
+ getSystemBrightnessModeAsync,
10
+ setSystemBrightnessModeAsync,
11
+ getPermissionsAsync,
12
+ requestPermissionsAsync,
13
+ addBrightnessListener,
14
+ } from './brightness';
15
+ export {
16
+ BrightnessMode,
17
+ PermissionStatus,
18
+ type BrightnessEvent,
19
+ type PermissionExpiration,
20
+ type PermissionResponse,
21
+ } from './types';
22
+ export type { EventSubscription } from 'expo-modules-core';
@@ -0,0 +1,32 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { EventSubscription } from 'expo-modules-core';
3
+ import type { BrightnessEvent, BrightnessMode, PermissionResponse } from './types';
4
+
5
+ const EXPO_BRIGHTNESS_MODULE_NAME = 'ExpoBrightness';
6
+
7
+ // Every system-brightness / brightness-mode method is optional and Android-only — each call
8
+ // site checks for its presence before calling through and throws an UnavailabilityError itself,
9
+ // matching upstream's own per-platform capability checks rather than assuming the native module
10
+ // implements the whole surface (same convention as packages/local-auth/src/core/native-module.ts
11
+ // and packages/haptics/src/core/native-module.ts). getBrightnessAsync/setBrightnessAsync are
12
+ // also optional so isAvailableAsync can probe for them without throwing.
13
+ export type INativeBrightnessModule = {
14
+ getBrightnessAsync?(): Promise<number>;
15
+ setBrightnessAsync?(value: number): Promise<void>;
16
+ getSystemBrightnessAsync?(): Promise<number>;
17
+ setSystemBrightnessAsync?(value: number): Promise<void>;
18
+ restoreSystemBrightnessAsync?(): Promise<void>;
19
+ isUsingSystemBrightnessAsync?(): Promise<boolean>;
20
+ getSystemBrightnessModeAsync?(): Promise<BrightnessMode>;
21
+ setSystemBrightnessModeAsync?(mode: BrightnessMode): Promise<void>;
22
+ getPermissionsAsync(): Promise<PermissionResponse>;
23
+ requestPermissionsAsync(): Promise<PermissionResponse>;
24
+ addListener(
25
+ eventName: 'Expo.brightnessDidChange',
26
+ listener: (event: BrightnessEvent) => void,
27
+ ): EventSubscription;
28
+ };
29
+
30
+ export const expoBrightness = requireNativeModule<INativeBrightnessModule>(
31
+ EXPO_BRIGHTNESS_MODULE_NAME,
32
+ );