@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.
- package/LICENSE +21 -0
- package/README.md +137 -2
- package/build/angular/index.d.ts +4 -0
- package/build/angular/index.js +4 -0
- package/build/angular/services/permissions.service/index.d.ts +5 -0
- package/build/angular/services/permissions.service/index.js +59 -0
- package/build/angular/services/prevent-screen-capture.service/index.d.ts +4 -0
- package/build/angular/services/prevent-screen-capture.service/index.js +65 -0
- package/build/angular/services/screenshot-listener.service/index.d.ts +4 -0
- package/build/angular/services/screenshot-listener.service/index.js +65 -0
- package/build/core/index.d.ts +2 -0
- package/build/core/index.js +2 -0
- package/build/core/native-module.d.ts +13 -0
- package/build/core/native-module.js +4 -0
- package/build/core/permissions-runtime.d.ts +5 -0
- package/build/core/permissions-runtime.js +16 -0
- package/build/core/screen-capture.d.ts +16 -0
- package/build/core/screen-capture.js +68 -0
- package/build/react/hooks/use-permissions/index.d.ts +7 -0
- package/build/react/hooks/use-permissions/index.js +37 -0
- package/build/react/hooks/use-prevent-screen-capture/index.d.ts +1 -0
- package/build/react/hooks/use-prevent-screen-capture/index.js +12 -0
- package/build/react/hooks/use-screenshot-listener/index.d.ts +1 -0
- package/build/react/hooks/use-screenshot-listener/index.js +12 -0
- package/build/react/index.d.ts +4 -0
- package/build/react/index.js +4 -0
- package/build/solid/index.d.ts +4 -0
- package/build/solid/index.js +4 -0
- package/build/solid/primitives/create-permissions.d.ts +8 -0
- package/build/solid/primitives/create-permissions.js +25 -0
- package/build/solid/primitives/create-prevent-screen-capture.d.ts +1 -0
- package/build/solid/primitives/create-prevent-screen-capture.js +11 -0
- package/build/solid/primitives/create-screenshot-listener.d.ts +1 -0
- package/build/solid/primitives/create-screenshot-listener.js +10 -0
- package/build/svelte/index.d.ts +4 -0
- package/build/svelte/index.js +4 -0
- package/build/svelte/use-permissions.svelte.d.ts +7 -0
- package/build/svelte/use-permissions.svelte.js +27 -0
- package/build/svelte/use-prevent-screen-capture.svelte.d.ts +1 -0
- package/build/svelte/use-prevent-screen-capture.svelte.js +10 -0
- package/build/svelte/use-screenshot-listener.svelte.d.ts +1 -0
- package/build/svelte/use-screenshot-listener.svelte.js +11 -0
- package/build/vue/composables/use-permissions/index.d.ts +8 -0
- package/build/vue/composables/use-permissions/index.js +19 -0
- package/build/vue/composables/use-prevent-screen-capture/index.d.ts +1 -0
- package/build/vue/composables/use-prevent-screen-capture/index.js +12 -0
- package/build/vue/composables/use-screenshot-listener/index.d.ts +1 -0
- package/build/vue/composables/use-screenshot-listener/index.js +13 -0
- package/build/vue/index.d.ts +4 -0
- package/build/vue/index.js +4 -0
- package/build-ngc/angular/index.d.ts +4 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/permissions.service/index.d.ts +8 -0
- package/build-ngc/angular/services/permissions.service/index.js +16 -0
- package/build-ngc/angular/services/permissions.service/index.js.map +1 -0
- package/build-ngc/angular/services/prevent-screen-capture.service/index.d.ts +7 -0
- package/build-ngc/angular/services/prevent-screen-capture.service/index.js +23 -0
- package/build-ngc/angular/services/prevent-screen-capture.service/index.js.map +1 -0
- package/build-ngc/angular/services/screenshot-listener.service/index.d.ts +7 -0
- package/build-ngc/angular/services/screenshot-listener.service/index.js +23 -0
- package/build-ngc/angular/services/screenshot-listener.service/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +2 -0
- package/build-ngc/core/index.js +3 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +13 -0
- package/build-ngc/core/native-module.js +5 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/permissions-runtime.d.ts +5 -0
- package/build-ngc/core/permissions-runtime.js +17 -0
- package/build-ngc/core/permissions-runtime.js.map +1 -0
- package/build-ngc/core/screen-capture.d.ts +16 -0
- package/build-ngc/core/screen-capture.js +69 -0
- package/build-ngc/core/screen-capture.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +148 -3
- package/src/angular/index.ts +4 -0
- package/src/angular/services/permissions.service/index.ts +11 -0
- package/src/angular/services/prevent-screen-capture.service/index.ts +25 -0
- package/src/angular/services/screenshot-listener.service/index.ts +22 -0
- package/src/core/index.ts +17 -0
- package/src/core/native-module.ts +27 -0
- package/src/core/permissions-runtime.ts +26 -0
- package/src/core/screen-capture.ts +102 -0
- package/src/react/hooks/use-permissions/index.ts +49 -0
- package/src/react/hooks/use-prevent-screen-capture/index.ts +17 -0
- package/src/react/hooks/use-screenshot-listener/index.ts +14 -0
- package/src/react/index.ts +4 -0
- package/src/solid/index.ts +4 -0
- package/src/solid/primitives/create-permissions.ts +39 -0
- package/src/solid/primitives/create-prevent-screen-capture.ts +14 -0
- package/src/solid/primitives/create-screenshot-listener.ts +13 -0
- package/src/svelte/index.ts +4 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +7 -0
- package/src/svelte/use-permissions.svelte.ts +41 -0
- package/src/svelte/use-prevent-screen-capture.svelte.ts +12 -0
- package/src/svelte/use-screenshot-listener.svelte.ts +13 -0
- package/src/vue/composables/use-permissions/index.ts +33 -0
- package/src/vue/composables/use-prevent-screen-capture/index.ts +18 -0
- package/src/vue/composables/use-screenshot-listener/index.ts +18 -0
- 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
|
-
#
|
|
1
|
+
# @symbiote-native/screen-capture
|
|
2
2
|
|
|
3
|
-
|
|
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/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,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,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,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,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,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/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/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
|
+
};
|