@symbiote-native/clipboard 0.0.1 → 0.1.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.
Files changed (51) hide show
  1. package/README.md +180 -26
  2. package/build/angular/index.d.ts +2 -0
  3. package/build/angular/index.js +7 -0
  4. package/build/angular/services/clipboard.service/index.d.ts +6 -0
  5. package/build/angular/services/clipboard.service/index.js +77 -0
  6. package/build/core/clipboard.d.ts +86 -0
  7. package/build/core/clipboard.js +137 -0
  8. package/build/core/index.d.ts +3 -0
  9. package/build/core/index.js +2 -0
  10. package/build/core/native-module.d.ts +17 -0
  11. package/build/core/native-module.js +7 -0
  12. package/build/core/types.d.ts +67 -0
  13. package/build/core/types.js +28 -0
  14. package/build/react/hooks/use-clipboard/index.d.ts +2 -0
  15. package/build/react/hooks/use-clipboard/index.js +14 -0
  16. package/build/react/index.d.ts +2 -0
  17. package/build/react/index.js +8 -0
  18. package/build/vue/composables/use-clipboard/index.d.ts +3 -0
  19. package/build/vue/composables/use-clipboard/index.js +20 -0
  20. package/build/vue/index.d.ts +2 -0
  21. package/build/vue/index.js +8 -0
  22. package/build-ngc/angular/index.d.ts +2 -0
  23. package/build-ngc/angular/index.js +8 -0
  24. package/build-ngc/angular/index.js.map +1 -0
  25. package/build-ngc/angular/services/clipboard.service/index.d.ts +9 -0
  26. package/build-ngc/angular/services/clipboard.service/index.js +36 -0
  27. package/build-ngc/angular/services/clipboard.service/index.js.map +1 -0
  28. package/build-ngc/core/clipboard.d.ts +86 -0
  29. package/build-ngc/core/clipboard.js +138 -0
  30. package/build-ngc/core/clipboard.js.map +1 -0
  31. package/build-ngc/core/index.d.ts +3 -0
  32. package/build-ngc/core/index.js +3 -0
  33. package/build-ngc/core/index.js.map +1 -0
  34. package/build-ngc/core/native-module.d.ts +17 -0
  35. package/build-ngc/core/native-module.js +8 -0
  36. package/build-ngc/core/native-module.js.map +1 -0
  37. package/build-ngc/core/types.d.ts +67 -0
  38. package/build-ngc/core/types.js +29 -0
  39. package/build-ngc/core/types.js.map +1 -0
  40. package/native-link.json +12 -0
  41. package/package.json +92 -1
  42. package/src/angular/index.ts +8 -0
  43. package/src/angular/services/clipboard.service/index.ts +35 -0
  44. package/src/core/clipboard.ts +163 -0
  45. package/src/core/index.ts +23 -0
  46. package/src/core/native-module.ts +42 -0
  47. package/src/core/types.ts +81 -0
  48. package/src/react/hooks/use-clipboard/index.ts +17 -0
  49. package/src/react/index.ts +9 -0
  50. package/src/vue/composables/use-clipboard/index.ts +25 -0
  51. package/src/vue/index.ts +9 -0
package/README.md CHANGED
@@ -1,44 +1,198 @@
1
1
  # @symbiote-native/clipboard
2
2
 
3
- **Status: skeleton only — not implemented, published early to reserve the npm name.**
3
+ A wrapper package for [SymbioteNative](../../README.md) that makes
4
+ [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/) — read/write clipboard
5
+ text, URLs, and images, plus a clipboard-change listener — usable from **every** adapter, React,
6
+ Vue, and Angular, not just React. Unlike `@symbiote-native/sensors`, which is all
7
+ `DeviceSensor`-shaped classes plus one free-function module (`Pedometer`), clipboard is closer to
8
+ `@symbiote-native/local-auth`'s shape — mostly stateless async functions — plus **one**
9
+ listener-based subscription (`addClipboardListener`) that each adapter wraps in its own
10
+ mount/unmount lifecycle (`useClipboard`).
4
11
 
5
- This package will be a port of [`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/)
6
- for [SymbioteNative](../../README.md) — read/write clipboard text, URLs, and images, plus a
7
- clipboard-change listener, reachable from every adapter (React, Vue, Angular), not just React.
8
- Nothing beyond this `package.json` + `README.md` exists yet.
12
+ ## Install
9
13
 
10
- ## Planned API
14
+ ```bash
15
+ npm install @symbiote-native/clipboard
16
+ ```
17
+
18
+ Depends on `expo-clipboard` and `expo-modules-core` directly (regular dependencies, pinned to an
19
+ exact version — never a caret range, since this package's `core/` is hand-ported against one
20
+ specific native API shape and a newer resolve could silently drift the two apart). Never install
21
+ `expo-clipboard` yourself, and never add the `expo` meta-package to this project — it bundles its
22
+ own Metro/Babel pipeline that conflicts with this project's own.
23
+
24
+ ## Required one-time step: native autolinking wiring
25
+
26
+ Unlike a plain RN native module, `expo-clipboard`'s native code is discovered by
27
+ `expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism — this needs wiring
28
+ into the native host app **once**, covering this package and every other `expo-modules-core`
29
+ package (`@symbiote-native/sensors`, `@symbiote-native/local-auth`) with zero further changes:
30
+
31
+ | Platform | Touches |
32
+ |---|---|
33
+ | iOS | `ios/Podfile` — add `use_expo_modules!` |
34
+ | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
35
+ | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
36
+ | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
37
+
38
+ Full mechanics live in the `symbiote-expo-native-module` skill. Clipboard itself needs no
39
+ `Info.plist`/`AndroidManifest.xml` permission entry on either platform — reading/writing the
40
+ clipboard requires no platform permission string.
41
+
42
+ ## Shape
43
+
44
+ ```
45
+ src/core/ getStringAsync/setStringAsync/hasStringAsync, getUrlAsync/setUrlAsync/
46
+ hasUrlAsync (iOS only), getImageAsync/setImageAsync/hasImageAsync, and
47
+ addClipboardListener/removeClipboardListener. native-module.ts resolves
48
+ the ExpoClipboard native module via expo-modules-core's
49
+ requireNativeModule.
50
+ src/react/hooks/ @symbiote-native/clipboard/react — useClipboard
51
+ src/vue/composables/ @symbiote-native/clipboard/vue — useClipboard (same name)
52
+ src/angular/services/ @symbiote-native/clipboard/angular — ClipboardService
53
+ ```
54
+
55
+ Every function except the listener is plain stateless async and re-exported as-is. The listener
56
+ subscription (`addClipboardListener`) lives once in `core`, framework-agnostic; each adapter's
57
+ `useClipboard` hook/composable/`ClipboardService.connect()` is a thin mount/unmount (or DI-scoped
58
+ `effect()`, for Angular) wrapper around that same subscription — the plumbing is written once and
59
+ shared by all three.
60
+
61
+ ## Use it
62
+
63
+ ```ts
64
+ import { getStringAsync, setStringAsync, addClipboardListener } from '@symbiote-native/clipboard';
65
+ ```
66
+
67
+ `useClipboard()`'s event only carries the clipboard's changed content *types*
68
+ (`IClipboardEvent.contentTypes`), never the string itself — every adapter's demo screen treats a
69
+ change as a cue to re-fetch via `getStringAsync()`, not a value to render directly.
70
+
71
+ ```tsx
72
+ // React
73
+ import { useEffect, useState } from 'react';
74
+ import { getStringAsync, setStringAsync } from '@symbiote-native/clipboard';
75
+ import { useClipboard } from '@symbiote-native/clipboard/react';
76
+
77
+ function ClipboardScreen() {
78
+ const clipboardEvent = useClipboard(); // IClipboardEvent | null
79
+ const [text, setText] = useState<string | null>(null);
80
+
81
+ useEffect(() => {
82
+ getStringAsync().then(setText);
83
+ }, [clipboardEvent]);
84
+
85
+ const handleCopy = (input: string) => setStringAsync(input).then(() => getStringAsync().then(setText));
86
+
87
+ return <Text>{text ?? 'checking…'}</Text>;
88
+ }
89
+ ```
90
+
91
+ ```vue
92
+ <!-- Vue -->
93
+ <script setup lang="ts">
94
+ import { onMounted, ref, watch } from 'vue';
95
+ import { getStringAsync, setStringAsync } from '@symbiote-native/clipboard';
96
+ import { useClipboard } from '@symbiote-native/clipboard/vue';
97
+
98
+ const text = ref('checking…');
99
+ function refresh(): void {
100
+ void getStringAsync().then(value => { text.value = value; });
101
+ }
102
+ onMounted(refresh);
11
103
 
12
- Forward-looking reference only — signatures below are copied from upstream's
13
- `Clipboard.ts`/`Clipboard.types.ts`, not yet ported here.
104
+ const clipboardEvent = useClipboard(); // Ref<IClipboardEvent | null>
105
+ watch(clipboardEvent, event => { if (event) refresh(); });
106
+
107
+ function handleCopy(input: string): void {
108
+ void setStringAsync(input).then(refresh);
109
+ }
110
+ </script>
111
+ <template>
112
+ <Text>{{ text }}</Text>
113
+ </template>
114
+ ```
14
115
 
15
116
  ```ts
16
- getStringAsync(options?: GetStringOptions): Promise<string>
17
- setStringAsync(text: string, options?: SetStringOptions): Promise<boolean>
117
+ // Angular
118
+ import { Component, Injector, effect, inject, signal } from '@angular/core';
119
+ import { ClipboardService, getStringAsync, setStringAsync } from '@symbiote-native/clipboard/angular';
120
+
121
+ @Component({ /* ... */ })
122
+ export class ClipboardScreen {
123
+ private readonly injector = inject(Injector);
124
+ private readonly clipboardEvent = inject(ClipboardService).connect(); // Signal<IClipboardEvent | null>
125
+ readonly text = signal('checking…');
126
+
127
+ constructor() {
128
+ this.refresh();
129
+ effect(() => { if (this.clipboardEvent() !== null) this.refresh(); }, { injector: this.injector });
130
+ }
131
+
132
+ handleCopy(input: string): void {
133
+ setStringAsync(input).then(() => this.refresh());
134
+ }
135
+
136
+ private refresh(): void {
137
+ getStringAsync().then(value => this.text.set(value));
138
+ }
139
+ }
140
+ ```
141
+
142
+ These are trimmed from the real demo screens — `examples/expo-react/screens/ClipboardScreen.tsx`,
143
+ `examples/expo-vue-sfc/screens/ClipboardScreen.vue`, `examples/expo-vue-tsx/screens/ClipboardScreen.tsx`,
144
+ `examples/expo-angular/src/screens/ClipboardScreen.ts` — which additionally show `hasStringAsync()`
145
+ status badges and, iOS-only, the `getUrlAsync`/`setUrlAsync`/`hasUrlAsync` URL surface.
146
+
147
+ ## API
148
+
149
+ ```ts
150
+ getStringAsync(options?: IGetStringOptions): Promise<string>
151
+ setStringAsync(text: string, options?: ISetStringOptions): Promise<boolean>
18
152
  hasStringAsync(): Promise<boolean>
19
153
  getUrlAsync(): Promise<string | null> // iOS only
20
154
  setUrlAsync(url: string): Promise<void> // iOS only
21
155
  hasUrlAsync(): Promise<boolean> // iOS only
22
- getImageAsync(options: GetImageOptions): Promise<ClipboardImage | null>
156
+ getImageAsync(options: IGetImageOptions): Promise<IClipboardImage | null>
23
157
  setImageAsync(base64Image: string): Promise<void>
24
158
  hasImageAsync(): Promise<boolean>
25
- addClipboardListener(listener: (event: ClipboardEvent) => void): EventSubscription
159
+ addClipboardListener(listener: (event: IClipboardEvent) => void): EventSubscription
26
160
  removeClipboardListener(subscription: EventSubscription) // deprecated, use subscription.remove()
27
161
  ```
28
162
 
29
- Plus `ContentType`, `StringFormat`, `GetStringOptions`, `SetStringOptions`, `GetImageOptions`,
30
- `ClipboardImage`, `ClipboardEvent` — ported from upstream's `Clipboard.types.ts`, renamed with
31
- this repo's `I`-prefix convention for exported types where applicable
32
- (`ts-js-best-practices`). Upstream also ships a `ClipboardPasteButton` component (native
33
- paste-button UI, iOS 16+) — whether that's in scope for this package's first real
34
- implementation is an open question for that later work, not decided here.
163
+ Plus `ContentType`, `StringFormat`, `IGetStringOptions`, `ISetStringOptions`, `IGetImageOptions`,
164
+ `IClipboardImage`, `IClipboardEvent` — ported from upstream's `Clipboard.types.ts`, renamed with
165
+ this repo's `I`-prefix convention for exported types (`ts-js-best-practices`); `ContentType` and
166
+ `StringFormat` stay unprefixed enums, matching `AuthenticationType`/`SecurityLevel` in
167
+ `@symbiote-native/local-auth`.
168
+
169
+ ```ts
170
+ // React
171
+ import { useClipboard } from '@symbiote-native/clipboard/react';
172
+ const clipboardEvent = useClipboard(); // IClipboardEvent | null
173
+
174
+ // Vue
175
+ import { useClipboard } from '@symbiote-native/clipboard/vue';
176
+ const clipboardEvent = useClipboard(); // Ref<IClipboardEvent | null>
177
+
178
+ // Angular
179
+ import { ClipboardService } from '@symbiote-native/clipboard/angular';
180
+ readonly clipboardEvent = inject(ClipboardService).connect(); // Signal<IClipboardEvent | null>
181
+ ```
182
+
183
+ `ClipboardPasteButton` (upstream's native paste-button view component, iOS 16+) is **not**
184
+ ported — out of scope for this pass. If it's ever wrapped, it follows
185
+ `symbiote-third-party-native-view`, not this package's `expo-modules-core` recipe.
186
+
187
+ ## Test it
35
188
 
36
- ## When this moves past skeleton
189
+ Tests exercise the JS layer only, against a fake native module in place of the real
190
+ `requireNativeModule` resolution (`src/core/**/*.test.ts`,
191
+ `src/{react,vue,angular}/**/*.test.{ts,tsx}`) — no Fabric/Descriptor angle at all, since clipboard
192
+ is a pure async-function + one-listener surface, never a view. Native rendering itself is verified
193
+ on-device (see the parent [README](../../README.md) for the project's testing model).
37
194
 
38
- See [`@symbiote-native/local-auth`](../local-auth) as the reference shape for what this package
39
- will look like once implemented — same `expo-modules-core`-based wrapping pattern (no native
40
- view, a handful of async functions + an event listener), same core/react/vue/angular layout.
41
- The real wrapping mechanics (why `expo-modules-core` is depended on directly and never the
42
- `expo` meta-package, why upstream JS is hand-ported rather than imported, how autolinking picks
43
- up the native module) are documented in the
44
- [`symbiote-expo-native-module`](../../.claude/skills/symbiote-expo-native-module/SKILL.md) skill.
195
+ Native autolinking wiring (Android's 3-layer registration, iOS Podfile/pod install) is done across
196
+ all four `examples/expo-*` canary apps. It isn't wired into the public non-Expo canaries
197
+ (`examples/react`, `examples/vue-sfc`, `examples/vue-tsx`, `examples/angular`) yet — those don't
198
+ depend on any `expo-modules-core` package today.
@@ -0,0 +1,2 @@
1
+ export { ClipboardService } from './services/clipboard.service';
2
+ export * from '../core';
@@ -0,0 +1,7 @@
1
+ // @symbiote-native/clipboard/angular: the Angular entry over the framework-agnostic core.
2
+ // Unlike @symbiote-native/local-auth (all stateless functions, plain re-export), clipboard has
3
+ // one listener-based piece — ClipboardService.connect() is the Angular-only lifecycle half; the
4
+ // addClipboardListener subscription plumbing all lives in core, shared with React/Vue. Mirrors
5
+ // @symbiote-native/sensors' AccelerometerService.
6
+ export { ClipboardService } from './services/clipboard.service/index.js';
7
+ export * from '../core/index.js';
@@ -0,0 +1,6 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { type IClipboardEvent } from '../../../core';
3
+ export declare class ClipboardService {
4
+ private readonly injector;
5
+ connect(): Signal<IClipboardEvent | null>;
6
+ }
@@ -0,0 +1,77 @@
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 { effect, inject, Injectable, Injector, signal } from '@angular/core';
36
+ import { addClipboardListener } from '../../../core/index.js';
37
+ // Angular twin of React's `useClipboard` hook and Vue's `useClipboard` composable. Angular has
38
+ // no per-instance hook — state and lifecycle live in DI instead, so `connect()` stands in for
39
+ // the hook's role: call it ONCE (typically from a component's field initializer, inside an
40
+ // injection context).
41
+ //
42
+ // readonly clipboardEvent = inject(ClipboardService).connect();
43
+ // // template: {{ clipboardEvent()?.contentTypes }}
44
+ //
45
+ // Mirrors AccelerometerService.connect() from @symbiote-native/sensors: the subscription doesn't
46
+ // depend on anything the caller's own signals could change between renders, so a single
47
+ // effect() that subscribes once and cleans up once is enough.
48
+ let ClipboardService = (() => {
49
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
50
+ let _classDescriptor;
51
+ let _classExtraInitializers = [];
52
+ let _classThis;
53
+ var ClipboardService = class {
54
+ static { _classThis = this; }
55
+ static {
56
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
57
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
58
+ ClipboardService = _classThis = _classDescriptor.value;
59
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
60
+ __runInitializers(_classThis, _classExtraInitializers);
61
+ }
62
+ // Captured in the constructor (itself always run inside an injection context by Angular's own
63
+ // DI) so `connect()` can create an `effect()` even when called from plain field-initializer
64
+ // code that is not, on its own, an active injection context — mirrors AccelerometerService.
65
+ injector = inject(Injector);
66
+ connect() {
67
+ const event = signal(null);
68
+ effect(onCleanup => {
69
+ const subscription = addClipboardListener(next => event.set(next));
70
+ onCleanup(() => subscription.remove());
71
+ }, { injector: this.injector });
72
+ return event.asReadonly();
73
+ }
74
+ };
75
+ return ClipboardService = _classThis;
76
+ })();
77
+ export { ClipboardService };
@@ -0,0 +1,86 @@
1
+ import type { EventSubscription } from 'expo-modules-core';
2
+ import type { IClipboardEvent, IClipboardImage, IGetImageOptions, IGetStringOptions, ISetStringOptions } from './types';
3
+ /**
4
+ * Gets the content of the user's clipboard.
5
+ *
6
+ * Note: On iOS 16+, if the user denies paste permission, this method returns an empty string.
7
+ * Due to iOS platform limitations, there is no way to distinguish between an empty clipboard
8
+ * and denied permission.
9
+ *
10
+ * @param options Options for the clipboard content to be retrieved.
11
+ * @returns A promise that resolves to the content of the clipboard, or an empty string if the
12
+ * clipboard is empty or permission was denied.
13
+ */
14
+ export declare function getStringAsync(options?: IGetStringOptions): Promise<string>;
15
+ /**
16
+ * Sets the content of the user's clipboard.
17
+ *
18
+ * @param text The string to save to the clipboard.
19
+ * @param options Options for the clipboard content to be set.
20
+ * @returns A promise that resolves to `true` once the string has been saved to the clipboard.
21
+ */
22
+ export declare function setStringAsync(text: string, options?: ISetStringOptions): Promise<boolean>;
23
+ /**
24
+ * Returns whether the clipboard has text content. Returns `true` for both plain text and rich
25
+ * text (e.g. HTML).
26
+ */
27
+ export declare function hasStringAsync(): Promise<boolean>;
28
+ /**
29
+ * Gets the URL from the user's clipboard.
30
+ *
31
+ * Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
32
+ * platform limitations, there is no way to distinguish between no URL in clipboard and denied
33
+ * permission.
34
+ * @platform ios
35
+ */
36
+ export declare function getUrlAsync(): Promise<string | null>;
37
+ /**
38
+ * Sets a URL in the user's clipboard. Behaves the same as `setStringAsync`, except that it sets
39
+ * the clipboard content type to be a URL, letting your app or other apps know that the clipboard
40
+ * contains a URL and behave accordingly.
41
+ * @platform ios
42
+ */
43
+ export declare function setUrlAsync(url: string): Promise<void>;
44
+ /**
45
+ * Returns whether the clipboard has URL content.
46
+ * @platform ios
47
+ */
48
+ export declare function hasUrlAsync(): Promise<boolean>;
49
+ /**
50
+ * Gets the image from the user's clipboard and returns it in the specified format.
51
+ *
52
+ * Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
53
+ * platform limitations, there is no way to distinguish between no image in clipboard and denied
54
+ * permission.
55
+ *
56
+ * @param options Specifies the desired format of the image.
57
+ * @returns If there was an image in the clipboard, resolves to an `IClipboardImage` object
58
+ * containing the base64 string and metadata of the image. Otherwise resolves to `null` (this
59
+ * includes cases where permission was denied).
60
+ */
61
+ export declare function getImageAsync(options: IGetImageOptions): Promise<IClipboardImage | null>;
62
+ /**
63
+ * Sets an image in the user's clipboard.
64
+ *
65
+ * @param base64Image Image encoded as a base64 string, without MIME type.
66
+ */
67
+ export declare function setImageAsync(base64Image: string): Promise<void>;
68
+ /**
69
+ * Returns whether the clipboard has image content.
70
+ */
71
+ export declare function hasImageAsync(): Promise<boolean>;
72
+ /**
73
+ * Adds a listener that fires whenever the content of the user's clipboard changes. Kept here at
74
+ * the core level, framework-agnostic, exactly like `Accelerometer.addListener` lives in
75
+ * `@symbiote-native/sensors`' core — each adapter's `useClipboard` hook/composable/service wraps
76
+ * this in its own mount/unmount lifecycle rather than reimplementing the subscription.
77
+ *
78
+ * @param listener Callback invoked with an `IClipboardEvent` describing the new clipboard
79
+ * content types whenever the clipboard changes.
80
+ */
81
+ export declare function addClipboardListener(listener: (event: IClipboardEvent) => void): EventSubscription;
82
+ /**
83
+ * Removes the listener added by `addClipboardListener`.
84
+ * @deprecated use `subscription.remove()` instead.
85
+ */
86
+ export declare function removeClipboardListener(subscription: EventSubscription): void;
@@ -0,0 +1,137 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { CLIPBOARD_CHANGED_EVENT_NAME, expoClipboard } from './native-module.js';
3
+ // Matches the module name upstream's own Clipboard.ts passes to every UnavailabilityError.
4
+ const NATIVE_MODULE_NAME = 'Clipboard';
5
+ /**
6
+ * Gets the content of the user's clipboard.
7
+ *
8
+ * Note: On iOS 16+, if the user denies paste permission, this method returns an empty string.
9
+ * Due to iOS platform limitations, there is no way to distinguish between an empty clipboard
10
+ * and denied permission.
11
+ *
12
+ * @param options Options for the clipboard content to be retrieved.
13
+ * @returns A promise that resolves to the content of the clipboard, or an empty string if the
14
+ * clipboard is empty or permission was denied.
15
+ */
16
+ export async function getStringAsync(options = {}) {
17
+ if (!expoClipboard.getStringAsync) {
18
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getStringAsync');
19
+ }
20
+ return expoClipboard.getStringAsync(options);
21
+ }
22
+ /**
23
+ * Sets the content of the user's clipboard.
24
+ *
25
+ * @param text The string to save to the clipboard.
26
+ * @param options Options for the clipboard content to be set.
27
+ * @returns A promise that resolves to `true` once the string has been saved to the clipboard.
28
+ */
29
+ export async function setStringAsync(text, options = {}) {
30
+ if (!expoClipboard.setStringAsync) {
31
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setStringAsync');
32
+ }
33
+ return expoClipboard.setStringAsync(text, options);
34
+ }
35
+ /**
36
+ * Returns whether the clipboard has text content. Returns `true` for both plain text and rich
37
+ * text (e.g. HTML).
38
+ */
39
+ export function hasStringAsync() {
40
+ if (!expoClipboard.hasStringAsync) {
41
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasStringAsync');
42
+ }
43
+ return expoClipboard.hasStringAsync();
44
+ }
45
+ /**
46
+ * Gets the URL from the user's clipboard.
47
+ *
48
+ * Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
49
+ * platform limitations, there is no way to distinguish between no URL in clipboard and denied
50
+ * permission.
51
+ * @platform ios
52
+ */
53
+ export async function getUrlAsync() {
54
+ if (!expoClipboard.getUrlAsync) {
55
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getUrlAsync');
56
+ }
57
+ return expoClipboard.getUrlAsync();
58
+ }
59
+ /**
60
+ * Sets a URL in the user's clipboard. Behaves the same as `setStringAsync`, except that it sets
61
+ * the clipboard content type to be a URL, letting your app or other apps know that the clipboard
62
+ * contains a URL and behave accordingly.
63
+ * @platform ios
64
+ */
65
+ export async function setUrlAsync(url) {
66
+ if (!expoClipboard.setUrlAsync) {
67
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setUrlAsync');
68
+ }
69
+ return expoClipboard.setUrlAsync(url);
70
+ }
71
+ /**
72
+ * Returns whether the clipboard has URL content.
73
+ * @platform ios
74
+ */
75
+ export async function hasUrlAsync() {
76
+ if (!expoClipboard.hasUrlAsync) {
77
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasUrlAsync');
78
+ }
79
+ return expoClipboard.hasUrlAsync();
80
+ }
81
+ /**
82
+ * Gets the image from the user's clipboard and returns it in the specified format.
83
+ *
84
+ * Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
85
+ * platform limitations, there is no way to distinguish between no image in clipboard and denied
86
+ * permission.
87
+ *
88
+ * @param options Specifies the desired format of the image.
89
+ * @returns If there was an image in the clipboard, resolves to an `IClipboardImage` object
90
+ * containing the base64 string and metadata of the image. Otherwise resolves to `null` (this
91
+ * includes cases where permission was denied).
92
+ */
93
+ export async function getImageAsync(options) {
94
+ if (!expoClipboard.getImageAsync) {
95
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getImageAsync');
96
+ }
97
+ return expoClipboard.getImageAsync(options);
98
+ }
99
+ /**
100
+ * Sets an image in the user's clipboard.
101
+ *
102
+ * @param base64Image Image encoded as a base64 string, without MIME type.
103
+ */
104
+ export async function setImageAsync(base64Image) {
105
+ if (!expoClipboard.setImageAsync) {
106
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setImageAsync');
107
+ }
108
+ return expoClipboard.setImageAsync(base64Image);
109
+ }
110
+ /**
111
+ * Returns whether the clipboard has image content.
112
+ */
113
+ export async function hasImageAsync() {
114
+ if (!expoClipboard.hasImageAsync) {
115
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasImageAsync');
116
+ }
117
+ return expoClipboard.hasImageAsync();
118
+ }
119
+ /**
120
+ * Adds a listener that fires whenever the content of the user's clipboard changes. Kept here at
121
+ * the core level, framework-agnostic, exactly like `Accelerometer.addListener` lives in
122
+ * `@symbiote-native/sensors`' core — each adapter's `useClipboard` hook/composable/service wraps
123
+ * this in its own mount/unmount lifecycle rather than reimplementing the subscription.
124
+ *
125
+ * @param listener Callback invoked with an `IClipboardEvent` describing the new clipboard
126
+ * content types whenever the clipboard changes.
127
+ */
128
+ export function addClipboardListener(listener) {
129
+ return expoClipboard.addListener(CLIPBOARD_CHANGED_EVENT_NAME, listener);
130
+ }
131
+ /**
132
+ * Removes the listener added by `addClipboardListener`.
133
+ * @deprecated use `subscription.remove()` instead.
134
+ */
135
+ export function removeClipboardListener(subscription) {
136
+ subscription.remove();
137
+ }
@@ -0,0 +1,3 @@
1
+ export { getStringAsync, setStringAsync, hasStringAsync, getUrlAsync, setUrlAsync, hasUrlAsync, getImageAsync, setImageAsync, hasImageAsync, addClipboardListener, removeClipboardListener, } from './clipboard';
2
+ export { ContentType, StringFormat, type IGetStringOptions, type ISetStringOptions, type IGetImageOptions, type IClipboardImage, type IClipboardEvent, } from './types';
3
+ export type { EventSubscription } from 'expo-modules-core';
@@ -0,0 +1,2 @@
1
+ export { getStringAsync, setStringAsync, hasStringAsync, getUrlAsync, setUrlAsync, hasUrlAsync, getImageAsync, setImageAsync, hasImageAsync, addClipboardListener, removeClipboardListener, } from './clipboard.js';
2
+ export { ContentType, StringFormat, } from './types.js';
@@ -0,0 +1,17 @@
1
+ import type { EventSubscription } from 'expo-modules-core';
2
+ import type { IClipboardEvent, IClipboardImage, IGetImageOptions, IGetStringOptions, ISetStringOptions } from './types';
3
+ export declare const CLIPBOARD_CHANGED_EVENT_NAME = "onClipboardChanged";
4
+ export type INativeClipboardModule = {
5
+ addListener(eventName: typeof CLIPBOARD_CHANGED_EVENT_NAME, listener: (event: IClipboardEvent) => void): EventSubscription;
6
+ removeAllListeners(eventName: typeof CLIPBOARD_CHANGED_EVENT_NAME): void;
7
+ getStringAsync?(options?: IGetStringOptions): Promise<string>;
8
+ setStringAsync?(text: string, options?: ISetStringOptions): Promise<boolean>;
9
+ hasStringAsync?(): Promise<boolean>;
10
+ getUrlAsync?(): Promise<string | null>;
11
+ setUrlAsync?(url: string): Promise<void>;
12
+ hasUrlAsync?(): Promise<boolean>;
13
+ getImageAsync?(options: IGetImageOptions): Promise<IClipboardImage | null>;
14
+ setImageAsync?(base64Image: string): Promise<void>;
15
+ hasImageAsync?(): Promise<boolean>;
16
+ };
17
+ export declare const expoClipboard: INativeClipboardModule;
@@ -0,0 +1,7 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_CLIPBOARD_MODULE_NAME = 'ExpoClipboard';
3
+ // The event name upstream's own ExpoClipboard.ts pairs with the native module's event map —
4
+ // kept here, next to the module resolution, since core/clipboard.ts's addClipboardListener is
5
+ // the only caller.
6
+ export const CLIPBOARD_CHANGED_EVENT_NAME = 'onClipboardChanged';
7
+ export const expoClipboard = requireNativeModule(EXPO_CLIPBOARD_MODULE_NAME);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Type used to define what type of data is stored in the clipboard.
3
+ */
4
+ export declare enum ContentType {
5
+ PLAIN_TEXT = "plain-text",
6
+ HTML = "html",
7
+ IMAGE = "image",
8
+ /**
9
+ * @platform iOS
10
+ */
11
+ URL = "url"
12
+ }
13
+ /**
14
+ * Type used to determine string format stored in the clipboard.
15
+ */
16
+ export declare enum StringFormat {
17
+ PLAIN_TEXT = "plainText",
18
+ HTML = "html"
19
+ }
20
+ export type IGetStringOptions = {
21
+ /**
22
+ * The target format of the clipboard string to be converted to, if possible.
23
+ * @default StringFormat.PLAIN_TEXT
24
+ */
25
+ preferredFormat?: StringFormat;
26
+ };
27
+ export type ISetStringOptions = {
28
+ /**
29
+ * The input format of the provided string. Adjusting this option can help other applications
30
+ * interpret copied string properly.
31
+ * @default StringFormat.PLAIN_TEXT
32
+ */
33
+ inputFormat?: StringFormat;
34
+ };
35
+ export type IGetImageOptions = {
36
+ /**
37
+ * The format of the clipboard image to be converted to.
38
+ */
39
+ format: 'png' | 'jpeg';
40
+ /**
41
+ * Specify the quality of the returned image, between `0` and `1`. Defaults to `1` (highest
42
+ * quality). Applicable only when `format` is set to `jpeg`, ignored otherwise.
43
+ * @default 1
44
+ */
45
+ jpegQuality?: number;
46
+ };
47
+ export type IClipboardImage = {
48
+ /**
49
+ * A Base64-encoded string of the image data, already prepended with a
50
+ * `data:image/png;base64,` or `data:image/jpeg;base64,` prefix. Its format depends on the
51
+ * `format` option passed to `getImageAsync`.
52
+ */
53
+ data: string;
54
+ /**
55
+ * Dimensions (`width` and `height`) of the image pasted from clipboard.
56
+ */
57
+ size: {
58
+ width: number;
59
+ height: number;
60
+ };
61
+ };
62
+ export type IClipboardEvent = {
63
+ /**
64
+ * An array of content types that are available on the clipboard.
65
+ */
66
+ contentTypes: ContentType[];
67
+ };