@ng-native/expo 0.15.0 → 0.17.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.
@@ -6,7 +6,8 @@
6
6
  * This is what a camera preview or a video player needs - a screen locked to portrait still has a
7
7
  * phone that is sideways.
8
8
  */
9
- import { InjectionToken, Service, computed, inject } from '@angular/core';
9
+ import { DestroyRef, InjectionToken, Service, computed, inject } from '@angular/core';
10
+ import { TopClaim, onWindowFront, sharedBySource } from '@ng-native/device';
10
11
  import { observed, observedFrom } from "./observed.js";
11
12
  import { expoModule, present } from "./native.js";
12
13
  import * as i0 from "@angular/core";
@@ -38,17 +39,30 @@ export class DeviceOrientation {
38
39
  portrait: expo.OrientationLock.PORTRAIT,
39
40
  landscape: expo.OrientationLock.LANDSCAPE,
40
41
  };
41
- return {
42
+ // One per module, so every app the runtime mounts locks the same screen.
43
+ return sharedBySource(expo, () => ({
42
44
  reported: observedFrom(async () => ORIENTATIONS[await expo.getOrientationAsync()] ?? 'unknown', (listener) => expo.addOrientationChangeListener(({ orientationInfo }) => listener(ORIENTATIONS[orientationInfo.orientation] ?? 'unknown'))),
43
45
  lock: (lock) => expo.lockAsync(LOCKS[lock]),
44
46
  unlock: () => expo.unlockAsync(),
45
- };
47
+ onWindowFront,
48
+ }));
46
49
  },
47
50
  });
48
51
  source = inject(DeviceOrientation.SOURCE);
49
52
  orientation = observed(this.source.reported, 'unknown');
50
53
  landscape = computed(() => this.orientation().startsWith('landscape'), /* @ts-ignore */
51
54
  ...(ngDevMode ? [{ debugName: "landscape" }] : /* istanbul ignore next */ []));
55
+ /** Every app's locks: the screen is the runtime's, not an app's. */
56
+ window = sharedBySource(this.source, () => lockWindow(this.source));
57
+ /** This app, among the apps locking the screen. */
58
+ owner = {};
59
+ constructor() {
60
+ inject(DestroyRef).onDestroy(() => {
61
+ // An app that goes takes its locks with it, and leaves the screen to the apps still here.
62
+ this.window.claims.drop(this.owner);
63
+ this.window.apply();
64
+ });
65
+ }
52
66
  /**
53
67
  * Pin the screen. Returns the function that unpins it, so a screen that wants landscape while
54
68
  * it is up can hand it to `DestroyRef.onDestroy` and not think about it again.
@@ -56,26 +70,20 @@ export class DeviceOrientation {
56
70
  lock(lock) {
57
71
  // A stack, as the status bar's is: a screen pushed over one with a lock of its own releases
58
72
  // only its own, and the lock beneath it comes back rather than the whole app unlocking.
59
- const entry = { lock };
60
- this.locks.push(entry);
61
- void this.source.lock(lock);
73
+ const drop = this.window.claims.push(this.owner, lock);
74
+ this.window.apply();
62
75
  return () => {
63
- const at = this.locks.indexOf(entry);
64
- if (at === -1)
65
- return;
66
- this.locks.splice(at, 1);
67
- if (at < this.locks.length)
68
- return;
69
- const below = this.locks.at(-1);
70
- void (below ? this.source.lock(below.lock) : this.source.unlock());
76
+ drop();
77
+ this.window.apply();
71
78
  };
72
79
  }
73
- /** Every lock not yet released, the one in force last. */
74
- locks = [];
75
80
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation, deps: [], target: i0.ɵɵFactoryTarget.Service });
76
81
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation });
77
82
  }
78
83
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DeviceOrientation, decorators: [{
79
84
  type: Service
80
- }] });
85
+ }], ctorParameters: () => [] });
86
+ function lockWindow(source) {
87
+ return new TopClaim((lock) => void source.lock(lock), () => void source.unlock(), source.onWindowFront);
88
+ }
81
89
  //# sourceMappingURL=orientation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"orientation.js","sourceRoot":"","sources":["../src/orientation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `DeviceOrientation`, bound to `expo-screen-orientation`.\n *\n * Named for the device rather than the screen because that is what it reports: a layout asking\n * which way *it* is laid out wants the `orientation` media feature, which needs none of this.\n * This is what a camera preview or a video player needs - a screen locked to portrait still has a\n * phone that is sideways.\n */\nimport { InjectionToken, Service, computed, inject, type Signal } from '@angular/core';\nimport { observed, observedFrom, type Observed } from './observed.ts';\nimport { expoModule, present } from './native.ts';\n\nexport type Orientation =\n 'unknown' | 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';\n\n/** What a screen may rotate to. `default` is portrait plus, on a phone, nothing else. */\nexport type OrientationLock = 'default' | 'all' | 'portrait' | 'landscape';\n\nexport interface OrientationSource {\n readonly reported: Observed<Orientation> | null;\n lock(lock: OrientationLock): Promise<void>;\n unlock(): Promise<void>;\n}\n\nconst NOTHING: OrientationSource = {\n reported: null,\n lock: () => Promise.resolve(),\n unlock: () => Promise.resolve(),\n};\n\n@Service()\nexport class DeviceOrientation {\n /** Overridden in a test to rotate a device that is not there. */\n static readonly SOURCE = new InjectionToken<OrientationSource>(\n 'angular-native.orientationSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-screen-orientation',\n () => require('expo-screen-orientation') as typeof import('expo-screen-orientation'),\n );\n if (!expo) return NOTHING;\n // Missing from the build: stand-ins, to say so when they are used.\n if (!present(expo)) return { reported: expo, lock: expo, unlock: expo } as never;\n\n const ORIENTATIONS: Record<number, Orientation> = {\n [expo.Orientation.UNKNOWN]: 'unknown',\n [expo.Orientation.PORTRAIT_UP]: 'portrait',\n [expo.Orientation.PORTRAIT_DOWN]: 'portrait-upside-down',\n [expo.Orientation.LANDSCAPE_LEFT]: 'landscape-left',\n [expo.Orientation.LANDSCAPE_RIGHT]: 'landscape-right',\n };\n const LOCKS: Record<OrientationLock, import('expo-screen-orientation').OrientationLock> = {\n default: expo.OrientationLock.DEFAULT,\n all: expo.OrientationLock.ALL,\n portrait: expo.OrientationLock.PORTRAIT,\n landscape: expo.OrientationLock.LANDSCAPE,\n };\n\n return {\n reported: observedFrom(\n async () => ORIENTATIONS[await expo.getOrientationAsync()] ?? 'unknown',\n (listener) =>\n expo.addOrientationChangeListener(({ orientationInfo }) =>\n listener(ORIENTATIONS[orientationInfo.orientation] ?? 'unknown'),\n ),\n ),\n lock: (lock) => expo.lockAsync(LOCKS[lock]),\n unlock: () => expo.unlockAsync(),\n };\n },\n },\n );\n\n private readonly source = inject(DeviceOrientation.SOURCE);\n\n readonly orientation: Signal<Orientation> = observed(this.source.reported, 'unknown');\n\n readonly landscape: Signal<boolean> = computed(() => this.orientation().startsWith('landscape'));\n\n /**\n * Pin the screen. Returns the function that unpins it, so a screen that wants landscape while\n * it is up can hand it to `DestroyRef.onDestroy` and not think about it again.\n */\n lock(lock: OrientationLock): () => void {\n // A stack, as the status bar's is: a screen pushed over one with a lock of its own releases\n // only its own, and the lock beneath it comes back rather than the whole app unlocking.\n const entry = { lock };\n this.locks.push(entry);\n void this.source.lock(lock);\n return () => {\n const at = this.locks.indexOf(entry);\n if (at === -1) return;\n this.locks.splice(at, 1);\n if (at < this.locks.length) return;\n const below = this.locks.at(-1);\n void (below ? this.source.lock(below.lock) : this.source.unlock());\n };\n }\n\n /** Every lock not yet released, the one in force last. */\n private readonly locks: { readonly lock: OrientationLock }[] = [];\n}\n"]}
1
+ {"version":3,"file":"orientation.js","sourceRoot":"","sources":["../src/orientation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `DeviceOrientation`, bound to `expo-screen-orientation`.\n *\n * Named for the device rather than the screen because that is what it reports: a layout asking\n * which way *it* is laid out wants the `orientation` media feature, which needs none of this.\n * This is what a camera preview or a video player needs - a screen locked to portrait still has a\n * phone that is sideways.\n */\nimport { DestroyRef, InjectionToken, Service, computed, inject, type Signal } from '@angular/core';\nimport { TopClaim, onWindowFront, sharedBySource } from '@ng-native/device';\nimport { observed, observedFrom, type Observed } from './observed.ts';\nimport { expoModule, present } from './native.ts';\n\nexport type Orientation =\n 'unknown' | 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';\n\n/** What a screen may rotate to. `default` is portrait plus, on a phone, nothing else. */\nexport type OrientationLock = 'default' | 'all' | 'portrait' | 'landscape';\n\nexport interface OrientationSource {\n readonly reported: Observed<Orientation> | null;\n lock(lock: OrientationLock): Promise<void>;\n unlock(): Promise<void>;\n /** Calls `listener` when a window comes to the front, to be locked again. */\n onWindowFront?(listener: () => void): () => void;\n}\n\nconst NOTHING: OrientationSource = {\n reported: null,\n lock: () => Promise.resolve(),\n unlock: () => Promise.resolve(),\n};\n\n@Service()\nexport class DeviceOrientation {\n /** Overridden in a test to rotate a device that is not there. */\n static readonly SOURCE = new InjectionToken<OrientationSource>(\n 'angular-native.orientationSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-screen-orientation',\n () => require('expo-screen-orientation') as typeof import('expo-screen-orientation'),\n );\n if (!expo) return NOTHING;\n // Missing from the build: stand-ins, to say so when they are used.\n if (!present(expo)) return { reported: expo, lock: expo, unlock: expo } as never;\n\n const ORIENTATIONS: Record<number, Orientation> = {\n [expo.Orientation.UNKNOWN]: 'unknown',\n [expo.Orientation.PORTRAIT_UP]: 'portrait',\n [expo.Orientation.PORTRAIT_DOWN]: 'portrait-upside-down',\n [expo.Orientation.LANDSCAPE_LEFT]: 'landscape-left',\n [expo.Orientation.LANDSCAPE_RIGHT]: 'landscape-right',\n };\n const LOCKS: Record<OrientationLock, import('expo-screen-orientation').OrientationLock> = {\n default: expo.OrientationLock.DEFAULT,\n all: expo.OrientationLock.ALL,\n portrait: expo.OrientationLock.PORTRAIT,\n landscape: expo.OrientationLock.LANDSCAPE,\n };\n\n // One per module, so every app the runtime mounts locks the same screen.\n return sharedBySource(expo, () => ({\n reported: observedFrom(\n async () => ORIENTATIONS[await expo.getOrientationAsync()] ?? 'unknown',\n (listener) =>\n expo.addOrientationChangeListener(({ orientationInfo }) =>\n listener(ORIENTATIONS[orientationInfo.orientation] ?? 'unknown'),\n ),\n ),\n lock: (lock) => expo.lockAsync(LOCKS[lock]),\n unlock: () => expo.unlockAsync(),\n onWindowFront,\n }));\n },\n },\n );\n\n private readonly source = inject(DeviceOrientation.SOURCE);\n\n readonly orientation: Signal<Orientation> = observed(this.source.reported, 'unknown');\n\n readonly landscape: Signal<boolean> = computed(() => this.orientation().startsWith('landscape'));\n\n /** Every app's locks: the screen is the runtime's, not an app's. */\n private readonly window = sharedBySource(this.source, () => lockWindow(this.source));\n /** This app, among the apps locking the screen. */\n private readonly owner = {};\n\n constructor() {\n inject(DestroyRef).onDestroy(() => {\n // An app that goes takes its locks with it, and leaves the screen to the apps still here.\n this.window.claims.drop(this.owner);\n this.window.apply();\n });\n }\n\n /**\n * Pin the screen. Returns the function that unpins it, so a screen that wants landscape while\n * it is up can hand it to `DestroyRef.onDestroy` and not think about it again.\n */\n lock(lock: OrientationLock): () => void {\n // A stack, as the status bar's is: a screen pushed over one with a lock of its own releases\n // only its own, and the lock beneath it comes back rather than the whole app unlocking.\n const drop = this.window.claims.push(this.owner, lock);\n this.window.apply();\n return () => {\n drop();\n this.window.apply();\n };\n }\n}\n\nfunction lockWindow(source: OrientationSource): TopClaim<OrientationLock> {\n return new TopClaim(\n (lock) => void source.lock(lock),\n () => void source.unlock(),\n source.onWindowFront,\n );\n}\n"]}
@@ -12,7 +12,8 @@
12
12
  * ```
13
13
  *
14
14
  * `prevent()` and `allow()` take a key, and capture stays prevented while any key is still held:
15
- * two screens that each prevent it do not undo each other. `screenshots` counts the screenshots
15
+ * two screens that each prevent it do not undo each other. Keys are the runtime's, as the window is:
16
+ * an app that goes releases the keys it holds, and leaves those another app holds. `screenshots` counts the screenshots
16
17
  * the user has taken, and the listener behind it lives as long as the app does and is removed
17
18
  * when the app is destroyed. Without the module installed, nothing is prevented and the count
18
19
  * stays at zero.
@@ -22,12 +23,24 @@ import { Permission } from './permissions.ts';
22
23
  import * as i0 from "@angular/core";
23
24
  type Expo = typeof import('expo-screen-capture');
24
25
  /** The module's functions this service calls. The real module is one; a test provides a fake. */
25
- export type NativeScreenCapture = Pick<Expo, 'isAvailableAsync' | 'preventScreenCaptureAsync' | 'allowScreenCaptureAsync' | 'enableAppSwitcherProtectionAsync' | 'disableAppSwitcherProtectionAsync' | 'addScreenshotListener' | 'getPermissionsAsync' | 'requestPermissionsAsync'>;
26
+ export type NativeScreenCapture = Pick<Expo, 'isAvailableAsync' | 'preventScreenCaptureAsync' | 'allowScreenCaptureAsync' | 'enableAppSwitcherProtectionAsync' | 'disableAppSwitcherProtectionAsync' | 'addScreenshotListener' | 'getPermissionsAsync' | 'requestPermissionsAsync'> & {
27
+ /** Calls `listener` when a window comes to the front, to be kept out of capture again. */
28
+ onWindowFront?(listener: () => void): () => void;
29
+ /**
30
+ * Prevents capture on the window now in front, past the module's own record of its keys, which
31
+ * would send nothing for a key it already has.
32
+ */
33
+ preventAgain?(): Promise<void>;
34
+ };
26
35
  export declare class ScreenCapture {
27
36
  /** Overridden in a test to take screenshots nobody took. */
28
37
  static readonly SOURCE: InjectionToken<NativeScreenCapture | null>;
29
38
  private readonly native;
30
39
  private readonly count;
40
+ /** Every app's keys: the window is the runtime's, not an app's. */
41
+ private readonly window;
42
+ /** This app, among the apps holding keys. */
43
+ private readonly owner;
31
44
  /**
32
45
  * The permission screenshot detection needs on Android 13 and earlier, where it reads the
33
46
  * photo library. Later Android needs none, and iOS always answers granted.
@@ -40,7 +53,7 @@ export declare class ScreenCapture {
40
53
  available(): Promise<boolean>;
41
54
  /**
42
55
  * Keeps the app out of screenshots and screen recordings until `allow()` is called with the same
43
- * key, or the app restarts. Omitting the key uses one shared default key.
56
+ * key, or the app goes. Omitting the key uses one shared default key.
44
57
  */
45
58
  prevent(key?: string): Promise<void>;
46
59
  /** Releases the key; capture is allowed again once no key is held. */
@@ -12,23 +12,47 @@
12
12
  * ```
13
13
  *
14
14
  * `prevent()` and `allow()` take a key, and capture stays prevented while any key is still held:
15
- * two screens that each prevent it do not undo each other. `screenshots` counts the screenshots
15
+ * two screens that each prevent it do not undo each other. Keys are the runtime's, as the window is:
16
+ * an app that goes releases the keys it holds, and leaves those another app holds. `screenshots` counts the screenshots
16
17
  * the user has taken, and the listener behind it lives as long as the app does and is removed
17
18
  * when the app is destroyed. Without the module installed, nothing is prevented and the count
18
19
  * stays at zero.
19
20
  */
20
21
  import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
22
+ import { onWindowFront, sharedBySource } from '@ng-native/device';
21
23
  import { expoModule, present } from "./native.js";
22
24
  import { Permission, UNAVAILABLE } from "./permissions.js";
23
25
  import * as i0 from "@angular/core";
26
+ /** The key `expo-screen-capture` uses when none is given. */
27
+ const DEFAULT_KEY = 'default';
24
28
  export class ScreenCapture {
25
29
  /** Overridden in a test to take screenshots nobody took. */
26
30
  static SOURCE = new InjectionToken('angular-native.screenCaptureSource', {
27
- factory: () => expoModule('expo-screen-capture', () => require('expo-screen-capture')),
31
+ factory: () => {
32
+ const expo = expoModule('expo-screen-capture', () => require('expo-screen-capture'));
33
+ const loaded = present(expo);
34
+ if (!loaded)
35
+ return expo;
36
+ // One per module, so every app the runtime mounts holds the same keys.
37
+ return sharedBySource(loaded, () => ({
38
+ ...loaded,
39
+ onWindowFront,
40
+ preventAgain: async () => {
41
+ const modules = globalThis.expo
42
+ ?.modules;
43
+ const native = modules?.['ExpoScreenCapture'];
44
+ await native?.preventScreenCapture?.();
45
+ },
46
+ }));
47
+ },
28
48
  });
29
49
  native = inject(ScreenCapture.SOURCE);
30
50
  count = signal(0, /* @ts-ignore */
31
51
  ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
52
+ /** Every app's keys: the window is the runtime's, not an app's. */
53
+ window = sharedBySource(this.native, () => new CaptureWindow(this.native));
54
+ /** This app, among the apps holding keys. */
55
+ owner = {};
32
56
  /**
33
57
  * The permission screenshot detection needs on Android 13 and earlier, where it reads the
34
58
  * photo library. Later Android needs none, and iOS always answers granted.
@@ -38,8 +62,10 @@ export class ScreenCapture {
38
62
  screenshots = this.count.asReadonly();
39
63
  constructor() {
40
64
  const listener = present(this.native)?.addScreenshotListener(() => this.count.update((n) => n + 1));
41
- if (listener)
42
- inject(DestroyRef).onDestroy(() => listener.remove());
65
+ inject(DestroyRef).onDestroy(() => {
66
+ listener?.remove();
67
+ this.window.leave(this.owner);
68
+ });
43
69
  }
44
70
  /** Whether the platform can prevent capture. */
45
71
  async available() {
@@ -47,14 +73,14 @@ export class ScreenCapture {
47
73
  }
48
74
  /**
49
75
  * Keeps the app out of screenshots and screen recordings until `allow()` is called with the same
50
- * key, or the app restarts. Omitting the key uses one shared default key.
76
+ * key, or the app goes. Omitting the key uses one shared default key.
51
77
  */
52
78
  async prevent(key) {
53
- await this.native?.preventScreenCaptureAsync(key);
79
+ await this.window.prevent(this.owner, key);
54
80
  }
55
81
  /** Releases the key; capture is allowed again once no key is held. */
56
82
  async allow(key) {
57
- await this.native?.allowScreenCaptureAsync(key);
83
+ await this.window.allow(this.owner, key);
58
84
  }
59
85
  /**
60
86
  * iOS: blurs the app while it is not in focus - in the app switcher, in the background, and
@@ -73,4 +99,39 @@ export class ScreenCapture {
73
99
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: ScreenCapture, decorators: [{
74
100
  type: Service
75
101
  }], ctorParameters: () => [] });
102
+ /** Screen capture, as every app on one module holds it. */
103
+ class CaptureWindow {
104
+ /** The apps holding each key. */
105
+ holders = new Map();
106
+ native;
107
+ constructor(native) {
108
+ this.native = native;
109
+ // Lives as long as the module, which is the runtime's.
110
+ present(native)?.onWindowFront?.(() => {
111
+ if (this.holders.size > 0)
112
+ void this.native?.preventAgain?.().catch(() => { });
113
+ });
114
+ }
115
+ async prevent(owner, key) {
116
+ const name = key ?? DEFAULT_KEY;
117
+ const holders = this.holders.get(name) ?? new Set();
118
+ this.holders.set(name, holders.add(owner));
119
+ await this.native?.preventScreenCaptureAsync(key);
120
+ }
121
+ async allow(owner, key) {
122
+ const name = key ?? DEFAULT_KEY;
123
+ const holders = this.holders.get(name);
124
+ holders?.delete(owner);
125
+ // Another app holding the key keeps it: the module would release it for both.
126
+ if (holders && holders.size > 0)
127
+ return;
128
+ this.holders.delete(name);
129
+ await this.native?.allowScreenCaptureAsync(key);
130
+ }
131
+ /** Releases `owner`'s keys, for an app that goes: those another app holds stay held. */
132
+ leave(owner) {
133
+ for (const name of [...this.holders.keys()])
134
+ void this.allow(owner, name).catch(() => { });
135
+ }
136
+ }
76
137
  //# sourceMappingURL=screen-capture.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"screen-capture.js","sourceRoot":"","sources":["../src/screen-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAe,MAAM,eAAe,CAAC;AACjG,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;;AAkB3D,MAAM,OAAO,aAAa;IACxB,4DAA4D;IAC5D,MAAM,CAAU,MAAM,GAAG,IAAI,cAAc,CACzC,oCAAoC,EACpC;QACE,OAAO,EAAE,GAAG,EAAE,CACZ,UAAU,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,qBAAqB,CAAS,CAAC;KAClF,CACF,CAAC;IAEe,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,KAAK,GAAG,MAAM,CAAC,CAAC;8EAAC,CAAC;IAEnC;;;OAGG;IACM,UAAU,GAAe,UAAU,CAAC,EAAE,CAC7C,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAC1E,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,uBAAuB,EAAE,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAC/E,CAAC;IAEF,0EAA0E;IACjE,WAAW,GAAmB,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IAE/D;QACE,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAC,GAAG,EAAE,CAChE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAChC,CAAC;QACF,IAAI,QAAQ;YAAE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,gDAAgD;IAChD,KAAK,CAAC,SAAS;QACb,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,IAAI,KAAK,CAAC;IAC1D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,GAAY;QACxB,MAAM,IAAI,CAAC,MAAM,EAAE,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACpD,CAAC;IAED,sEAAsE;IACtE,KAAK,CAAC,KAAK,CAAC,GAAY;QACtB,MAAM,IAAI,CAAC,MAAM,EAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,kBAAkB,CAAC,aAAsB;QAC7C,MAAM,IAAI,CAAC,MAAM,EAAE,gCAAgC,CAAC,aAAa,CAAC,CAAC;IACrE,CAAC;IAED,KAAK,CAAC,oBAAoB;QACxB,MAAM,IAAI,CAAC,MAAM,EAAE,iCAAiC,EAAE,CAAC;IACzD,CAAC;uGA7DU,aAAa;wGAAb,aAAa;;2FAAb,aAAa;kBADzB,OAAO","sourcesContent":["/**\n * `ScreenCapture`, bound to `expo-screen-capture`: keeping a screen out of screenshots and\n * recordings, and knowing when one was taken.\n *\n * ```ts\n * private readonly capture = inject(ScreenCapture);\n *\n * constructor() {\n * void this.capture.prevent('card-details');\n * inject(DestroyRef).onDestroy(() => void this.capture.allow('card-details'));\n * }\n * ```\n *\n * `prevent()` and `allow()` take a key, and capture stays prevented while any key is still held:\n * two screens that each prevent it do not undo each other. `screenshots` counts the screenshots\n * the user has taken, and the listener behind it lives as long as the app does and is removed\n * when the app is destroyed. Without the module installed, nothing is prevented and the count\n * stays at zero.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { expoModule, present } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-screen-capture');\n\n/** The module's functions this service calls. The real module is one; a test provides a fake. */\nexport type NativeScreenCapture = Pick<\n Expo,\n | 'isAvailableAsync'\n | 'preventScreenCaptureAsync'\n | 'allowScreenCaptureAsync'\n | 'enableAppSwitcherProtectionAsync'\n | 'disableAppSwitcherProtectionAsync'\n | 'addScreenshotListener'\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n>;\n\n@Service()\nexport class ScreenCapture {\n /** Overridden in a test to take screenshots nobody took. */\n static readonly SOURCE = new InjectionToken<NativeScreenCapture | null>(\n 'angular-native.screenCaptureSource',\n {\n factory: () =>\n expoModule('expo-screen-capture', () => require('expo-screen-capture') as Expo),\n },\n );\n\n private readonly native = inject(ScreenCapture.SOURCE);\n private readonly count = signal(0);\n\n /**\n * The permission screenshot detection needs on Android 13 and earlier, where it reads the\n * photo library. Later Android needs none, and iOS always answers granted.\n */\n readonly permission: Permission = Permission.of(\n () => this.native?.getPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** How many screenshots the user has taken while the app was in front. */\n readonly screenshots: Signal<number> = this.count.asReadonly();\n\n constructor() {\n const listener = present(this.native)?.addScreenshotListener(() =>\n this.count.update((n) => n + 1),\n );\n if (listener) inject(DestroyRef).onDestroy(() => listener.remove());\n }\n\n /** Whether the platform can prevent capture. */\n async available(): Promise<boolean> {\n return (await this.native?.isAvailableAsync()) ?? false;\n }\n\n /**\n * Keeps the app out of screenshots and screen recordings until `allow()` is called with the same\n * key, or the app restarts. Omitting the key uses one shared default key.\n */\n async prevent(key?: string): Promise<void> {\n await this.native?.preventScreenCaptureAsync(key);\n }\n\n /** Releases the key; capture is allowed again once no key is held. */\n async allow(key?: string): Promise<void> {\n await this.native?.allowScreenCaptureAsync(key);\n }\n\n /**\n * iOS: blurs the app while it is not in focus - in the app switcher, in the background, and\n * during interruptions - by `blurIntensity` from 0 to 1, half by default. Android already hides\n * it in the app switcher while capture is prevented.\n */\n async protectAppSwitcher(blurIntensity?: number): Promise<void> {\n await this.native?.enableAppSwitcherProtectionAsync(blurIntensity);\n }\n\n async unprotectAppSwitcher(): Promise<void> {\n await this.native?.disableAppSwitcherProtectionAsync();\n }\n}\n"]}
1
+ {"version":3,"file":"screen-capture.js","sourceRoot":"","sources":["../src/screen-capture.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `ScreenCapture`, bound to `expo-screen-capture`: keeping a screen out of screenshots and\n * recordings, and knowing when one was taken.\n *\n * ```ts\n * private readonly capture = inject(ScreenCapture);\n *\n * constructor() {\n * void this.capture.prevent('card-details');\n * inject(DestroyRef).onDestroy(() => void this.capture.allow('card-details'));\n * }\n * ```\n *\n * `prevent()` and `allow()` take a key, and capture stays prevented while any key is still held:\n * two screens that each prevent it do not undo each other. Keys are the runtime's, as the window is:\n * an app that goes releases the keys it holds, and leaves those another app holds. `screenshots` counts the screenshots\n * the user has taken, and the listener behind it lives as long as the app does and is removed\n * when the app is destroyed. Without the module installed, nothing is prevented and the count\n * stays at zero.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport { onWindowFront, sharedBySource } from '@ng-native/device';\nimport { expoModule, present } from './native.ts';\nimport { Permission, UNAVAILABLE } from './permissions.ts';\n\ntype Expo = typeof import('expo-screen-capture');\n\n/** The module's functions this service calls. The real module is one; a test provides a fake. */\nexport type NativeScreenCapture = Pick<\n Expo,\n | 'isAvailableAsync'\n | 'preventScreenCaptureAsync'\n | 'allowScreenCaptureAsync'\n | 'enableAppSwitcherProtectionAsync'\n | 'disableAppSwitcherProtectionAsync'\n | 'addScreenshotListener'\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n> & {\n /** Calls `listener` when a window comes to the front, to be kept out of capture again. */\n onWindowFront?(listener: () => void): () => void;\n /**\n * Prevents capture on the window now in front, past the module's own record of its keys, which\n * would send nothing for a key it already has.\n */\n preventAgain?(): Promise<void>;\n};\n\n/** The key `expo-screen-capture` uses when none is given. */\nconst DEFAULT_KEY = 'default';\n\n@Service()\nexport class ScreenCapture {\n /** Overridden in a test to take screenshots nobody took. */\n static readonly SOURCE = new InjectionToken<NativeScreenCapture | null>(\n 'angular-native.screenCaptureSource',\n {\n factory: () => {\n const expo = expoModule(\n 'expo-screen-capture',\n () => require('expo-screen-capture') as Expo,\n );\n const loaded = present(expo);\n if (!loaded) return expo;\n // One per module, so every app the runtime mounts holds the same keys.\n return sharedBySource(loaded, () => ({\n ...loaded,\n onWindowFront,\n preventAgain: async () => {\n const modules = (globalThis as { expo?: { modules?: Record<string, unknown> } }).expo\n ?.modules;\n const native = modules?.['ExpoScreenCapture'] as\n { preventScreenCapture?(): Promise<void> } | undefined;\n await native?.preventScreenCapture?.();\n },\n }));\n },\n },\n );\n\n private readonly native = inject(ScreenCapture.SOURCE);\n private readonly count = signal(0);\n /** Every app's keys: the window is the runtime's, not an app's. */\n private readonly window = sharedBySource(this.native, () => new CaptureWindow(this.native));\n /** This app, among the apps holding keys. */\n private readonly owner = {};\n\n /**\n * The permission screenshot detection needs on Android 13 and earlier, where it reads the\n * photo library. Later Android needs none, and iOS always answers granted.\n */\n readonly permission: Permission = Permission.of(\n () => this.native?.getPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync?.() ?? Promise.resolve(UNAVAILABLE),\n );\n\n /** How many screenshots the user has taken while the app was in front. */\n readonly screenshots: Signal<number> = this.count.asReadonly();\n\n constructor() {\n const listener = present(this.native)?.addScreenshotListener(() =>\n this.count.update((n) => n + 1),\n );\n inject(DestroyRef).onDestroy(() => {\n listener?.remove();\n this.window.leave(this.owner);\n });\n }\n\n /** Whether the platform can prevent capture. */\n async available(): Promise<boolean> {\n return (await this.native?.isAvailableAsync()) ?? false;\n }\n\n /**\n * Keeps the app out of screenshots and screen recordings until `allow()` is called with the same\n * key, or the app goes. Omitting the key uses one shared default key.\n */\n async prevent(key?: string): Promise<void> {\n await this.window.prevent(this.owner, key);\n }\n\n /** Releases the key; capture is allowed again once no key is held. */\n async allow(key?: string): Promise<void> {\n await this.window.allow(this.owner, key);\n }\n\n /**\n * iOS: blurs the app while it is not in focus - in the app switcher, in the background, and\n * during interruptions - by `blurIntensity` from 0 to 1, half by default. Android already hides\n * it in the app switcher while capture is prevented.\n */\n async protectAppSwitcher(blurIntensity?: number): Promise<void> {\n await this.native?.enableAppSwitcherProtectionAsync(blurIntensity);\n }\n\n async unprotectAppSwitcher(): Promise<void> {\n await this.native?.disableAppSwitcherProtectionAsync();\n }\n}\n\n/** Screen capture, as every app on one module holds it. */\nclass CaptureWindow {\n /** The apps holding each key. */\n private readonly holders = new Map<string, Set<object>>();\n private readonly native: NativeScreenCapture | null;\n\n constructor(native: NativeScreenCapture | null) {\n this.native = native;\n // Lives as long as the module, which is the runtime's.\n present(native)?.onWindowFront?.(() => {\n if (this.holders.size > 0) void this.native?.preventAgain?.().catch(() => {});\n });\n }\n\n async prevent(owner: object, key: string | undefined): Promise<void> {\n const name = key ?? DEFAULT_KEY;\n const holders = this.holders.get(name) ?? new Set();\n this.holders.set(name, holders.add(owner));\n await this.native?.preventScreenCaptureAsync(key);\n }\n\n async allow(owner: object, key: string | undefined): Promise<void> {\n const name = key ?? DEFAULT_KEY;\n const holders = this.holders.get(name);\n holders?.delete(owner);\n // Another app holding the key keeps it: the module would release it for both.\n if (holders && holders.size > 0) return;\n this.holders.delete(name);\n await this.native?.allowScreenCaptureAsync(key);\n }\n\n /** Releases `owner`'s keys, for an app that goes: those another app holds stay held. */\n leave(owner: object): void {\n for (const name of [...this.holders.keys()]) void this.allow(owner, name).catch(() => {});\n }\n}\n"]}
@@ -29,6 +29,7 @@
29
29
  * platform rather than injecting it, because the first call happens before there is an injector.
30
30
  */
31
31
  import { InjectionToken } from '@angular/core';
32
+ import { nextFrame as defaultNextFrame } from '@ng-native/fabric';
32
33
  import { expoModuleNow } from "./native.js";
33
34
  export function expoSplashScreen() {
34
35
  const expo = expoModuleNow('expo-splash-screen', () => require('expo-splash-screen'));
@@ -86,7 +87,7 @@ export class Splash {
86
87
  * is the empty one. Waiting a frame is what makes the handover invisible, and is why this is a
87
88
  * method rather than two calls at a call site.
88
89
  */
89
- async hideWhenReady(work, nextFrame = defaultFrame) {
90
+ async hideWhenReady(work, nextFrame = defaultNextFrame) {
90
91
  try {
91
92
  await work;
92
93
  }
@@ -96,15 +97,6 @@ export class Splash {
96
97
  }
97
98
  }
98
99
  }
99
- /** One turn of the frame loop, or the macrotask queue where there is no loop to wait on. */
100
- const defaultFrame = () => new Promise((resolve) => {
101
- const raf = globalThis
102
- .requestAnimationFrame;
103
- if (raf)
104
- raf(() => resolve());
105
- else
106
- setTimeout(resolve, 0);
107
- });
108
100
  /**
109
101
  * The one instance, exported directly because it is needed before an injector exists: holding the
110
102
  * splash is the first thing an entry file does.
@@ -1 +1 @@
1
- {"version":3,"file":"splash-screen.js","sourceRoot":"","sources":["../src/splash-screen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQ5C,MAAM,UAAU,gBAAgB;IAC9B,MAAM,IAAI,GAAG,aAAa,CACxB,oBAAoB,EACpB,GAAG,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAwC,CAC3E,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO;QACL,oBAAoB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,EAAE;QACvD,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,MAAM;IACA,MAAM,CAAgE;IAC/E,QAAQ,GAA0D,IAAI,CAAC;IACvE,IAAI,GAAG,KAAK,CAAC;IAErB;;;OAGG;IACH,YAAY,MAAqE;QAC/E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,IAAY,MAAM;QAChB,IAAI,CAAC,QAAQ,KAAK;YAChB,MAAM,EAAE,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM;SACxE,CAAC;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,8FAA8F;IAC9F,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,KAAK,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QAClB,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,aAAa,CAAC,IAAsB,EAAE,SAAS,GAAG,YAAY;QAClE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC;QACb,CAAC;gBAAS,CAAC;YACT,MAAM,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;IACH,CAAC;CACF;AAED,4FAA4F;AAC5F,MAAM,YAAY,GAAG,GAAkB,EAAE,CACvC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACtB,MAAM,GAAG,GAAI,UAAmE;SAC7E,qBAAqB,CAAC;IACzB,IAAI,GAAG;QAAE,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;;QACzB,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEL;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,cAAc,CAAS,6BAA6B,EAAE;IACpF,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY;CAC5B,CAAC,CAAC","sourcesContent":["/**\n * `SplashScreen`, bound to `expo-splash-screen`. Held until the app has something worth showing.\n *\n * The whole point is the ordering, and it is easy to get subtly wrong: the native screen hides\n * itself as soon as the first frame is drawn, so an app that loads fonts before mounting shows a\n * blank window for as long as that takes. Preventing the auto-hide has to happen at module scope,\n * before any of that, and hiding has to happen after the first real frame:\n *\n * ```ts\n * import { splashScreen } from '@ng-native/expo/splash-screen';\n * import { loadFonts } from '@ng-native/expo/fonts';\n *\n * // At module scope, before anything renders.\n * splashScreen.hold();\n *\n * AppRegistry.registerRunnable('main', ({ rootTag }) => {\n * const fonts = loadFonts(styleSheetOf(GlobalStyles));\n * mount(Number(rootTag), App, getFabricUIManager(), { globalStyles: styleSheetOf(GlobalStyles) });\n * void splashScreen.hideWhenReady(fonts);\n * });\n * ```\n *\n * Mounting does not wait for the fonts; the splash screen does. Text laid out before they arrive\n * is laid out again as each face registers, behind the splash, so nothing is ever seen in the\n * fallback face.\n *\n * `hideWhenReady` is that shape as one call, which is the only thing worth wrapping here: every\n * function underneath is already a plain promise with no React in it. The class takes its\n * platform rather than injecting it, because the first call happens before there is an injector.\n */\nimport { InjectionToken } from '@angular/core';\nimport { expoModuleNow } from './native.ts';\n\n/** The slice of `expo-splash-screen` this needs. */\nexport interface NativeSplashScreen {\n preventAutoHideAsync(): Promise<boolean>;\n hideAsync(): Promise<void>;\n}\n\nexport function expoSplashScreen(): NativeSplashScreen | null {\n const expo = expoModuleNow(\n 'expo-splash-screen',\n () => require('expo-splash-screen') as typeof import('expo-splash-screen'),\n );\n if (!expo) return null;\n return {\n preventAutoHideAsync: () => expo.preventAutoHideAsync(),\n hideAsync: () => expo.hideAsync(),\n };\n}\n\nexport class Splash {\n private readonly source: NativeSplashScreen | null | (() => NativeSplashScreen | null);\n private resolved: { readonly native: NativeSplashScreen | null } | null = null;\n private held = false;\n\n /**\n * The module, or a function that reaches it: the shared instance is created when its file is\n * imported, and reaching the module then would fail an import rather than the first call.\n */\n constructor(native: NativeSplashScreen | null | (() => NativeSplashScreen | null)) {\n this.source = native;\n }\n\n private get native(): NativeSplashScreen | null {\n this.resolved ??= {\n native: typeof this.source === 'function' ? this.source() : this.source,\n };\n return this.resolved.native;\n }\n\n /** Whether the module is installed. Without it the screen hides itself on the first frame. */\n get available(): boolean {\n return this.native !== null;\n }\n\n /**\n * Keep the splash up. Called at module scope, before anything is mounted.\n *\n * Failures are swallowed: the only ones are races with the screen already having gone, and an\n * app that cannot hold its splash should still start.\n */\n hold(): void {\n if (!this.native || this.held) return;\n this.held = true;\n void this.native.preventAutoHideAsync().catch(() => {});\n }\n\n /** Let it go. Safe to call whether or not it was ever held. */\n async hide(): Promise<void> {\n if (!this.native) return;\n this.held = false;\n await this.native.hideAsync().catch(() => {});\n }\n\n /**\n * Hide once `work` is done *and* the first frame after it has been drawn.\n *\n * Hiding the moment the work resolves uncovers the frame that was on screen while it ran, which\n * is the empty one. Waiting a frame is what makes the handover invisible, and is why this is a\n * method rather than two calls at a call site.\n */\n async hideWhenReady(work: Promise<unknown>, nextFrame = defaultFrame): Promise<void> {\n try {\n await work;\n } finally {\n await nextFrame();\n await this.hide();\n }\n }\n}\n\n/** One turn of the frame loop, or the macrotask queue where there is no loop to wait on. */\nconst defaultFrame = (): Promise<void> =>\n new Promise((resolve) => {\n const raf = (globalThis as { requestAnimationFrame?: (cb: () => void) => void })\n .requestAnimationFrame;\n if (raf) raf(() => resolve());\n else setTimeout(resolve, 0);\n });\n\n/**\n * The one instance, exported directly because it is needed before an injector exists: holding the\n * splash is the first thing an entry file does.\n */\nexport const splashScreen = new Splash(expoSplashScreen);\n\nexport const SplashScreen = new InjectionToken<Splash>('angular-native.splashScreen', {\n factory: () => splashScreen,\n});\nexport type SplashScreen = Splash;\n"]}
1
+ {"version":3,"file":"splash-screen.js","sourceRoot":"","sources":["../src/splash-screen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQ5C,MAAM,UAAU,gBAAgB;IAC9B,MAAM,IAAI,GAAG,aAAa,CACxB,oBAAoB,EACpB,GAAG,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAwC,CAC3E,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO;QACL,oBAAoB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB,EAAE;QACvD,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,MAAM;IACA,MAAM,CAAgE;IAC/E,QAAQ,GAA0D,IAAI,CAAC;IACvE,IAAI,GAAG,KAAK,CAAC;IAErB;;;OAGG;IACH,YAAY,MAAqE;QAC/E,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,IAAY,MAAM;QAChB,IAAI,CAAC,QAAQ,KAAK;YAChB,MAAM,EAAE,OAAO,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM;SACxE,CAAC;QACF,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,8FAA8F;IAC9F,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC;IAC9B,CAAC;IAED;;;;;OAKG;IACH,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO;QACtC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,KAAK,IAAI,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QAClB,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,aAAa,CAAC,IAAsB,EAAE,SAAS,GAAG,gBAAgB;QACtE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC;QACb,CAAC;gBAAS,CAAC;YACT,MAAM,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACpB,CAAC;IACH,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEzD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,cAAc,CAAS,6BAA6B,EAAE;IACpF,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY;CAC5B,CAAC,CAAC","sourcesContent":["/**\n * `SplashScreen`, bound to `expo-splash-screen`. Held until the app has something worth showing.\n *\n * The whole point is the ordering, and it is easy to get subtly wrong: the native screen hides\n * itself as soon as the first frame is drawn, so an app that loads fonts before mounting shows a\n * blank window for as long as that takes. Preventing the auto-hide has to happen at module scope,\n * before any of that, and hiding has to happen after the first real frame:\n *\n * ```ts\n * import { splashScreen } from '@ng-native/expo/splash-screen';\n * import { loadFonts } from '@ng-native/expo/fonts';\n *\n * // At module scope, before anything renders.\n * splashScreen.hold();\n *\n * AppRegistry.registerRunnable('main', ({ rootTag }) => {\n * const fonts = loadFonts(styleSheetOf(GlobalStyles));\n * mount(Number(rootTag), App, getFabricUIManager(), { globalStyles: styleSheetOf(GlobalStyles) });\n * void splashScreen.hideWhenReady(fonts);\n * });\n * ```\n *\n * Mounting does not wait for the fonts; the splash screen does. Text laid out before they arrive\n * is laid out again as each face registers, behind the splash, so nothing is ever seen in the\n * fallback face.\n *\n * `hideWhenReady` is that shape as one call, which is the only thing worth wrapping here: every\n * function underneath is already a plain promise with no React in it. The class takes its\n * platform rather than injecting it, because the first call happens before there is an injector.\n */\nimport { InjectionToken } from '@angular/core';\nimport { nextFrame as defaultNextFrame } from '@ng-native/fabric';\nimport { expoModuleNow } from './native.ts';\n\n/** The slice of `expo-splash-screen` this needs. */\nexport interface NativeSplashScreen {\n preventAutoHideAsync(): Promise<boolean>;\n hideAsync(): Promise<void>;\n}\n\nexport function expoSplashScreen(): NativeSplashScreen | null {\n const expo = expoModuleNow(\n 'expo-splash-screen',\n () => require('expo-splash-screen') as typeof import('expo-splash-screen'),\n );\n if (!expo) return null;\n return {\n preventAutoHideAsync: () => expo.preventAutoHideAsync(),\n hideAsync: () => expo.hideAsync(),\n };\n}\n\nexport class Splash {\n private readonly source: NativeSplashScreen | null | (() => NativeSplashScreen | null);\n private resolved: { readonly native: NativeSplashScreen | null } | null = null;\n private held = false;\n\n /**\n * The module, or a function that reaches it: the shared instance is created when its file is\n * imported, and reaching the module then would fail an import rather than the first call.\n */\n constructor(native: NativeSplashScreen | null | (() => NativeSplashScreen | null)) {\n this.source = native;\n }\n\n private get native(): NativeSplashScreen | null {\n this.resolved ??= {\n native: typeof this.source === 'function' ? this.source() : this.source,\n };\n return this.resolved.native;\n }\n\n /** Whether the module is installed. Without it the screen hides itself on the first frame. */\n get available(): boolean {\n return this.native !== null;\n }\n\n /**\n * Keep the splash up. Called at module scope, before anything is mounted.\n *\n * Failures are swallowed: the only ones are races with the screen already having gone, and an\n * app that cannot hold its splash should still start.\n */\n hold(): void {\n if (!this.native || this.held) return;\n this.held = true;\n void this.native.preventAutoHideAsync().catch(() => {});\n }\n\n /** Let it go. Safe to call whether or not it was ever held. */\n async hide(): Promise<void> {\n if (!this.native) return;\n this.held = false;\n await this.native.hideAsync().catch(() => {});\n }\n\n /**\n * Hide once `work` is done *and* the first frame after it has been drawn.\n *\n * Hiding the moment the work resolves uncovers the frame that was on screen while it ran, which\n * is the empty one. Waiting a frame is what makes the handover invisible, and is why this is a\n * method rather than two calls at a call site.\n */\n async hideWhenReady(work: Promise<unknown>, nextFrame = defaultNextFrame): Promise<void> {\n try {\n await work;\n } finally {\n await nextFrame();\n await this.hide();\n }\n }\n}\n\n/**\n * The one instance, exported directly because it is needed before an injector exists: holding the\n * splash is the first thing an entry file does.\n */\nexport const splashScreen = new Splash(expoSplashScreen);\n\nexport const SplashScreen = new InjectionToken<Splash>('angular-native.splashScreen', {\n factory: () => splashScreen,\n});\nexport type SplashScreen = Splash;\n"]}
@@ -7,7 +7,8 @@
7
7
  "android": {
8
8
  "modules": [
9
9
  "dev.ngnative.expo.OngoingNotificationModule",
10
- "dev.ngnative.expo.SearchFieldModule"
10
+ "dev.ngnative.expo.SearchFieldModule",
11
+ "dev.ngnative.expo.KeyboardModule"
11
12
  ]
12
13
  }
13
14
  }
@@ -16,8 +16,16 @@ Pod::Spec.new do |s|
16
16
  s.static_framework = true
17
17
 
18
18
  s.dependency 'ExpoModulesCore'
19
- # `@expo/ui` is an optional peer. Where the app has it, the modifiers it is given are built.
20
- has_expo_ui = system('node', '-e', "require.resolve('@expo/ui/package.json', { paths: [process.argv[1]] })", __dir__, out: File::NULL, err: File::NULL)
19
+ # `@expo/ui` is an optional peer. Where the app depends on it, which is when Expo links its pod,
20
+ # the modifiers it is given are built. Read from the app's package.json, the nearest above its
21
+ # Podfile: resolving the package finds a copy the app does not depend on, in a workspace's root
22
+ # or pnpm's store, and its pod is then not there to depend on. Not an optional dependency, which
23
+ # an install can leave out.
24
+ app = Pod::Config.instance.installation_root.ascend.find { |dir| (dir + 'package.json').file? }
25
+ app_package = app ? JSON.parse((app + 'package.json').read) : {}
26
+ has_expo_ui = %w[dependencies devDependencies].any? do |kind|
27
+ app_package.dig(kind, '@expo/ui')
28
+ end
21
29
  s.dependency 'ExpoUI' if has_expo_ui
22
30
 
23
31
  s.source_files = '**/*.swift'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-native/expo",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Expo modules as Angular services for Angular Native, plus registerExpoView for a module's native view.",
5
5
  "keywords": [
6
6
  "angular",
@@ -119,6 +119,10 @@
119
119
  "types": "./dist/keep-awake.d.ts",
120
120
  "default": "./dist/keep-awake.js"
121
121
  },
122
+ "./keyboard": {
123
+ "types": "./dist/keyboard.d.ts",
124
+ "default": "./dist/keyboard.js"
125
+ },
122
126
  "./language-model": {
123
127
  "types": "./dist/language-model.d.ts",
124
128
  "default": "./dist/language-model.js"
@@ -217,7 +221,8 @@
217
221
  "access": "public"
218
222
  },
219
223
  "dependencies": {
220
- "@ng-native/fabric": "0.15.0"
224
+ "@ng-native/device": "0.17.0",
225
+ "@ng-native/fabric": "0.17.0"
221
226
  },
222
227
  "nx": {
223
228
  "tags": [
@@ -255,7 +260,7 @@
255
260
  "expo-image-picker": "^57.0.0",
256
261
  "expo-keep-awake": "^57.0.0",
257
262
  "expo-local-authentication": "^57.0.0",
258
- "expo-local-llm": "^0.6.0",
263
+ "expo-local-llm": "^0.8.1",
259
264
  "expo-localization": "^57.0.0",
260
265
  "expo-location": "^57.0.0",
261
266
  "expo-maps": "^57.0.0",
@@ -300,7 +305,7 @@
300
305
  "expo-image-picker": "^57.0.20",
301
306
  "expo-keep-awake": "^57.0.2",
302
307
  "expo-local-authentication": "^57.0.3",
303
- "expo-local-llm": "^0.6.0",
308
+ "expo-local-llm": "^0.8.1",
304
309
  "expo-localization": "^57.0.2",
305
310
  "expo-location": "^57.0.20",
306
311
  "expo-maps": "^57.0.3",