@symbiote-native/brightness 3.0.1 → 3.0.3

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/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # @symbiote-native/brightness
2
2
 
3
3
  A wrapper package for [SymbioteNative](../../README.md) that makes
4
- [`expo-brightness`](https://github.com/expo/expo/tree/main/packages/expo-brightness) — screen
4
+ [`expo-brightness`](https://github.com/expo/expo/tree/main/packages/expo-brightness) - screen
5
5
  brightness get/set, Android-only system-brightness-mode control, an iOS-only brightness-change
6
- listener, and permission get/request — usable from **every** adapter: React, Vue, Svelte, Solid,
7
- and Angular, not just React. Unlike [`@symbiote-native/sensors`](../sensors) — a `DeviceSensor` class per instance,
8
- subscribed through a hook/composable/service — this package's surface is mostly stateless free
6
+ listener, and permission get/request - usable from **every** adapter: React, Vue, Svelte, Solid,
7
+ and Angular, not just React. Unlike [`@symbiote-native/sensors`](../sensors) - a `DeviceSensor` class per instance,
8
+ subscribed through a hook/composable/service - this package's surface is mostly stateless free
9
9
  async functions plus one listener; only the permission surface gets its own lifecycle hook/
10
10
  composable/service, mirroring upstream's own `usePermissions`. Built the same way as
11
11
  [`@symbiote-native/local-auth`](../local-auth), [`@symbiote-native/battery`](../battery), and
12
- [`@symbiote-native/haptics`](../haptics) — an `expo-modules-core`-based wrapper (see the
12
+ [`@symbiote-native/haptics`](../haptics) - an `expo-modules-core`-based wrapper (see the
13
13
  `symbiote-expo-native-module` project skill for the full mechanism: why `expo-modules-core` is
14
14
  depended on directly and never the `expo` meta-package, why the upstream JS is hand-ported into
15
15
  `core/` rather than imported, and how autolinking picks up the native module).
@@ -28,17 +28,17 @@ npx @symbiote-native/cli new my-app --brightness
28
28
  npx @symbiote-native/cli add --brightness
29
29
  ```
30
30
 
31
- Either way: installs `@symbiote-native/brightness` and wires the native autolinking automatically — see
31
+ Either way: installs `@symbiote-native/brightness` and wires the native autolinking automatically - see
32
32
  [`@symbiote-native/cli`](../cli).
33
33
 
34
34
  <details>
35
- <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
35
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
36
36
 
37
37
  ```bash
38
38
  npm install @symbiote-native/brightness
39
39
  ```
40
40
 
41
- `expo-brightness` and `expo-modules-core` come along as regular, pinned dependencies — never
41
+ `expo-brightness` and `expo-modules-core` come along as regular, pinned dependencies - never
42
42
  install either yourself, and never add the `expo` meta-package to this project (it bundles its own
43
43
  Metro/Babel pipeline that conflicts with this project's own).
44
44
 
@@ -46,23 +46,23 @@ Metro/Babel pipeline that conflicts with this project's own).
46
46
 
47
47
  Unlike a plain RN native module, `expo-brightness`'s native code is discovered by
48
48
  `expo-modules-autolinking`, not the `react-native.config.cjs`/podspec mechanism other wrappers in
49
- this repo use — this needs wiring into the native host app **once**, covering this package and
49
+ this repo use - this needs wiring into the native host app **once**, covering this package and
50
50
  every other `expo-modules-core` package with zero further changes:
51
51
 
52
52
  | Platform | Touches |
53
53
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
- | iOS | `ios/Podfile` — add `use_expo_modules!` |
55
- | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
56
- | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
57
- | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
58
- | Android | `AndroidManifest.xml` — `android.permission.WRITE_SETTINGS`, required to set the system-wide brightness (`setSystemBrightnessAsync`/`setSystemBrightnessModeAsync`) |
59
-
60
- Full mechanics — the Podfile pieces that normally ship inside the `expo` package, the `expo`
61
- peer-dependency exclusion list — live in the `symbiote-expo-native-module` skill. Reference
54
+ | iOS | `ios/Podfile` - add `use_expo_modules!` |
55
+ | iOS | `AppDelegate.swift` - Expo's runtime-bootstrap hook |
56
+ | Android | `settings.gradle` / `app/build.gradle` - resolve and include the Expo Gradle projects |
57
+ | Android | `MainApplication.kt` - Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
58
+ | Android | `AndroidManifest.xml` - `android.permission.WRITE_SETTINGS`, required to set the system-wide brightness (`setSystemBrightnessAsync`/`setSystemBrightnessModeAsync`) |
59
+
60
+ Full mechanics - the Podfile pieces that normally ship inside the `expo` package, the `expo`
61
+ peer-dependency exclusion list - live in the `symbiote-expo-native-module` skill. Reference
62
62
  implementation: `examples/expo-react/android/app/src/main/AndroidManifest.xml` (the
63
63
  `WRITE_SETTINGS` permission) and
64
64
  `examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt` (the
65
- `BrightnessModule` registration) — the Vue and Angular Expo canaries wire the same three layers.
65
+ `BrightnessModule` registration) - the Vue and Angular Expo canaries wire the same three layers.
66
66
 
67
67
  </details>
68
68
 
@@ -73,24 +73,24 @@ src/core/ isAvailableAsync, get/setBrightnessAsync, the Android-
73
73
  system-brightness-mode surface, permission get/request, and
74
74
  addBrightnessListener (iOS only); native-module.ts resolves
75
75
  ExpoBrightness through expo-modules-core's requireNativeModule.
76
- src/react/hooks/ @symbiote-native/brightness/react — usePermissions
77
- src/vue/composables/ @symbiote-native/brightness/vue — usePermissions (same name)
78
- src/svelte/runes/ @symbiote-native/brightness/svelte — usePermissions (same name)
79
- src/solid/primitives/ @symbiote-native/brightness/solid — createPermissions (Solid says
76
+ src/react/hooks/ @symbiote-native/brightness/react - usePermissions
77
+ src/vue/composables/ @symbiote-native/brightness/vue - usePermissions (same name)
78
+ src/svelte/runes/ @symbiote-native/brightness/svelte - usePermissions (same name)
79
+ src/solid/primitives/ @symbiote-native/brightness/solid - createPermissions (Solid says
80
80
  create*, not use*)
81
- src/angular/services/ @symbiote-native/brightness/angular — PermissionsService
81
+ src/angular/services/ @symbiote-native/brightness/angular - PermissionsService
82
82
  ```
83
83
 
84
84
  Each adapter's hook/composable/service wraps the same one-shot `getPermissionsAsync`/
85
85
  `requestPermissionsAsync` pair in its own lifecycle idiom (auto-fetch on mount, exposed as a
86
- tuple/object/accessor/signal). Every other export — brightness get/set, the system-brightness-mode
87
- surface, the listener — is a stateless free-function re-export, written once in `core/` and shared
86
+ tuple/object/accessor/signal). Every other export - brightness get/set, the system-brightness-mode
87
+ surface, the listener - is a stateless free-function re-export, written once in `core/` and shared
88
88
  verbatim by every adapter.
89
89
 
90
90
  ## Use it
91
91
 
92
92
  ```tsx
93
- // React — trimmed from examples/expo-react/screens/BrightnessScreen.tsx
93
+ // React - trimmed from examples/expo-react/screens/BrightnessScreen.tsx
94
94
  import { useEffect, useState } from 'react';
95
95
  import {
96
96
  addBrightnessListener,
@@ -105,7 +105,7 @@ function BrightnessScreen() {
105
105
 
106
106
  useEffect(() => {
107
107
  getBrightnessAsync().then(setBrightness);
108
- // iOS only — never fires on Android, so there the value only changes via setBrightnessAsync.
108
+ // iOS only - never fires on Android, so there the value only changes via setBrightnessAsync.
109
109
  const subscription = addBrightnessListener(event =>
110
110
  setBrightness(event.brightness),
111
111
  );
@@ -130,7 +130,7 @@ function BrightnessScreen() {
130
130
  ```
131
131
 
132
132
  ```vue
133
- <!-- Vue — trimmed from examples/expo-vue-sfc/screens/BrightnessScreen.vue -->
133
+ <!-- Vue - trimmed from examples/expo-vue-sfc/screens/BrightnessScreen.vue -->
134
134
  <script setup lang="ts">
135
135
  import { onMounted, onUnmounted, ref } from 'vue';
136
136
  import {
@@ -173,7 +173,7 @@ onUnmounted(() => subscription?.remove());
173
173
  ```
174
174
 
175
175
  ```svelte
176
- <!-- Svelte — trimmed from examples/expo-svelte/screens/BrightnessScreen.svelte -->
176
+ <!-- Svelte - trimmed from examples/expo-svelte/screens/BrightnessScreen.svelte -->
177
177
  <script lang="ts">
178
178
  import {
179
179
  addBrightnessListener,
@@ -214,7 +214,7 @@ onUnmounted(() => subscription?.remove());
214
214
  ```
215
215
 
216
216
  ```tsx
217
- // Solid — trimmed from examples/expo-solid/screens/BrightnessScreen.tsx
217
+ // Solid - trimmed from examples/expo-solid/screens/BrightnessScreen.tsx
218
218
  import { createSignal, onCleanup } from 'solid-js';
219
219
  import {
220
220
  addBrightnessListener,
@@ -254,7 +254,7 @@ function BrightnessScreen() {
254
254
  ```
255
255
 
256
256
  ```ts
257
- // Angular — trimmed from examples/expo-angular/src/screens/BrightnessScreen.ts
257
+ // Angular - trimmed from examples/expo-angular/src/screens/BrightnessScreen.ts
258
258
  import { Component, inject, signal } from '@angular/core';
259
259
  import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
260
260
  import {
@@ -307,7 +307,7 @@ export class BrightnessScreen {
307
307
  The real demo screens (`examples/expo-react/screens/BrightnessScreen.tsx` and its Vue/Svelte/
308
308
  Solid/Angular twins in `examples/expo-vue-sfc`, `examples/expo-vue-tsx`, `examples/expo-svelte`,
309
309
  `examples/expo-solid`, `examples/expo-angular`) additionally
310
- gate an Android-only system-brightness-mode card behind `Platform.OS === 'android'` — see
310
+ gate an Android-only system-brightness-mode card behind `Platform.OS === 'android'` - see
311
311
  [API](#api) below for that surface's full signatures.
312
312
 
313
313
  ## API
@@ -317,7 +317,7 @@ its own lifecycle hook/composable/service (matching upstream's own `usePermissio
317
317
 
318
318
  ```ts
319
319
  isAvailableAsync(): Promise<boolean>
320
- // Whether the brightness API is available — !!native.getBrightnessAsync.
320
+ // Whether the brightness API is available - !!native.getBrightnessAsync.
321
321
 
322
322
  getBrightnessAsync(): Promise<number>
323
323
  setBrightnessAsync(brightnessValue: number): Promise<void>
@@ -325,26 +325,26 @@ setBrightnessAsync(brightnessValue: number): Promise<void>
325
325
 
326
326
  getSystemBrightnessAsync(): Promise<number>
327
327
  setSystemBrightnessAsync(brightnessValue: number): Promise<void>
328
- // System-wide brightness. On iOS these delegate straight to get/setBrightnessAsync — there is
328
+ // System-wide brightness. On iOS these delegate straight to get/setBrightnessAsync - there is
329
329
  // no separate system-level value on that platform.
330
330
 
331
331
  restoreSystemBrightnessAsync(): Promise<void>
332
332
  isUsingSystemBrightnessAsync(): Promise<boolean>
333
333
  getSystemBrightnessModeAsync(): Promise<BrightnessMode> // UNKNOWN / AUTOMATIC / MANUAL
334
334
  setSystemBrightnessModeAsync(mode: BrightnessMode): Promise<void>
335
- // Android only — every one of these is a no-op (or resolves a fixed fallback) on iOS/other
335
+ // Android only - every one of these is a no-op (or resolves a fixed fallback) on iOS/other
336
336
  // platforms. setSystemBrightnessModeAsync(UNKNOWN) is also always a no-op, even on Android.
337
337
 
338
338
  getPermissionsAsync(): Promise<PermissionResponse>
339
339
  requestPermissionsAsync(): Promise<PermissionResponse>
340
- // Plain passthroughs to the native module — no platform gating.
340
+ // Plain passthroughs to the native module - no platform gating.
341
341
 
342
342
  addBrightnessListener(listener: (event: BrightnessEvent) => void): EventSubscription
343
- // @platform ios — fires only on iOS; never fires on Android or web.
343
+ // @platform ios - fires only on iOS; never fires on Android or web.
344
344
  ```
345
345
 
346
346
  Plus `BrightnessMode` (enum: `UNKNOWN`/`AUTOMATIC`/`MANUAL`), `BrightnessEvent`, and
347
- `PermissionStatus`/`PermissionResponse`/`PermissionExpiration` — the latter three re-exported
347
+ `PermissionStatus`/`PermissionResponse`/`PermissionExpiration` - the latter three re-exported
348
348
  verbatim from `expo-modules-core`, never the `expo` meta-package.
349
349
 
350
350
  ```ts
@@ -375,10 +375,10 @@ const [status, request] = usePermissions();
375
375
  // Vue
376
376
  const { status, request } = usePermissions();
377
377
 
378
- // Svelte — a boxed getter, read as permissions.status
378
+ // Svelte - a boxed getter, read as permissions.status
379
379
  const permissions = usePermissions();
380
380
 
381
- // Solid — accessors, so status() is what a tracked scope reads
381
+ // Solid - accessors, so status() is what a tracked scope reads
382
382
  const { status, request } = createPermissions();
383
383
 
384
384
  // Angular
@@ -392,32 +392,43 @@ readonly status = inject(PermissionsService).connect();
392
392
  is no separate system-level brightness there); `restoreSystemBrightnessAsync`/
393
393
  `setSystemBrightnessModeAsync` no-op, `isUsingSystemBrightnessAsync` resolves `false`, and
394
394
  `getSystemBrightnessModeAsync` resolves `BrightnessMode.UNKNOWN` on every other platform.
395
- - **`addBrightnessListener` is iOS only.** It never fires on Android or web — an app relying on
395
+ - **`addBrightnessListener` is iOS only.** It never fires on Android or web - an app relying on
396
396
  it for cross-platform behavior needs its own fallback (e.g. polling `getBrightnessAsync`).
397
- - **The iOS Simulator never round-trips brightness at all.** `UIScreen.main.brightness` — what
398
- `getBrightnessAsync`/`setBrightnessAsync` read and write on iOS — isn't implemented by the
397
+ - **The iOS Simulator never round-trips brightness at all.** `UIScreen.main.brightness` - what
398
+ `getBrightnessAsync`/`setBrightnessAsync` read and write on iOS - isn't implemented by the
399
399
  Simulator: `set` is a silent no-op and `get` always resolves a constant (observed `1.0`),
400
400
  regardless of what was written. This is a long-standing Apple Simulator limitation reproducible
401
- in any app, including stock Expo Go — not a bug in this wrapper. It only reproduces on a real
401
+ in any app, including stock Expo Go - not a bug in this wrapper. It only reproduces on a real
402
402
  iOS device; Android's emulator has no equivalent gap (see the `symbiote-expo-native-module`
403
403
  skill for the verified symptom and the underlying `BrightnessModule.swift` call).
404
404
 
405
+ ## Common questions
406
+
407
+ - **iOS change disappears.** `setBrightnessAsync` persists only until the device locks.
408
+ - **Android works only while the app is open.** It applies to the current activity; call
409
+ `restoreSystemBrightnessAsync` to return to the system value.
410
+ - **System brightness on Android.** `setSystemBrightnessAsync` (experimental) needs `WRITE_SETTINGS`.
411
+ - **Permission result empty after granting.** Reported upstream; re-query after returning from settings.
412
+
413
+ Sources: [Expo docs: Brightness](https://docs.expo.dev/versions/latest/sdk/brightness/),
414
+ [expo/expo#18967](https://github.com/expo/expo/issues/18967).
415
+
405
416
  ## Test it
406
417
 
407
- No Fabric/Descriptor angle at all — brightness is a pure async-function + listener + permission
418
+ No Fabric/Descriptor angle at all - brightness is a pure async-function + listener + permission
408
419
  surface, never a view. Tests inject a fake native-module object in place of the real
409
420
  `requireNativeModule` resolution (`src/core/*.test.ts`,
410
421
  `src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`,
411
- `vitest`), the same pattern `expo-brightness` itself uses upstream — no `installFabric()`, no
422
+ `vitest`), the same pattern `expo-brightness` itself uses upstream - no `installFabric()`, no
412
423
  ViewConfig. Native rendering itself is verified on-device (see the parent
413
424
  [README](../../README.md) for the project's testing model).
414
425
 
415
426
  ## Known gaps
416
427
 
417
- - Not yet wired into the public, non-Expo `examples/react` canary — only in `examples/expo-react`
428
+ - Not yet wired into the public, non-Expo `examples/react` canary - only in `examples/expo-react`
418
429
  (and its Vue/Svelte/Solid/Angular Expo twins, `examples/expo-vue-sfc`/`examples/expo-vue-tsx`/
419
430
  `examples/expo-svelte`/`examples/expo-solid`/`examples/expo-angular`, all of which already have
420
431
  full three-layer native wiring: Android `MainApplication.kt` registration + `WRITE_SETTINGS`,
421
432
  plus the iOS Podfile/pod install).
422
- - Tests exercise the JS layer only (fake native module, `vitest`) — no on-device/simulator
433
+ - Tests exercise the JS layer only (fake native module, `vitest`) - no on-device/simulator
423
434
  automated smoke test yet, only manual verification.
@@ -1,12 +1,5 @@
1
- import { type Signal } from '@angular/core';
1
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
2
2
  import { type PermissionResponse } from '../../../core';
3
- export declare class PermissionsService {
4
- private readonly status;
5
- private readonly fetchError;
6
- private isAutoFetchStarted;
7
- /** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
8
- readonly error: Signal<Error | null>;
9
- connect(): Signal<PermissionResponse | null>;
10
- get(): Promise<PermissionResponse>;
11
- request(): Promise<PermissionResponse>;
3
+ export declare class PermissionsService extends PermissionsServiceBase<PermissionResponse> {
4
+ constructor();
12
5
  }
@@ -1,3 +1,4 @@
1
+ // Angular twin of React's `usePermissions`: `inject(PermissionsService).connect()` gives a signal
1
2
  var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
3
  function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
4
  var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
@@ -32,62 +33,26 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
32
33
  }
33
34
  return useValue ? value : void 0;
34
35
  };
35
- // Angular twin of React's usePermissions hook and Vue's usePermissions composable.
36
- // `connect()` auto-fetches once (no ongoing subscription to clean up, so no Injector/effect
37
- // needed, unlike BatteryStateService) and returns a readonly signal; get()/request() are
38
- // imperative one-shot methods.
39
- //
40
- // readonly status = inject(PermissionsService).connect();
41
- // // template: {{ status()?.granted }}
42
- //
43
- // The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
44
- // escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
45
- // "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
46
- // only the automatic call is made safe.
47
- import { Injectable, signal } from '@angular/core';
36
+ import { Injectable } from '@angular/core';
37
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
48
38
  import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core/index.js';
49
39
  let PermissionsService = (() => {
50
40
  let _classDecorators = [Injectable({ providedIn: 'root' })];
51
41
  let _classDescriptor;
52
42
  let _classExtraInitializers = [];
53
43
  let _classThis;
54
- var PermissionsService = class {
44
+ let _classSuper = PermissionsServiceBase;
45
+ var PermissionsService = class extends _classSuper {
55
46
  static { _classThis = this; }
56
47
  static {
57
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
58
49
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
59
50
  PermissionsService = _classThis = _classDescriptor.value;
60
51
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
61
52
  __runInitializers(_classThis, _classExtraInitializers);
62
53
  }
63
- status = signal(null);
64
- fetchError = signal(null);
65
- isAutoFetchStarted = false;
66
- /** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
67
- error = this.fetchError.asReadonly();
68
- connect() {
69
- // Latched rather than guarded on `status() === null`: a failed fetch leaves status null
70
- // forever, so the old guard turned every later connect() into another native call — unbounded
71
- // once connect() is reached from a change-detected expression instead of a field initializer.
72
- if (!this.isAutoFetchStarted) {
73
- this.isAutoFetchStarted = true;
74
- this.get().catch((cause) => {
75
- this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
76
- });
77
- }
78
- return this.status.asReadonly();
79
- }
80
- async get() {
81
- const response = await getPermissionsAsync();
82
- this.status.set(response);
83
- this.fetchError.set(null);
84
- return response;
85
- }
86
- async request() {
87
- const response = await requestPermissionsAsync();
88
- this.status.set(response);
89
- this.fetchError.set(null);
90
- return response;
54
+ constructor() {
55
+ super(getPermissionsAsync, requestPermissionsAsync);
91
56
  }
92
57
  };
93
58
  return PermissionsService = _classThis;
@@ -1,15 +1,8 @@
1
- import { type Signal } from '@angular/core';
1
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
2
2
  import { type PermissionResponse } from '../../../core';
3
3
  import * as i0 from "@angular/core";
4
- export declare class PermissionsService {
5
- private readonly status;
6
- private readonly fetchError;
7
- private isAutoFetchStarted;
8
- /** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
9
- readonly error: Signal<Error | null>;
10
- connect(): Signal<PermissionResponse | null>;
11
- get(): Promise<PermissionResponse>;
12
- request(): Promise<PermissionResponse>;
4
+ export declare class PermissionsService extends PermissionsServiceBase<PermissionResponse> {
5
+ constructor();
13
6
  static ɵfac: i0.ɵɵFactoryDeclaration<PermissionsService, never>;
14
7
  static ɵprov: i0.ɵɵInjectableDeclaration<PermissionsService>;
15
8
  }
@@ -1,49 +1,11 @@
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
- //
9
- // The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
10
- // escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
11
- // "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
12
- // only the automatic call is made safe.
13
- import { Injectable, signal } from '@angular/core';
1
+ // Angular twin of React's `usePermissions`: `inject(PermissionsService).connect()` gives a signal
2
+ import { Injectable } from '@angular/core';
3
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
14
4
  import { getPermissionsAsync, requestPermissionsAsync, } from '../../../core';
15
5
  import * as i0 from "@angular/core";
16
- export class PermissionsService {
17
- status = signal(null, /* @ts-ignore */
18
- ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
19
- fetchError = signal(null, /* @ts-ignore */
20
- ...(ngDevMode ? [{ debugName: "fetchError" }] : /* istanbul ignore next */ []));
21
- isAutoFetchStarted = false;
22
- /** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
23
- error = this.fetchError.asReadonly();
24
- connect() {
25
- // Latched rather than guarded on `status() === null`: a failed fetch leaves status null
26
- // forever, so the old guard turned every later connect() into another native call — unbounded
27
- // once connect() is reached from a change-detected expression instead of a field initializer.
28
- if (!this.isAutoFetchStarted) {
29
- this.isAutoFetchStarted = true;
30
- this.get().catch((cause) => {
31
- this.fetchError.set(cause instanceof Error ? cause : new Error(String(cause)));
32
- });
33
- }
34
- return this.status.asReadonly();
35
- }
36
- async get() {
37
- const response = await getPermissionsAsync();
38
- this.status.set(response);
39
- this.fetchError.set(null);
40
- return response;
41
- }
42
- async request() {
43
- const response = await requestPermissionsAsync();
44
- this.status.set(response);
45
- this.fetchError.set(null);
46
- return response;
6
+ export class PermissionsService extends PermissionsServiceBase {
7
+ constructor() {
8
+ super(getPermissionsAsync, requestPermissionsAsync);
47
9
  }
48
10
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
49
11
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, providedIn: 'root' });
@@ -51,5 +13,5 @@ export class PermissionsService {
51
13
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PermissionsService, decorators: [{
52
14
  type: Injectable,
53
15
  args: [{ providedIn: 'root' }]
54
- }] });
16
+ }], ctorParameters: () => [] });
55
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/permissions.service/index.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,4FAA4F;AAC5F,yFAAyF;AACzF,+BAA+B;AAC/B,EAAE;AACF,4DAA4D;AAC5D,yCAAyC;AACzC,EAAE;AACF,gGAAgG;AAChG,kGAAkG;AAClG,mGAAmG;AACnG,wCAAwC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AAChE,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAExB,MAAM,eAAe,CAAC;;AAGvB,MAAM,OAAO,kBAAkB;IACZ,MAAM,GAAG,MAAM,CAA4B,IAAI;+EAAC,CAAC;IACjD,UAAU,GAAG,MAAM,CAAe,IAAI;mFAAC,CAAC;IACjD,kBAAkB,GAAG,KAAK,CAAC;IAEnC,gGAAgG;IACvF,KAAK,GAAyB,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC;IAEpE,OAAO;QACL,wFAAwF;QACxF,8FAA8F;QAC9F,8FAA8F;QAC9F,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC7B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;YAC/B,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBAClC,IAAI,CAAC,UAAU,CAAC,GAAG,CACjB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAC1D,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,GAAG;QACP,MAAM,QAAQ,GAAG,MAAM,mBAAmB,EAAE,CAAC;QAC7C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,QAAQ,GAAG,MAAM,uBAAuB,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAClB,CAAC;uGAnCU,kBAAkB;2GAAlB,kBAAkB,cADL,MAAM;;2FACnB,kBAAkB;kBAD9B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/permissions.service/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAElG,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACL,mBAAmB,EACnB,uBAAuB,GAExB,MAAM,eAAe,CAAC;;AAGvB,MAAM,OAAO,kBAAmB,SAAQ,sBAA0C;IAChF;QACE,KAAK,CAAC,mBAAmB,EAAE,uBAAuB,CAAC,CAAC;IACtD,CAAC;uGAHU,kBAAkB;2GAAlB,kBAAkB,cADL,MAAM;;2FACnB,kBAAkB;kBAD9B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/brightness",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "expo-brightness wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Screen brightness get/set, Android-only system-brightness-mode control, an iOS-only brightness-change listener, and permission get/request.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "expo-brightness": "57.0.1",
73
- "expo-modules-core": "57.0.5"
73
+ "expo-modules-core": "57.0.20"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "@angular/core": ">=20",
@@ -80,12 +80,12 @@
80
80
  "solid-js": ">=1.9.0",
81
81
  "svelte": ">=5.56.0",
82
82
  "vue": ">=3.5.0",
83
- "@symbiote-native/angular": "^3.1.1",
84
- "@symbiote-native/engine": "^1.3.0",
85
- "@symbiote-native/react": "^3.0.3",
86
- "@symbiote-native/solid": "^3.0.3",
87
- "@symbiote-native/svelte": "^3.0.3",
88
- "@symbiote-native/vue": "^3.0.3"
83
+ "@symbiote-native/angular": "^3.2.0",
84
+ "@symbiote-native/engine": "^1.5.0",
85
+ "@symbiote-native/react": "^3.2.0",
86
+ "@symbiote-native/solid": "^3.1.0",
87
+ "@symbiote-native/svelte": "^3.1.0",
88
+ "@symbiote-native/vue": "^3.2.0"
89
89
  },
90
90
  "peerDependenciesMeta": {
91
91
  "@symbiote-native/angular": {
@@ -133,13 +133,13 @@
133
133
  "solid-js": "^1.9.14",
134
134
  "svelte": "^5.56.0",
135
135
  "typescript": "~6.0.0",
136
- "@symbiote-native/angular": "3.1.1",
137
- "@symbiote-native/engine": "1.3.0",
138
- "@symbiote-native/react": "3.0.3",
139
- "@symbiote-native/solid": "3.0.3",
140
- "@symbiote-native/svelte": "3.0.3",
141
- "@symbiote-native/test-utils": "0.4.3",
142
- "@symbiote-native/vue": "3.0.3"
136
+ "@symbiote-native/angular": "3.2.0",
137
+ "@symbiote-native/engine": "1.5.0",
138
+ "@symbiote-native/react": "3.2.0",
139
+ "@symbiote-native/solid": "3.1.0",
140
+ "@symbiote-native/svelte": "3.1.0",
141
+ "@symbiote-native/test-utils": "0.4.6",
142
+ "@symbiote-native/vue": "3.2.0"
143
143
  },
144
144
  "scripts": {
145
145
  "typecheck": "tsc --build",
@@ -1,16 +1,7 @@
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
- //
9
- // The auto-fetch has nobody to reject to, so its failure lands in the `error` signal instead of
10
- // escaping as an unhandled rejection: a null status with a non-null error is how a consumer tells
11
- // "the fetch failed" from "not fetched yet". get()/request() still reject to their direct caller —
12
- // only the automatic call is made safe.
13
- import { Injectable, signal, type Signal } from '@angular/core';
1
+ // Angular twin of React's `usePermissions`: `inject(PermissionsService).connect()` gives a signal
2
+
3
+ import { Injectable } from '@angular/core';
4
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
14
5
  import {
15
6
  getPermissionsAsync,
16
7
  requestPermissionsAsync,
@@ -18,40 +9,8 @@ import {
18
9
  } from '../../../core';
19
10
 
20
11
  @Injectable({ providedIn: 'root' })
21
- export class PermissionsService {
22
- private readonly status = signal<PermissionResponse | null>(null);
23
- private readonly fetchError = signal<Error | null>(null);
24
- private isAutoFetchStarted = false;
25
-
26
- /** Why the auto-fetch left `status` at null. Cleared by the next successful get()/request(). */
27
- readonly error: Signal<Error | null> = this.fetchError.asReadonly();
28
-
29
- connect(): Signal<PermissionResponse | null> {
30
- // Latched rather than guarded on `status() === null`: a failed fetch leaves status null
31
- // forever, so the old guard turned every later connect() into another native call — unbounded
32
- // once connect() is reached from a change-detected expression instead of a field initializer.
33
- if (!this.isAutoFetchStarted) {
34
- this.isAutoFetchStarted = true;
35
- this.get().catch((cause: unknown) => {
36
- this.fetchError.set(
37
- cause instanceof Error ? cause : new Error(String(cause)),
38
- );
39
- });
40
- }
41
- return this.status.asReadonly();
42
- }
43
-
44
- async get(): Promise<PermissionResponse> {
45
- const response = await getPermissionsAsync();
46
- this.status.set(response);
47
- this.fetchError.set(null);
48
- return response;
49
- }
50
-
51
- async request(): Promise<PermissionResponse> {
52
- const response = await requestPermissionsAsync();
53
- this.status.set(response);
54
- this.fetchError.set(null);
55
- return response;
12
+ export class PermissionsService extends PermissionsServiceBase<PermissionResponse> {
13
+ constructor() {
14
+ super(getPermissionsAsync, requestPermissionsAsync);
56
15
  }
57
16
  }