@symbiote-native/sharing 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +80 -52
  2. package/build/angular/index.d.ts +1 -0
  3. package/build/angular/index.js +1 -3
  4. package/build/angular/inject-incoming-share.d.ts +4 -0
  5. package/build/angular/inject-incoming-share.js +5 -0
  6. package/build/core/incoming-share-hook.d.ts +4 -0
  7. package/build/core/incoming-share-hook.js +13 -0
  8. package/build/core/incoming-share-result.d.ts +3 -0
  9. package/build/core/incoming-share-result.js +11 -0
  10. package/build/core/incoming-share-store.d.ts +9 -0
  11. package/build/core/incoming-share-store.js +100 -0
  12. package/build/core/index.d.ts +5 -2
  13. package/build/core/index.js +3 -1
  14. package/build/core/native-module.d.ts +4 -1
  15. package/build/core/sharing.d.ts +4 -1
  16. package/build/core/sharing.js +20 -0
  17. package/build/core/types.d.ts +41 -0
  18. package/build/react/index.d.ts +2 -0
  19. package/build/react/index.js +2 -0
  20. package/build/react/use-incoming-share.d.ts +3 -0
  21. package/build/react/use-incoming-share.js +10 -0
  22. package/build/solid/index.d.ts +2 -0
  23. package/build/solid/index.js +2 -0
  24. package/build/solid/use-incoming-share.d.ts +4 -0
  25. package/build/solid/use-incoming-share.js +5 -0
  26. package/build/svelte/index.d.ts +2 -0
  27. package/build/svelte/index.js +2 -0
  28. package/build/svelte/use-incoming-share.svelte.d.ts +7 -0
  29. package/build/svelte/use-incoming-share.svelte.js +8 -0
  30. package/build/vue/index.d.ts +2 -0
  31. package/build/vue/index.js +2 -0
  32. package/build/vue/use-incoming-share.d.ts +4 -0
  33. package/build/vue/use-incoming-share.js +5 -0
  34. package/build-ngc/angular/index.d.ts +1 -0
  35. package/build-ngc/angular/index.js +1 -3
  36. package/build-ngc/angular/index.js.map +1 -1
  37. package/build-ngc/angular/inject-incoming-share.d.ts +4 -0
  38. package/build-ngc/angular/inject-incoming-share.js +6 -0
  39. package/build-ngc/angular/inject-incoming-share.js.map +1 -0
  40. package/build-ngc/core/incoming-share-hook.d.ts +4 -0
  41. package/build-ngc/core/incoming-share-hook.js +14 -0
  42. package/build-ngc/core/incoming-share-hook.js.map +1 -0
  43. package/build-ngc/core/incoming-share-result.d.ts +3 -0
  44. package/build-ngc/core/incoming-share-result.js +12 -0
  45. package/build-ngc/core/incoming-share-result.js.map +1 -0
  46. package/build-ngc/core/incoming-share-store.d.ts +9 -0
  47. package/build-ngc/core/incoming-share-store.js +101 -0
  48. package/build-ngc/core/incoming-share-store.js.map +1 -0
  49. package/build-ngc/core/index.d.ts +5 -2
  50. package/build-ngc/core/index.js +3 -1
  51. package/build-ngc/core/index.js.map +1 -1
  52. package/build-ngc/core/native-module.d.ts +4 -1
  53. package/build-ngc/core/native-module.js.map +1 -1
  54. package/build-ngc/core/sharing.d.ts +4 -1
  55. package/build-ngc/core/sharing.js +20 -0
  56. package/build-ngc/core/sharing.js.map +1 -1
  57. package/build-ngc/core/types.d.ts +41 -0
  58. package/package.json +23 -23
  59. package/src/angular/index.ts +1 -3
  60. package/src/angular/inject-incoming-share.ts +13 -0
  61. package/src/core/incoming-share-hook.ts +28 -0
  62. package/src/core/incoming-share-result.ts +17 -0
  63. package/src/core/incoming-share-store.ts +118 -0
  64. package/src/core/index.ts +26 -2
  65. package/src/core/native-module.ts +10 -4
  66. package/src/core/sharing.ts +33 -1
  67. package/src/core/types.ts +52 -0
  68. package/src/react/index.ts +2 -0
  69. package/src/react/use-incoming-share.ts +24 -0
  70. package/src/solid/index.ts +2 -0
  71. package/src/solid/use-incoming-share.ts +13 -0
  72. package/src/svelte/index.ts +2 -0
  73. package/src/svelte/svelte-ambient.d.ts +3 -0
  74. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  75. package/src/svelte/use-incoming-share.svelte.ts +22 -0
  76. package/src/vue/index.ts +2 -0
  77. package/src/vue/use-incoming-share.ts +13 -0
package/README.md CHANGED
@@ -1,34 +1,29 @@
1
1
  # @symbiote-native/sharing
2
2
 
3
- A wrapper package for [SymbioteNative](../../README.md) that makes
4
- [`expo-sharing`](https://github.com/expo/expo/tree/main/packages/expo-sharing) — the platform
5
- share sheet for a local file — usable from **every** adapter, React, Vue, Svelte, Solid, and
6
- Angular, not just React. Built the same way as [`@symbiote-native/secure-store`](../secure-store): an
7
- `expo-modules-core`-based wrapper (see the `symbiote-expo-native-module` project skill for the
8
- full mechanism — why `expo-modules-core` is depended on directly and never the `expo`
9
- meta-package, why the upstream JS is hand-ported into `core/` rather than imported, and how
10
- autolinking picks up the native module).
3
+ Let users send a file from your app to any other app, and receive files other apps share into
4
+ yours. One API for every [SymbioteNative](../../README.md) adapter (React, Vue, Svelte, Solid and
5
+ Angular).
11
6
 
12
- ## Scope: outgoing share only
7
+ It wraps [`expo-sharing`](https://github.com/expo/expo/tree/main/packages/expo-sharing) the same
8
+ way [`@symbiote-native/secure-store`](../secure-store) wraps its upstream: `expo-modules-core` is a
9
+ direct dependency, never the `expo` meta-package, and the upstream JS is hand-ported into `core/`.
10
+ The mechanics live in the `symbiote-expo-native-module` project skill.
13
11
 
14
- `expo-sharing` has two halves. This package ships one of them.
12
+ ## Scope: outgoing and incoming share
13
+
14
+ `expo-sharing` has two halves, both ported to every adapter.
15
15
 
16
16
  | Half | Upstream API | Here |
17
17
  | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------- |
18
- | **Outgoing** — hand a local file to another app | `shareAsync`, `isAvailableAsync` | ✅ ported in full |
19
- | **Incoming** — receive files other apps share _into_ your app | `useIncomingShare`, `getSharedPayloads`, `getResolvedSharedPayloadsAsync`, `clearSharedPayloads` | ❌ not ported |
20
-
21
- The incoming half is not a thinner JS surface — it needs a whole **iOS Share Extension target**.
22
- Upstream's config plugin (`plugin/src/ios/*`) generates one: a second Xcode target with its own
23
- build phases, its own `Info.plist`, an entitlements file, and an App Group ID shared between the
24
- app and the extension, plus Android intent filters on the main activity. That is native
25
- app-extension scaffolding, the same category this repo already parks `expo-widgets` in — no
26
- JS-reachable runtime module can stand in for it, and SymbioteNative has no native-target
27
- scaffolding story yet. So it is deliberately out of scope for this pass rather than shipped
28
- half-working. If you need incoming share today, use `expo-sharing` directly in a React-only app
29
- with Expo's own prebuild pipeline.
18
+ | **Outgoing** - hand a local file to another app | `shareAsync`, `isAvailableAsync` | ported in full |
19
+ | **Incoming** - receive files other apps share _into_ your app | `useIncomingShare`, `getSharedPayloads`, `getResolvedSharedPayloadsAsync`, `clearSharedPayloads` | ported in full |
30
20
 
31
- Everything below describes the outgoing half.
21
+ The incoming JS runtime reads what the native module holds. The native module only holds data
22
+ once the host app carries a share target: an **iOS Share Extension target** (a second Xcode target
23
+ with its own `Info.plist`, entitlements and an App Group shared with the app) plus Android intent
24
+ filters on the main activity. That target is native app scaffolding, generated upstream by
25
+ `expo-sharing`'s config plugin (`plugin/src/ios/*`) or set up by hand. This package does not
26
+ generate or touch it: without a share target the incoming functions return empty arrays.
32
27
 
33
28
  ## Install
34
29
 
@@ -44,62 +39,60 @@ npx @symbiote-native/cli new my-app --sharing
44
39
  npx @symbiote-native/cli add --sharing
45
40
  ```
46
41
 
47
- Either way: installs `@symbiote-native/sharing` and wires the native autolinking automatically —
42
+ Either way: installs `@symbiote-native/sharing` and wires the native autolinking automatically -
48
43
  see [`@symbiote-native/cli`](../cli).
49
44
 
50
45
  <details>
51
- <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
46
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
52
47
 
53
48
  ```bash
54
49
  npm install @symbiote-native/sharing
55
50
  ```
56
51
 
57
- `expo-sharing` and `expo-modules-core` come along as regular, pinned dependencies — never install
52
+ `expo-sharing` and `expo-modules-core` come along as regular, pinned dependencies - never install
58
53
  either yourself, and never add the `expo` meta-package to this project (it bundles its own
59
54
  Metro/Babel pipeline that conflicts with this project's own).
60
55
 
61
56
  ### Required one-time step: native autolinking wiring
62
57
 
63
58
  Unlike a plain RN native module, `expo-sharing`'s native code is discovered by
64
- `expo-modules-autolinking` — this needs wiring into the native host app **once**, covering this
59
+ `expo-modules-autolinking` - this needs wiring into the native host app **once**, covering this
65
60
  package and every other `expo-modules-core` package with zero further changes:
66
61
 
67
62
  | Platform | Touches |
68
63
  | -------- | ------------------------------------------------------------------------------------- |
69
- | iOS | `ios/Podfile` — add `use_expo_modules!` |
70
- | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
71
- | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
72
- | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a native-module name map |
64
+ | iOS | `ios/Podfile` - add `use_expo_modules!` |
65
+ | iOS | `AppDelegate.swift` - Expo's runtime-bootstrap hook |
66
+ | Android | `settings.gradle` / `app/build.gradle` - resolve and include the Expo Gradle projects |
67
+ | Android | `MainApplication.kt` - Expo's bootstrap hook, plus a native-module name map |
73
68
 
74
69
  Full mechanics live in the `symbiote-expo-native-module` skill. The per-package half of that
75
- table — the Gradle dependency and the module map entry — is generated by
70
+ table - the Gradle dependency and the module map entry - is generated by
76
71
  [`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's
77
72
  `native-link.json` on every install.
78
73
 
79
74
  There is no `Info.plist` usage description to add: opening the share sheet needs no iOS
80
75
  permission. On Android, the `SharingFileProvider` and the `<queries>` block the chooser needs on
81
76
  API 30+ ship inside `expo-sharing`'s own `AndroidManifest.xml` and merge into your app
82
- automatically once the Gradle project is included — nothing to declare by hand.
77
+ automatically once the Gradle project is included - nothing to declare by hand.
83
78
 
84
79
  </details>
85
80
 
86
81
  ## Shape
87
82
 
88
83
  ```
89
- src/core/ the whole API: isAvailableAsync + shareAsync. native-module.ts
90
- resolves ExpoSharing through expo-modules-core's requireNativeModule.
91
- src/angular/ @symbiote-native/sharing/angular
84
+ src/core/ shareAsync, isAvailableAsync, the incoming payload functions, and the framework-agnostic
85
+ incoming-share store (refresh, AppState resync, payload comparison).
86
+ native-module.ts resolves ExpoSharing through expo-modules-core's requireNativeModule.
87
+ src/react|vue|solid|svelte/ use-incoming-share: the hook over the store
88
+ src/angular/ inject-incoming-share: the same over signals
92
89
  ```
93
90
 
94
- `./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto `src/core/` —
95
- no physical per-framework file. Both exports are stateless free functions — no per-instance state,
96
- no event stream — so there is nothing for a hook, composable, or service to wrap, the same
97
- reason [`@symbiote-native/secure-store`](../secure-store) does the same. (The incoming-share half
98
- above is exactly the part that _would_ have needed one; it is the reason this package has no
99
- `hooks/`, `composables/`, or `services/` folder.) `./angular` stays a physical file/subpath since
100
- Angular ships through a separate `ngc`/AOT build (`build-ngc/`). Import from
101
- `@symbiote-native/sharing` directly if you don't care which adapter you're on; the per-adapter
102
- subpaths exist so every wrapper package has the same import surface.
91
+ Outgoing share is two stateless free functions, so only the incoming half has adapter code. Each
92
+ adapter subpath re-exports `core` plus its own `useIncomingShare` (Angular: `injectIncomingShare`,
93
+ call it in an injection context). React returns the result object, Vue a `ComputedRef`, Solid an
94
+ `Accessor`, Svelte a `{ current }` box, Angular a `Signal`. `./angular` ships through a separate
95
+ `ngc`/AOT build (`build-ngc/`).
103
96
 
104
97
  ## Use it
105
98
 
@@ -122,12 +115,28 @@ await shareAsync(localFileUri, {
122
115
  });
123
116
  ```
124
117
 
118
+ Receiving what other apps shared into yours:
119
+
120
+ ```ts
121
+ import { useIncomingShare } from '@symbiote-native/sharing/react';
122
+
123
+ const { sharedPayloads, resolvedSharedPayloads, isResolving, error, clearSharedPayloads } =
124
+ useIncomingShare();
125
+ ```
126
+
127
+ The hook reads the payloads synchronously, resolves them after mount, and re-reads whenever the
128
+ app returns to the foreground. Resolving a shared URL may need the network.
129
+
125
130
  ## API
126
131
 
127
- | Export | Signature | Notes |
128
- | ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------- |
129
- | `isAvailableAsync` | `() => Promise<boolean>` | `true` on Android and iOS. Reports on the native module, not on any device capability. |
130
- | `shareAsync` | `(url, options?) => Promise<void>` | Opens the share sheet for a local file. Resolves when the sheet is dismissed. |
132
+ | Export | Signature | Notes |
133
+ | -------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------- |
134
+ | `isAvailableAsync` | `() => Promise<boolean>` | `true` on Android and iOS. Reports on the native module, not on any device capability. |
135
+ | `shareAsync` | `(url, options?) => Promise<void>` | Opens the share sheet for a local file. Resolves when the sheet is dismissed. |
136
+ | `getSharedPayloads` | `() => ISharePayload[]` | Raw data shared with the app, empty when nothing was shared. |
137
+ | `getResolvedSharedPayloadsAsync` | `() => Promise<IResolvedSharePayload[]>` | Adds display details per payload. |
138
+ | `clearSharedPayloads` | `() => void` | Clears the data shared with the app. |
139
+ | `useIncomingShare` | `() => IUseIncomingShareResult` | Payloads, resolved payloads, `isResolving`, `error`, `clearSharedPayloads`, `refreshSharePayloads`. |
131
140
 
132
141
  `ISharingOptions`: `mimeType` (Android), `UTI` (iOS), `dialogTitle`, `anchor` (iOS iPad).
133
142
  `ISharingAnchor`: `x`, `y`, `width`, `height`, all optional, all in points.
@@ -135,7 +144,7 @@ await shareAsync(localFileUri, {
135
144
  ## Notes
136
145
 
137
146
  - **`url` must be local.** A `file://` URI or a path from a file-system API. A remote `http(s)`
138
- URL is not downloaded first — fetch it to a local file yourself, then share that.
147
+ URL is not downloaded first - fetch it to a local file yourself, then share that.
139
148
  - **A resolved promise is not a delivery receipt.** Neither platform reports which app the user
140
149
  picked, or whether they picked one at all: the promise resolves when the sheet closes. iOS
141
150
  resolves on every dismissal path, including "picked Print, then cancelled the print dialog".
@@ -144,16 +153,35 @@ await shareAsync(localFileUri, {
144
153
  with upstream, not because it changes behavior.
145
154
  - **Android runs one share at a time.** A second `shareAsync` while a chooser is open rejects
146
155
  rather than queueing.
147
- - **The url is validated before the native call** — non-empty string — so a bad argument fails
156
+ - **The iOS share extension is experimental upstream.** It opens the main app target instead of
157
+ handling the share in its own view controller, which Apple does not officially support and may
158
+ stop working in a future iOS release.
159
+ - **The url is validated before the native call**: non-empty string - so a bad argument fails
148
160
  with a readable message instead of as an argument-conversion failure on iOS or a generic
149
161
  "Failed to share the file" on Android. Upstream has no such guard.
150
162
 
163
+ ## Common questions
164
+
165
+ - **Android: "Not allowed to read file under given URL" / "Failed to find configured root".** Share
166
+ a file from the app's own document or cache directory (for example one written with
167
+ [`@symbiote-native/file-system`](../file-system)). A path owned by another app, such as a media
168
+ library asset, can be rejected: copy it into `Paths.cache` first.
169
+ - **Works on iOS, the receiving Android app cannot open it.** Same cause: Android hands the other
170
+ app a content URI, and only files in the app's directories are covered.
171
+ - **Can I share a URL or text?** Not with `shareAsync`, which takes a local file.
172
+ - **Did the user actually share?** Unknowable: the promise resolves when the sheet closes.
173
+
174
+ Sources: [Expo docs: Sharing](https://docs.expo.dev/versions/latest/sdk/sharing/),
175
+ [expo/expo#5933](https://github.com/expo/expo/issues/5933),
176
+ [expo/expo#18887](https://github.com/expo/expo/issues/18887),
177
+ [expo/expo#9222](https://github.com/expo/expo/issues/9222).
178
+
151
179
  ## Test it
152
180
 
153
181
  ```bash
154
182
  pnpm vitest run packages/sharing
155
183
  ```
156
184
 
157
- The core tests fake the native module in place of `requireNativeModule`'s runtime resolution —
185
+ The core tests fake the native module in place of `requireNativeModule`'s runtime resolution -
158
186
  `ExpoSharing` only exists on a device, so a headless run would otherwise throw at import. The
159
187
  share sheet itself can only be verified on a device or simulator.
@@ -1 +1,2 @@
1
1
  export * from '../core';
2
+ export { injectIncomingShare } from './inject-incoming-share';
@@ -1,4 +1,2 @@
1
- // @symbiote-native/sharing/angular: the Angular entry over the framework-agnostic core. Same
2
- // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
- // service, so this is a plain re-export.
4
1
  export * from '../core/index.js';
2
+ export { injectIncomingShare } from './inject-incoming-share.js';
@@ -0,0 +1,4 @@
1
+ import { type Signal } from '@angular/core';
2
+ import type { IUseIncomingShareResult } from '../core/types';
3
+ /** Angular twin of `expo-sharing`'s `useIncomingShare`, call it in an injection context */
4
+ export declare const injectIncomingShare: () => Signal<IUseIncomingShareResult>;
@@ -0,0 +1,5 @@
1
+ import { computed } from '@angular/core';
2
+ import { createEventValueHook } from '@symbiote-native/angular';
3
+ import { createIncomingShareHook } from '../core/incoming-share-hook.js';
4
+ /** Angular twin of `expo-sharing`'s `useIncomingShare`, call it in an injection context */
5
+ export const injectIncomingShare = createIncomingShareHook(createEventValueHook, box => box(), computed);
@@ -0,0 +1,4 @@
1
+ import { INCOMING_SHARE_CHANGE } from './incoming-share-store';
2
+ import type { IIncomingShareStore } from './incoming-share-store';
3
+ import type { IIncomingShareSnapshot, IUseIncomingShareResult } from './types';
4
+ export declare function createIncomingShareHook<TSnapshotBox, TResultBox>(createEventValueHook: (event: typeof INCOMING_SHARE_CHANGE, getValue: (store: IIncomingShareStore) => IIncomingShareSnapshot) => (getSource: () => IIncomingShareStore) => TSnapshotBox, readBox: (box: TSnapshotBox) => IIncomingShareSnapshot, derive: (compute: () => IUseIncomingShareResult) => TResultBox): () => TResultBox;
@@ -0,0 +1,13 @@
1
+ import { createIncomingShareStore, INCOMING_SHARE_CHANGE, } from './incoming-share-store.js';
2
+ import { withShareActions } from './incoming-share-result.js';
3
+ // Getter-shaped `useIncomingShare` for the adapters whose event-value hook takes a source getter
4
+ // (Vue, Solid, Svelte, Angular). `readBox` unwraps that adapter's reactive box and `derive` wraps
5
+ // the result in its own computed, React keeps its own wiring since it passes the source directly
6
+ export function createIncomingShareHook(createEventValueHook, readBox, derive) {
7
+ const useShareSnapshot = createEventValueHook(INCOMING_SHARE_CHANGE, store => store.getSnapshot());
8
+ return function useIncomingShare() {
9
+ const store = createIncomingShareStore();
10
+ const snapshotBox = useShareSnapshot(() => store);
11
+ return derive(() => withShareActions(readBox(snapshotBox), store));
12
+ };
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { IIncomingShareStore } from './incoming-share-store';
2
+ import type { IIncomingShareSnapshot, IUseIncomingShareResult } from './types';
3
+ export declare function withShareActions(snapshot: IIncomingShareSnapshot, store: IIncomingShareStore): IUseIncomingShareResult;
@@ -0,0 +1,11 @@
1
+ // Every adapter's `useIncomingShare` reports its own reactive snapshot, the two actions come
2
+ // from the store so they are written once
3
+ export function withShareActions(snapshot, store) {
4
+ return {
5
+ ...snapshot,
6
+ clearSharedPayloads: store.clear,
7
+ refreshSharePayloads: () => {
8
+ void store.refresh();
9
+ },
10
+ };
11
+ }
@@ -0,0 +1,9 @@
1
+ import type { IEventValueSource } from '@symbiote-native/engine';
2
+ import type { IIncomingShareSnapshot } from './types';
3
+ export declare const INCOMING_SHARE_CHANGE = "change";
4
+ export type IIncomingShareStore = IEventValueSource<IIncomingShareSnapshot, typeof INCOMING_SHARE_CHANGE> & {
5
+ getSnapshot: () => IIncomingShareSnapshot;
6
+ refresh: () => Promise<void>;
7
+ clear: () => void;
8
+ };
9
+ export declare function createIncomingShareStore(): IIncomingShareStore;
@@ -0,0 +1,100 @@
1
+ import { AppState } from '@symbiote-native/engine';
2
+ import { clearSharedPayloads, getResolvedSharedPayloadsAsync, getSharedPayloads, } from './sharing.js';
3
+ export const INCOMING_SHARE_CHANGE = 'change';
4
+ // Multiset comparison, so the same payloads in another order do not trigger a re-resolve
5
+ function sharePayloadsAreEqual(a, b) {
6
+ if (a.length !== b.length)
7
+ return false;
8
+ const counts = new Map();
9
+ const keyOf = (item) => `${item.value}|${item.mimeType}|${item.shareType}`;
10
+ for (const item of a)
11
+ counts.set(keyOf(item), (counts.get(keyOf(item)) ?? 0) + 1);
12
+ for (const item of b) {
13
+ const count = counts.get(keyOf(item));
14
+ if (!count)
15
+ return false;
16
+ counts.set(keyOf(item), count - 1);
17
+ }
18
+ return true;
19
+ }
20
+ function toError(cause, fallbackMessage) {
21
+ return cause instanceof Error ? cause : new Error(fallbackMessage);
22
+ }
23
+ export function createIncomingShareStore() {
24
+ let snapshot = {
25
+ sharedPayloads: getSharedPayloads(),
26
+ resolvedSharedPayloads: [],
27
+ isResolving: false,
28
+ error: null,
29
+ };
30
+ // Starts empty, not at the first snapshot, so a first refresh with data always resolves it
31
+ let lastRefreshed = [];
32
+ const listeners = new Set();
33
+ let appStateSubscription = null;
34
+ function update(patch) {
35
+ snapshot = { ...snapshot, ...patch };
36
+ for (const listener of listeners)
37
+ listener(snapshot);
38
+ }
39
+ async function resolvePayloads() {
40
+ update({ isResolving: true });
41
+ try {
42
+ update({
43
+ resolvedSharedPayloads: await getResolvedSharedPayloadsAsync(),
44
+ });
45
+ }
46
+ catch (cause) {
47
+ update({
48
+ error: toError(cause, 'Unknown error during shared payload resolution'),
49
+ });
50
+ }
51
+ finally {
52
+ update({ isResolving: false });
53
+ }
54
+ }
55
+ async function refresh() {
56
+ try {
57
+ const next = getSharedPayloads();
58
+ // Skip the resolve when nothing changed, it can cost a network round trip
59
+ if (sharePayloadsAreEqual(next, lastRefreshed))
60
+ return;
61
+ lastRefreshed = next;
62
+ update({ sharedPayloads: next, resolvedSharedPayloads: [], error: null });
63
+ if (next.length > 0)
64
+ await resolvePayloads();
65
+ }
66
+ catch (cause) {
67
+ update({ error: toError(cause, 'Failed to resolve data') });
68
+ }
69
+ }
70
+ function start() {
71
+ void refresh();
72
+ appStateSubscription = AppState.addEventListener('change', status => {
73
+ if (status === 'active')
74
+ void refresh();
75
+ });
76
+ }
77
+ function stop() {
78
+ appStateSubscription?.remove();
79
+ appStateSubscription = null;
80
+ }
81
+ return {
82
+ getSnapshot: () => snapshot,
83
+ refresh,
84
+ clear: clearSharedPayloads,
85
+ addListener(_event, listener) {
86
+ const wasIdle = listeners.size === 0;
87
+ listeners.add(listener);
88
+ // After `add`, so the first refresh's synchronous update reaches this listener
89
+ if (wasIdle)
90
+ start();
91
+ return {
92
+ remove: () => {
93
+ listeners.delete(listener);
94
+ if (listeners.size === 0)
95
+ stop();
96
+ },
97
+ };
98
+ },
99
+ };
100
+ }
@@ -1,2 +1,5 @@
1
- export { isAvailableAsync, shareAsync } from './sharing';
2
- export type { ISharingAnchor, ISharingOptions } from './types';
1
+ export { clearSharedPayloads, getResolvedSharedPayloadsAsync, getSharedPayloads, isAvailableAsync, shareAsync, } from './sharing';
2
+ export { createIncomingShareStore, INCOMING_SHARE_CHANGE, } from './incoming-share-store';
3
+ export type { IIncomingShareStore } from './incoming-share-store';
4
+ export { withShareActions } from './incoming-share-result';
5
+ export type { IBaseResolvedSharePayload, IContentType, IIncomingShareSnapshot, IResolvedSharePayload, IShareType, ISharePayload, ISharingAnchor, ISharingOptions, ITextBasedResolvedSharePayload, IUriBasedResolvedSharePayload, IUseIncomingShareResult, } from './types';
@@ -1 +1,3 @@
1
- export { isAvailableAsync, shareAsync } from './sharing.js';
1
+ export { clearSharedPayloads, getResolvedSharedPayloadsAsync, getSharedPayloads, isAvailableAsync, shareAsync, } from './sharing.js';
2
+ export { createIncomingShareStore, INCOMING_SHARE_CHANGE, } from './incoming-share-store.js';
3
+ export { withShareActions } from './incoming-share-result.js';
@@ -1,6 +1,9 @@
1
- import type { ISharingOptions } from './types';
1
+ import type { IResolvedSharePayload, ISharePayload, ISharingOptions } from './types';
2
2
  export type INativeSharingModule = {
3
3
  isAvailableAsync?(): Promise<boolean>;
4
4
  shareAsync?(url: string, options: ISharingOptions): Promise<void>;
5
+ getSharedPayloads?(): ISharePayload[];
6
+ getResolvedSharedPayloadsAsync?(): Promise<IResolvedSharePayload[]>;
7
+ clearSharedPayloads?(): void;
5
8
  };
6
9
  export declare const expoSharing: INativeSharingModule;
@@ -1,4 +1,4 @@
1
- import type { ISharingOptions } from './types';
1
+ import type { IResolvedSharePayload, ISharePayload, ISharingOptions } from './types';
2
2
  /**
3
3
  * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
4
4
  *
@@ -16,3 +16,6 @@ export declare function isAvailableAsync(): Promise<boolean>;
16
16
  * platform reports which app received the file, so a resolved promise is not a delivery receipt.
17
17
  */
18
18
  export declare function shareAsync(url: string, options?: ISharingOptions): Promise<void>;
19
+ export declare function getSharedPayloads(): ISharePayload[];
20
+ export declare function getResolvedSharedPayloadsAsync(): Promise<IResolvedSharePayload[]>;
21
+ export declare function clearSharedPayloads(): void;
@@ -29,6 +29,26 @@ export async function shareAsync(url, options = {}) {
29
29
  }
30
30
  await expoSharing.shareAsync(url, options);
31
31
  }
32
+ // Empty until the host app carries the share target (iOS Share Extension, Android intent filters)
33
+ export function getSharedPayloads() {
34
+ if (!expoSharing.getSharedPayloads) {
35
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getSharedPayloads');
36
+ }
37
+ return expoSharing.getSharedPayloads();
38
+ }
39
+ // Adds display details per payload, resolving a shared URL may need the network
40
+ export async function getResolvedSharedPayloadsAsync() {
41
+ if (!expoSharing.getResolvedSharedPayloadsAsync) {
42
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getResolvedSharedPayloadsAsync');
43
+ }
44
+ return expoSharing.getResolvedSharedPayloadsAsync();
45
+ }
46
+ export function clearSharedPayloads() {
47
+ if (!expoSharing.clearSharedPayloads) {
48
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'clearSharedPayloads');
49
+ }
50
+ expoSharing.clearSharedPayloads();
51
+ }
32
52
  // Not in upstream, which hands anything straight to the native module. An empty or non-string
33
53
  // url surfaces there as an argument-conversion failure on iOS and a generic "Failed to share the
34
54
  // file" on Android — both far enough from the call site to be worth failing early with the
@@ -36,3 +36,44 @@ export type ISharingOptions = {
36
36
  */
37
37
  anchor?: ISharingAnchor;
38
38
  };
39
+ export type IShareType = 'text' | 'url' | 'audio' | 'image' | 'video' | 'file';
40
+ export type IContentType = 'text' | 'audio' | 'image' | 'video' | 'file' | 'website';
41
+ /** Raw data shared with the app (android, ios) */
42
+ export type ISharePayload = {
43
+ /** Message body for `text`, the URL for `url`, otherwise typically the file URI */
44
+ value: string;
45
+ shareType: IShareType;
46
+ /** MIME type of `value` */
47
+ mimeType?: string;
48
+ };
49
+ export type IBaseResolvedSharePayload = ISharePayload & {
50
+ /** Where the content can be read from (the redirect target for a URL), null for text */
51
+ contentUri: string | null;
52
+ contentType: IContentType | null;
53
+ contentMimeType: string | null;
54
+ /** The `suggestedFilename` HTTP header if present, otherwise the last path component */
55
+ originalName: string | null;
56
+ contentSize: number | null;
57
+ };
58
+ export type IUriBasedResolvedSharePayload = IBaseResolvedSharePayload & {
59
+ contentType: 'audio' | 'file' | 'video' | 'image' | 'website';
60
+ contentUri: string;
61
+ };
62
+ export type ITextBasedResolvedSharePayload = IBaseResolvedSharePayload & {
63
+ contentType?: 'text';
64
+ };
65
+ /** A payload plus the details needed to display it, resolving a URL may need the network */
66
+ export type IResolvedSharePayload = IUriBasedResolvedSharePayload | ITextBasedResolvedSharePayload;
67
+ export type IIncomingShareSnapshot = {
68
+ /** Unresolved payloads, available synchronously from the first read */
69
+ sharedPayloads: ISharePayload[];
70
+ /** Empty while resolving or when resolving failed */
71
+ resolvedSharedPayloads: IResolvedSharePayload[];
72
+ isResolving: boolean;
73
+ /** The error hit while resolving, null on success */
74
+ error: Error | null;
75
+ };
76
+ export type IUseIncomingShareResult = IIncomingShareSnapshot & {
77
+ clearSharedPayloads: () => void;
78
+ refreshSharePayloads: () => void;
79
+ };
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useIncomingShare } from './use-incoming-share';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useIncomingShare } from './use-incoming-share.js';
@@ -0,0 +1,3 @@
1
+ import type { IUseIncomingShareResult } from '../core/types';
2
+ /** React twin of `expo-sharing`'s `useIncomingShare` */
3
+ export declare function useIncomingShare(): IUseIncomingShareResult;
@@ -0,0 +1,10 @@
1
+ import { useState } from 'react';
2
+ import { createEventValueHook } from '@symbiote-native/react';
3
+ import { createIncomingShareStore, INCOMING_SHARE_CHANGE, } from '../core/incoming-share-store.js';
4
+ import { withShareActions } from '../core/incoming-share-result.js';
5
+ const useShareSnapshot = createEventValueHook(INCOMING_SHARE_CHANGE, store => store.getSnapshot());
6
+ /** React twin of `expo-sharing`'s `useIncomingShare` */
7
+ export function useIncomingShare() {
8
+ const [store] = useState(createIncomingShareStore);
9
+ return withShareActions(useShareSnapshot(store), store);
10
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useIncomingShare } from './use-incoming-share';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useIncomingShare } from './use-incoming-share.js';
@@ -0,0 +1,4 @@
1
+ import { type Accessor } from 'solid-js';
2
+ import type { IUseIncomingShareResult } from '../core/types';
3
+ /** Solid twin of `expo-sharing`'s `useIncomingShare` */
4
+ export declare const useIncomingShare: () => Accessor<IUseIncomingShareResult>;
@@ -0,0 +1,5 @@
1
+ import { createMemo } from 'solid-js';
2
+ import { createEventValueHook } from '@symbiote-native/solid';
3
+ import { createIncomingShareHook } from '../core/incoming-share-hook.js';
4
+ /** Solid twin of `expo-sharing`'s `useIncomingShare` */
5
+ export const useIncomingShare = createIncomingShareHook(createEventValueHook, box => box(), createMemo);
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useIncomingShare } from './use-incoming-share.svelte';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useIncomingShare } from './use-incoming-share.svelte';
@@ -0,0 +1,7 @@
1
+ import type { IUseIncomingShareResult } from '../core/types';
2
+ type IBox<TValue> = {
3
+ readonly current: TValue;
4
+ };
5
+ /** Svelte twin of `expo-sharing`'s `useIncomingShare` */
6
+ export declare const useIncomingShare: () => IBox<IUseIncomingShareResult>;
7
+ export {};
@@ -0,0 +1,8 @@
1
+ import { createEventValueHook } from '@symbiote-native/svelte/runes/create-event-value-hook';
2
+ import { createIncomingShareHook } from '../core/incoming-share-hook.js';
3
+ /** Svelte twin of `expo-sharing`'s `useIncomingShare` */
4
+ export const useIncomingShare = createIncomingShareHook(createEventValueHook, box => box.current, compute => ({
5
+ get current() {
6
+ return compute();
7
+ },
8
+ }));
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useIncomingShare } from './use-incoming-share';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useIncomingShare } from './use-incoming-share.js';
@@ -0,0 +1,4 @@
1
+ import { type ComputedRef } from '@vue/runtime-core';
2
+ import type { IUseIncomingShareResult } from '../core/types';
3
+ /** Vue twin of `expo-sharing`'s `useIncomingShare` */
4
+ export declare const useIncomingShare: () => ComputedRef<IUseIncomingShareResult>;
@@ -0,0 +1,5 @@
1
+ import { computed } from '@vue/runtime-core';
2
+ import { createEventValueHook } from '@symbiote-native/vue';
3
+ import { createIncomingShareHook } from '../core/incoming-share-hook.js';
4
+ /** Vue twin of `expo-sharing`'s `useIncomingShare` */
5
+ export const useIncomingShare = createIncomingShareHook(createEventValueHook, box => box.value, computed);
@@ -1 +1,2 @@
1
1
  export * from '../core';
2
+ export { injectIncomingShare } from './inject-incoming-share';
@@ -1,5 +1,3 @@
1
- // @symbiote-native/sharing/angular: the Angular entry over the framework-agnostic core. Same
2
- // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
- // service, so this is a plain re-export.
4
1
  export * from '../core';
2
+ export { injectIncomingShare } from './inject-incoming-share';
5
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,0FAA0F;AAC1F,yCAAyC;AACzC,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type Signal } from '@angular/core';
2
+ import type { IUseIncomingShareResult } from '../core/types';
3
+ /** Angular twin of `expo-sharing`'s `useIncomingShare`, call it in an injection context */
4
+ export declare const injectIncomingShare: () => Signal<IUseIncomingShareResult>;