@symbiote-native/screen-capture 0.0.0-stage → 0.1.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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -2
  3. package/build/angular/index.d.ts +4 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/permissions.service/index.d.ts +5 -0
  6. package/build/angular/services/permissions.service/index.js +59 -0
  7. package/build/angular/services/prevent-screen-capture.service/index.d.ts +4 -0
  8. package/build/angular/services/prevent-screen-capture.service/index.js +65 -0
  9. package/build/angular/services/screenshot-listener.service/index.d.ts +4 -0
  10. package/build/angular/services/screenshot-listener.service/index.js +65 -0
  11. package/build/core/index.d.ts +2 -0
  12. package/build/core/index.js +2 -0
  13. package/build/core/native-module.d.ts +13 -0
  14. package/build/core/native-module.js +4 -0
  15. package/build/core/permissions-runtime.d.ts +5 -0
  16. package/build/core/permissions-runtime.js +16 -0
  17. package/build/core/screen-capture.d.ts +16 -0
  18. package/build/core/screen-capture.js +68 -0
  19. package/build/react/hooks/use-permissions/index.d.ts +7 -0
  20. package/build/react/hooks/use-permissions/index.js +37 -0
  21. package/build/react/hooks/use-prevent-screen-capture/index.d.ts +1 -0
  22. package/build/react/hooks/use-prevent-screen-capture/index.js +12 -0
  23. package/build/react/hooks/use-screenshot-listener/index.d.ts +1 -0
  24. package/build/react/hooks/use-screenshot-listener/index.js +12 -0
  25. package/build/react/index.d.ts +4 -0
  26. package/build/react/index.js +4 -0
  27. package/build/solid/index.d.ts +4 -0
  28. package/build/solid/index.js +4 -0
  29. package/build/solid/primitives/create-permissions.d.ts +8 -0
  30. package/build/solid/primitives/create-permissions.js +25 -0
  31. package/build/solid/primitives/create-prevent-screen-capture.d.ts +1 -0
  32. package/build/solid/primitives/create-prevent-screen-capture.js +11 -0
  33. package/build/solid/primitives/create-screenshot-listener.d.ts +1 -0
  34. package/build/solid/primitives/create-screenshot-listener.js +10 -0
  35. package/build/svelte/index.d.ts +4 -0
  36. package/build/svelte/index.js +4 -0
  37. package/build/svelte/use-permissions.svelte.d.ts +7 -0
  38. package/build/svelte/use-permissions.svelte.js +27 -0
  39. package/build/svelte/use-prevent-screen-capture.svelte.d.ts +1 -0
  40. package/build/svelte/use-prevent-screen-capture.svelte.js +10 -0
  41. package/build/svelte/use-screenshot-listener.svelte.d.ts +1 -0
  42. package/build/svelte/use-screenshot-listener.svelte.js +11 -0
  43. package/build/vue/composables/use-permissions/index.d.ts +8 -0
  44. package/build/vue/composables/use-permissions/index.js +19 -0
  45. package/build/vue/composables/use-prevent-screen-capture/index.d.ts +1 -0
  46. package/build/vue/composables/use-prevent-screen-capture/index.js +12 -0
  47. package/build/vue/composables/use-screenshot-listener/index.d.ts +1 -0
  48. package/build/vue/composables/use-screenshot-listener/index.js +13 -0
  49. package/build/vue/index.d.ts +4 -0
  50. package/build/vue/index.js +4 -0
  51. package/build-ngc/angular/index.d.ts +4 -0
  52. package/build-ngc/angular/index.js +5 -0
  53. package/build-ngc/angular/index.js.map +1 -0
  54. package/build-ngc/angular/services/permissions.service/index.d.ts +8 -0
  55. package/build-ngc/angular/services/permissions.service/index.js +16 -0
  56. package/build-ngc/angular/services/permissions.service/index.js.map +1 -0
  57. package/build-ngc/angular/services/prevent-screen-capture.service/index.d.ts +7 -0
  58. package/build-ngc/angular/services/prevent-screen-capture.service/index.js +23 -0
  59. package/build-ngc/angular/services/prevent-screen-capture.service/index.js.map +1 -0
  60. package/build-ngc/angular/services/screenshot-listener.service/index.d.ts +7 -0
  61. package/build-ngc/angular/services/screenshot-listener.service/index.js +23 -0
  62. package/build-ngc/angular/services/screenshot-listener.service/index.js.map +1 -0
  63. package/build-ngc/core/index.d.ts +2 -0
  64. package/build-ngc/core/index.js +3 -0
  65. package/build-ngc/core/index.js.map +1 -0
  66. package/build-ngc/core/native-module.d.ts +13 -0
  67. package/build-ngc/core/native-module.js +5 -0
  68. package/build-ngc/core/native-module.js.map +1 -0
  69. package/build-ngc/core/permissions-runtime.d.ts +5 -0
  70. package/build-ngc/core/permissions-runtime.js +17 -0
  71. package/build-ngc/core/permissions-runtime.js.map +1 -0
  72. package/build-ngc/core/screen-capture.d.ts +16 -0
  73. package/build-ngc/core/screen-capture.js +69 -0
  74. package/build-ngc/core/screen-capture.js.map +1 -0
  75. package/native-link.json +12 -0
  76. package/package.json +148 -3
  77. package/src/angular/index.ts +4 -0
  78. package/src/angular/services/permissions.service/index.ts +11 -0
  79. package/src/angular/services/prevent-screen-capture.service/index.ts +25 -0
  80. package/src/angular/services/screenshot-listener.service/index.ts +22 -0
  81. package/src/core/index.ts +17 -0
  82. package/src/core/native-module.ts +27 -0
  83. package/src/core/permissions-runtime.ts +26 -0
  84. package/src/core/screen-capture.ts +102 -0
  85. package/src/react/hooks/use-permissions/index.ts +49 -0
  86. package/src/react/hooks/use-prevent-screen-capture/index.ts +17 -0
  87. package/src/react/hooks/use-screenshot-listener/index.ts +14 -0
  88. package/src/react/index.ts +4 -0
  89. package/src/solid/index.ts +4 -0
  90. package/src/solid/primitives/create-permissions.ts +39 -0
  91. package/src/solid/primitives/create-prevent-screen-capture.ts +14 -0
  92. package/src/solid/primitives/create-screenshot-listener.ts +13 -0
  93. package/src/svelte/index.ts +4 -0
  94. package/src/svelte/svelte-ambient.d.ts +3 -0
  95. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  96. package/src/svelte/use-permissions.svelte.ts +41 -0
  97. package/src/svelte/use-prevent-screen-capture.svelte.ts +12 -0
  98. package/src/svelte/use-screenshot-listener.svelte.ts +13 -0
  99. package/src/vue/composables/use-permissions/index.ts +33 -0
  100. package/src/vue/composables/use-prevent-screen-capture/index.ts +18 -0
  101. package/src/vue/composables/use-screenshot-listener/index.ts +18 -0
  102. package/src/vue/index.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,138 @@
1
- # Temporary Holding Version
1
+ # @symbiote-native/screen-capture
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Keep a screen that shows sensitive or paid content out of screenshots and recordings, and find out
4
+ when a screenshot is taken. One API for every [SymbioteNative](../../README.md) adapter (React,
5
+ Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-screen-capture`](https://github.com/expo/expo/tree/main/packages/expo-screen-capture)
8
+ (including an iOS app-switcher privacy blur) the same way [`@symbiote-native/print`](../print)
9
+ wraps its upstream: `expo-modules-core` is a direct dependency and the upstream JS is hand-ported
10
+ into `core/`. See the `symbiote-expo-native-module` project skill for the full mechanism.
11
+
12
+ ## Install
13
+
14
+ **New app:**
15
+
16
+ ```bash
17
+ npx @symbiote-native/cli new my-app --screen-capture
18
+ ```
19
+
20
+ **Existing SymbioteNative app:**
21
+
22
+ ```bash
23
+ npx @symbiote-native/cli add --screen-capture
24
+ ```
25
+
26
+ Either way: installs `@symbiote-native/screen-capture` and wires the native autolinking
27
+ automatically, see [`@symbiote-native/cli`](../cli).
28
+
29
+ <details>
30
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
31
+
32
+ ```bash
33
+ npm install @symbiote-native/screen-capture
34
+ ```
35
+
36
+ `expo-screen-capture` and `expo-modules-core` come along as regular, pinned dependencies, never
37
+ install either yourself, and never add the `expo` meta-package to this project.
38
+
39
+ ### Required one-time step: native autolinking wiring
40
+
41
+ Same one-time app wiring every `expo-modules-core` package shares, see
42
+ [`@symbiote-native/print`'s README](../print/README.md#required-one-time-step-native-autolinking-wiring)
43
+ for the full table; nothing package-specific here.
44
+
45
+ ### Permissions and no config plugin
46
+
47
+ - **No config plugin.** Upstream ships none.
48
+ - **Android screenshot detection** needs a storage-read permission on API < 33 (`READ_MEDIA_IMAGES`
49
+ on API 33, `READ_EXTERNAL_STORAGE` below it, nothing on API 34+). Both entries ship in
50
+ `expo-screen-capture`'s own bundled `AndroidManifest.xml` and auto-merge via Gradle's manifest
51
+ merger, no `native-link.json` entry needed.
52
+ - **iOS never needs a permission**: `getPermissionsAsync`/`requestPermissionsAsync` always
53
+ resolve granted there, matching upstream.
54
+
55
+ </details>
56
+
57
+ ## Shape
58
+
59
+ ```
60
+ src/core/ the async functions + the shared permissions-runtime get/request wrapper.
61
+ native-module.ts resolves ExpoScreenCapture through expo-modules-core's
62
+ requireNativeModule.
63
+ src/react/ usePreventScreenCapture + useScreenshotListener + usePermissions (hooks)
64
+ src/vue/ same three, as composables
65
+ src/solid/ same three, as primitives
66
+ src/svelte/ same three, as runes
67
+ src/angular/ PreventScreenCaptureService + ScreenshotListenerService +
68
+ PermissionsService, `connect()` shape matching
69
+ `@symbiote-native/keep-awake`/`brightness`'s own services
70
+ ```
71
+
72
+ The plain async functions stay framework-agnostic, re-exported by every adapter's own barrel.
73
+ `usePreventScreenCapture`/`useScreenshotListener`/`usePermissions` are ported to **every**
74
+ adapter, not just React, per this project's `components_split_logic_view_lifecycle` convention.
75
+
76
+ ## Use it
77
+
78
+ ```ts
79
+ import {
80
+ addScreenshotListener,
81
+ preventScreenCaptureAsync,
82
+ allowScreenCaptureAsync,
83
+ } from '@symbiote-native/screen-capture';
84
+
85
+ await preventScreenCaptureAsync();
86
+ const subscription = addScreenshotListener(() => console.log('screenshot taken'));
87
+ // later
88
+ subscription.remove();
89
+ await allowScreenCaptureAsync();
90
+ ```
91
+
92
+ ## API
93
+
94
+ | Export | Signature | Notes |
95
+ | ------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
96
+ | `isAvailableAsync` | `() => Promise<boolean>` | Whether the native module implements the prevent/allow pair. |
97
+ | `preventScreenCaptureAsync` | `(key?: string) => Promise<void>` | Blocks screenshots/recordings until a matching `allowScreenCaptureAsync`. |
98
+ | `allowScreenCaptureAsync` | `(key?: string) => Promise<void>` | Re-allows once every active key is released. |
99
+ | `enableAppSwitcherProtectionAsync` | `(blurIntensity?: number) => Promise<void>` | iOS only; throws `UnavailabilityError` on Android. |
100
+ | `disableAppSwitcherProtectionAsync` | `() => Promise<void>` | iOS only; throws `UnavailabilityError` on Android. |
101
+ | `addScreenshotListener` | `(listener: () => void) => EventSubscription` | Fires when the user takes a screenshot while foregrounded. |
102
+ | `removeScreenshotListener` | `(subscription: EventSubscription) => void` | Deprecated upstream; prefer `subscription.remove()`. |
103
+ | `getPermissionsAsync` | `() => Promise<PermissionResponse>` | Android-only concept; always granted on iOS. |
104
+ | `requestPermissionsAsync` | `() => Promise<PermissionResponse>` | Android-only concept; always granted on iOS. |
105
+
106
+ ## Notes
107
+
108
+ - **Prevent and allow are counted by key.** Use a distinct `key` per caller so one screen leaving
109
+ does not unblock another.
110
+ - **Screenshot callback on Android 13 and lower needs `READ_MEDIA_IMAGES`.** Android 14+ needs no
111
+ permission for blocking or the callback. Google Play restricts that permission to apps that need
112
+ broad photo access.
113
+ - **Test on an emulator:** `adb shell input keyevent 120` triggers a screenshot on Android; the iOS
114
+ Simulator has Device > Trigger Screenshot.
115
+
116
+ ## Common questions
117
+
118
+ - **What does a protected screen look like?** Blocked or black on Android; hidden behind a blank
119
+ layer on iOS. Test on a device.
120
+ - **The prevent hook does nothing.** Check it runs on mount on a real device or emulator, and use a
121
+ distinct `key` per caller.
122
+ - **The screenshot callback never fires.** Foreground only; Android 13 and lower need
123
+ `READ_MEDIA_IMAGES`.
124
+ - **App switcher preview.** Call `enableAppSwitcherProtectionAsync()` on iOS.
125
+
126
+ Sources: [Expo docs: ScreenCapture](https://docs.expo.dev/versions/latest/sdk/screen-capture/),
127
+ [expo/expo#37874](https://github.com/expo/expo/pull/37874),
128
+ [expo/expo#21416](https://github.com/expo/expo/issues/21416).
129
+
130
+ ## Test it
131
+
132
+ ```bash
133
+ pnpm vitest run packages/screen-capture
134
+ ```
135
+
136
+ Upstream ships no test suite of its own for this SDK version, so nothing to port, every test
137
+ here is net-new coverage (key-counted prevent/allow, the app-switcher pair's iOS-only failure,
138
+ the screenshot listener, and the permission fallback on iOS).
@@ -0,0 +1,4 @@
1
+ export * from '../core';
2
+ export { PreventScreenCaptureService } from './services/prevent-screen-capture.service';
3
+ export { ScreenshotListenerService } from './services/screenshot-listener.service';
4
+ export { PermissionsService } from './services/permissions.service';
@@ -0,0 +1,4 @@
1
+ export * from '../core/index.js';
2
+ export { PreventScreenCaptureService } from './services/prevent-screen-capture.service/index.js';
3
+ export { ScreenshotListenerService } from './services/screenshot-listener.service/index.js';
4
+ export { PermissionsService } from './services/permissions.service/index.js';
@@ -0,0 +1,5 @@
1
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
2
+ import type { PermissionResponse } from '../../../core';
3
+ export declare class PermissionsService extends PermissionsServiceBase<PermissionResponse> {
4
+ constructor();
5
+ }
@@ -0,0 +1,59 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Injectable } from '@angular/core';
36
+ import { PermissionsServiceBase } from '@symbiote-native/angular';
37
+ import { getPermissionsAsync, requestPermissionsAsync } from '../../../core/index.js';
38
+ let PermissionsService = (() => {
39
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
40
+ let _classDescriptor;
41
+ let _classExtraInitializers = [];
42
+ let _classThis;
43
+ let _classSuper = PermissionsServiceBase;
44
+ var PermissionsService = class extends _classSuper {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ PermissionsService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ constructor() {
54
+ super(getPermissionsAsync, requestPermissionsAsync);
55
+ }
56
+ };
57
+ return PermissionsService = _classThis;
58
+ })();
59
+ export { PermissionsService };
@@ -0,0 +1,4 @@
1
+ export declare class PreventScreenCaptureService {
2
+ private readonly injector;
3
+ connect(key?: string): void;
4
+ }
@@ -0,0 +1,65 @@
1
+ // Angular twin of `../../react`'s `usePreventScreenCapture`, `connect()` shape matching
2
+ // `@symbiote-native/keep-awake`'s own `KeepAwakeService`
3
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
4
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
5
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
6
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
7
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
8
+ var _, done = false;
9
+ for (var i = decorators.length - 1; i >= 0; i--) {
10
+ var context = {};
11
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
12
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
13
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
14
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
15
+ if (kind === "accessor") {
16
+ if (result === void 0) continue;
17
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
18
+ if (_ = accept(result.get)) descriptor.get = _;
19
+ if (_ = accept(result.set)) descriptor.set = _;
20
+ if (_ = accept(result.init)) initializers.unshift(_);
21
+ }
22
+ else if (_ = accept(result)) {
23
+ if (kind === "field") initializers.unshift(_);
24
+ else descriptor[key] = _;
25
+ }
26
+ }
27
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
28
+ done = true;
29
+ };
30
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
31
+ var useValue = arguments.length > 2;
32
+ for (var i = 0; i < initializers.length; i++) {
33
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
34
+ }
35
+ return useValue ? value : void 0;
36
+ };
37
+ import { effect, inject, Injectable, Injector } from '@angular/core';
38
+ import { allowScreenCaptureAsync, preventScreenCaptureAsync, } from '../../../core/index.js';
39
+ let PreventScreenCaptureService = (() => {
40
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ var PreventScreenCaptureService = class {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ PreventScreenCaptureService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ injector = inject(Injector);
54
+ connect(key = 'default') {
55
+ effect(onCleanup => {
56
+ preventScreenCaptureAsync(key);
57
+ onCleanup(() => {
58
+ allowScreenCaptureAsync(key);
59
+ });
60
+ }, { injector: this.injector });
61
+ }
62
+ };
63
+ return PreventScreenCaptureService = _classThis;
64
+ })();
65
+ export { PreventScreenCaptureService };
@@ -0,0 +1,4 @@
1
+ export declare class ScreenshotListenerService {
2
+ private readonly injector;
3
+ connect(listener: () => void): void;
4
+ }
@@ -0,0 +1,65 @@
1
+ // Angular twin of `../../react`'s `useScreenshotListener`, `connect()` shape matching
2
+ // `@symbiote-native/keep-awake`'s own `KeepAwakeService`
3
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
4
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
5
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
6
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
7
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
8
+ var _, done = false;
9
+ for (var i = decorators.length - 1; i >= 0; i--) {
10
+ var context = {};
11
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
12
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
13
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
14
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
15
+ if (kind === "accessor") {
16
+ if (result === void 0) continue;
17
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
18
+ if (_ = accept(result.get)) descriptor.get = _;
19
+ if (_ = accept(result.set)) descriptor.set = _;
20
+ if (_ = accept(result.init)) initializers.unshift(_);
21
+ }
22
+ else if (_ = accept(result)) {
23
+ if (kind === "field") initializers.unshift(_);
24
+ else descriptor[key] = _;
25
+ }
26
+ }
27
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
28
+ done = true;
29
+ };
30
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
31
+ var useValue = arguments.length > 2;
32
+ for (var i = 0; i < initializers.length; i++) {
33
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
34
+ }
35
+ return useValue ? value : void 0;
36
+ };
37
+ import { effect, inject, Injectable, Injector } from '@angular/core';
38
+ import { addScreenshotListener } from '../../../core/index.js';
39
+ let ScreenshotListenerService = (() => {
40
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ var ScreenshotListenerService = class {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ ScreenshotListenerService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ injector = inject(Injector);
54
+ connect(listener) {
55
+ effect(onCleanup => {
56
+ const subscription = addScreenshotListener(listener);
57
+ onCleanup(() => {
58
+ subscription.remove();
59
+ });
60
+ }, { injector: this.injector });
61
+ }
62
+ };
63
+ return ScreenshotListenerService = _classThis;
64
+ })();
65
+ export { ScreenshotListenerService };
@@ -0,0 +1,2 @@
1
+ export { addScreenshotListener, allowScreenCaptureAsync, disableAppSwitcherProtectionAsync, enableAppSwitcherProtectionAsync, getPermissionsAsync, isAvailableAsync, preventScreenCaptureAsync, removeScreenshotListener, requestPermissionsAsync, } from './screen-capture';
2
+ export { PermissionStatus, type EventSubscription, type PermissionExpiration, type PermissionResponse, } from 'expo-modules-core';
@@ -0,0 +1,2 @@
1
+ export { addScreenshotListener, allowScreenCaptureAsync, disableAppSwitcherProtectionAsync, enableAppSwitcherProtectionAsync, getPermissionsAsync, isAvailableAsync, preventScreenCaptureAsync, removeScreenshotListener, requestPermissionsAsync, } from './screen-capture.js';
2
+ export { PermissionStatus, } from 'expo-modules-core';
@@ -0,0 +1,13 @@
1
+ import type { EventSubscription, PermissionResponse } from 'expo-modules-core';
2
+ export declare const ON_SCREENSHOT_EVENT_NAME = "onScreenshot";
3
+ export type INativeScreenCaptureModule = {
4
+ addListener(eventName: typeof ON_SCREENSHOT_EVENT_NAME, listener: () => void): EventSubscription;
5
+ removeAllListeners(eventName: typeof ON_SCREENSHOT_EVENT_NAME): void;
6
+ preventScreenCapture?(): Promise<void>;
7
+ allowScreenCapture?(): Promise<void>;
8
+ enableAppSwitcherProtection?(blurIntensity: number): Promise<void>;
9
+ disableAppSwitcherProtection?(): Promise<void>;
10
+ getPermissionsAsync?(): Promise<PermissionResponse>;
11
+ requestPermissionsAsync?(): Promise<PermissionResponse>;
12
+ };
13
+ export declare const expoScreenCapture: INativeScreenCaptureModule;
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_SCREEN_CAPTURE_MODULE_NAME = 'ExpoScreenCapture';
3
+ export const ON_SCREENSHOT_EVENT_NAME = 'onScreenshot';
4
+ export const expoScreenCapture = requireNativeModule(EXPO_SCREEN_CAPTURE_MODULE_NAME);
@@ -0,0 +1,5 @@
1
+ export type IPermissionsApi<TPermission> = {
2
+ get: () => Promise<TPermission>;
3
+ request: () => Promise<TPermission>;
4
+ };
5
+ export declare function createPermissionsApi<TPermission>(getMethod: () => Promise<TPermission>, requestMethod: () => Promise<TPermission>, record: (response: TPermission) => void): IPermissionsApi<TPermission>;
@@ -0,0 +1,16 @@
1
+ // The get/request wrapper every adapter's `usePermissions` needs, extracted once so
2
+ // `src/vue`/`src/solid`/`src/svelte`/`src/react` each supply only their own state primitive
3
+ export function createPermissionsApi(getMethod, requestMethod, record) {
4
+ return {
5
+ get: async () => {
6
+ const response = await getMethod();
7
+ record(response);
8
+ return response;
9
+ },
10
+ request: async () => {
11
+ const response = await requestMethod();
12
+ record(response);
13
+ return response;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,16 @@
1
+ import type { EventSubscription, PermissionResponse } from 'expo-modules-core';
2
+ export declare function isAvailableAsync(): Promise<boolean>;
3
+ /** Repeated calls with the same `key` are no-ops until a matching `allowScreenCaptureAsync`. */
4
+ export declare function preventScreenCaptureAsync(key?: string): Promise<void>;
5
+ export declare function allowScreenCaptureAsync(key?: string): Promise<void>;
6
+ /** iOS only; throws `UnavailabilityError` on Android. */
7
+ export declare function enableAppSwitcherProtectionAsync(blurIntensity?: number): Promise<void>;
8
+ /** iOS only; throws `UnavailabilityError` on Android. */
9
+ export declare function disableAppSwitcherProtectionAsync(): Promise<void>;
10
+ export declare function addScreenshotListener(listener: () => void): EventSubscription;
11
+ /** @deprecated use `subscription.remove()` instead. */
12
+ export declare function removeScreenshotListener(subscription: EventSubscription): void;
13
+ /** Only Android requires a permission to detect screenshots; iOS always resolves granted. */
14
+ export declare function getPermissionsAsync(): Promise<PermissionResponse>;
15
+ /** Only Android requires a permission to detect screenshots; iOS always resolves granted. */
16
+ export declare function requestPermissionsAsync(): Promise<PermissionResponse>;
@@ -0,0 +1,68 @@
1
+ import { PermissionStatus, UnavailabilityError } from 'expo-modules-core';
2
+ import { expoScreenCapture, ON_SCREENSHOT_EVENT_NAME } from './native-module.js';
3
+ const NATIVE_MODULE_NAME = 'ScreenCapture';
4
+ const activeKeys = new Set();
5
+ export async function isAvailableAsync() {
6
+ return (!!expoScreenCapture.preventScreenCapture &&
7
+ !!expoScreenCapture.allowScreenCapture);
8
+ }
9
+ /** Repeated calls with the same `key` are no-ops until a matching `allowScreenCaptureAsync`. */
10
+ export async function preventScreenCaptureAsync(key = 'default') {
11
+ if (!expoScreenCapture.preventScreenCapture) {
12
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'preventScreenCaptureAsync');
13
+ }
14
+ if (!activeKeys.has(key)) {
15
+ activeKeys.add(key);
16
+ await expoScreenCapture.preventScreenCapture();
17
+ }
18
+ }
19
+ export async function allowScreenCaptureAsync(key = 'default') {
20
+ if (!expoScreenCapture.allowScreenCapture) {
21
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'allowScreenCaptureAsync');
22
+ }
23
+ activeKeys.delete(key);
24
+ if (activeKeys.size === 0) {
25
+ await expoScreenCapture.allowScreenCapture();
26
+ }
27
+ }
28
+ /** iOS only; throws `UnavailabilityError` on Android. */
29
+ export async function enableAppSwitcherProtectionAsync(blurIntensity = 0.5) {
30
+ if (!expoScreenCapture.enableAppSwitcherProtection) {
31
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'enableAppSwitcherProtectionAsync');
32
+ }
33
+ await expoScreenCapture.enableAppSwitcherProtection(blurIntensity);
34
+ }
35
+ /** iOS only; throws `UnavailabilityError` on Android. */
36
+ export async function disableAppSwitcherProtectionAsync() {
37
+ if (!expoScreenCapture.disableAppSwitcherProtection) {
38
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'disableAppSwitcherProtectionAsync');
39
+ }
40
+ await expoScreenCapture.disableAppSwitcherProtection();
41
+ }
42
+ export function addScreenshotListener(listener) {
43
+ return expoScreenCapture.addListener(ON_SCREENSHOT_EVENT_NAME, listener);
44
+ }
45
+ /** @deprecated use `subscription.remove()` instead. */
46
+ export function removeScreenshotListener(subscription) {
47
+ subscription.remove();
48
+ }
49
+ const defaultPermissionsResponse = {
50
+ granted: true,
51
+ expires: 'never',
52
+ canAskAgain: true,
53
+ status: PermissionStatus.GRANTED,
54
+ };
55
+ /** Only Android requires a permission to detect screenshots; iOS always resolves granted. */
56
+ export async function getPermissionsAsync() {
57
+ if (expoScreenCapture.getPermissionsAsync) {
58
+ return expoScreenCapture.getPermissionsAsync();
59
+ }
60
+ return defaultPermissionsResponse;
61
+ }
62
+ /** Only Android requires a permission to detect screenshots; iOS always resolves granted. */
63
+ export async function requestPermissionsAsync() {
64
+ if (expoScreenCapture.requestPermissionsAsync) {
65
+ return expoScreenCapture.requestPermissionsAsync();
66
+ }
67
+ return defaultPermissionsResponse;
68
+ }
@@ -0,0 +1,7 @@
1
+ import type { PermissionResponse } from '../../../core';
2
+ export declare function usePermissions(): [
3
+ PermissionResponse | null,
4
+ () => Promise<PermissionResponse>,
5
+ () => Promise<PermissionResponse>,
6
+ Error | null
7
+ ];
@@ -0,0 +1,37 @@
1
+ // Same shape as `@symbiote-native/brightness`'s own `usePermissions` by design - each package
2
+ // owns its copy. The mount fetch has nobody to reject to, so its failure lands in the 4th
3
+ // tuple slot (`error`)
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { getPermissionsAsync, requestPermissionsAsync } from '../../../core/index.js';
6
+ export function usePermissions() {
7
+ const isMounted = useRef(true);
8
+ const [status, setStatus] = useState(null);
9
+ const [error, setError] = useState(null);
10
+ const getPermission = useCallback(async () => {
11
+ const response = await getPermissionsAsync();
12
+ if (isMounted.current) {
13
+ setStatus(response);
14
+ setError(null);
15
+ }
16
+ return response;
17
+ }, []);
18
+ const requestPermission = useCallback(async () => {
19
+ const response = await requestPermissionsAsync();
20
+ if (isMounted.current) {
21
+ setStatus(response);
22
+ setError(null);
23
+ }
24
+ return response;
25
+ }, []);
26
+ useEffect(() => {
27
+ isMounted.current = true;
28
+ getPermission().catch((cause) => {
29
+ if (isMounted.current)
30
+ setError(cause instanceof Error ? cause : new Error(String(cause)));
31
+ });
32
+ return () => {
33
+ isMounted.current = false;
34
+ };
35
+ }, [getPermission]);
36
+ return [status, requestPermission, getPermission, error];
37
+ }
@@ -0,0 +1 @@
1
+ export declare function usePreventScreenCapture(key?: string): void;
@@ -0,0 +1,12 @@
1
+ // React lifecycle wiring over the framework-agnostic core (core/screen-capture.ts): prevents on
2
+ // mount, allows on unmount, mirroring upstream's `usePreventScreenCapture`
3
+ import { useEffect } from 'react';
4
+ import { allowScreenCaptureAsync, preventScreenCaptureAsync, } from '../../../core/index.js';
5
+ export function usePreventScreenCapture(key = 'default') {
6
+ useEffect(() => {
7
+ preventScreenCaptureAsync(key);
8
+ return () => {
9
+ allowScreenCaptureAsync(key);
10
+ };
11
+ }, [key]);
12
+ }
@@ -0,0 +1 @@
1
+ export declare function useScreenshotListener(listener: () => void): void;
@@ -0,0 +1,12 @@
1
+ // React lifecycle wiring over the framework-agnostic core (core/screen-capture.ts): subscribes
2
+ // on mount, unsubscribes on unmount, mirroring upstream's `useScreenshotListener`
3
+ import { useEffect } from 'react';
4
+ import { addScreenshotListener } from '../../../core/index.js';
5
+ export function useScreenshotListener(listener) {
6
+ useEffect(() => {
7
+ const subscription = addScreenshotListener(listener);
8
+ return () => {
9
+ subscription.remove();
10
+ };
11
+ }, [listener]);
12
+ }
@@ -0,0 +1,4 @@
1
+ export * from '../core';
2
+ export { usePreventScreenCapture } from './hooks/use-prevent-screen-capture';
3
+ export { useScreenshotListener } from './hooks/use-screenshot-listener';
4
+ export { usePermissions } from './hooks/use-permissions';
@@ -0,0 +1,4 @@
1
+ export * from '../core/index.js';
2
+ export { usePreventScreenCapture } from './hooks/use-prevent-screen-capture/index.js';
3
+ export { useScreenshotListener } from './hooks/use-screenshot-listener/index.js';
4
+ export { usePermissions } from './hooks/use-permissions/index.js';
@@ -0,0 +1,4 @@
1
+ export * from '../core';
2
+ export { createPreventScreenCapture } from './primitives/create-prevent-screen-capture';
3
+ export { createScreenshotListener } from './primitives/create-screenshot-listener';
4
+ export { createPermissions } from './primitives/create-permissions';
@@ -0,0 +1,4 @@
1
+ export * from '../core/index.js';
2
+ export { createPreventScreenCapture } from './primitives/create-prevent-screen-capture.js';
3
+ export { createScreenshotListener } from './primitives/create-screenshot-listener.js';
4
+ export { createPermissions } from './primitives/create-permissions.js';
@@ -0,0 +1,8 @@
1
+ import { type Accessor } from 'solid-js';
2
+ import type { PermissionResponse } from '../../core';
3
+ export declare function createPermissions(): {
4
+ status: Accessor<PermissionResponse | null>;
5
+ error: Accessor<Error | null>;
6
+ request: () => Promise<PermissionResponse>;
7
+ get: () => Promise<PermissionResponse>;
8
+ };