@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.
- package/README.md +80 -52
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +1 -3
- package/build/angular/inject-incoming-share.d.ts +4 -0
- package/build/angular/inject-incoming-share.js +5 -0
- package/build/core/incoming-share-hook.d.ts +4 -0
- package/build/core/incoming-share-hook.js +13 -0
- package/build/core/incoming-share-result.d.ts +3 -0
- package/build/core/incoming-share-result.js +11 -0
- package/build/core/incoming-share-store.d.ts +9 -0
- package/build/core/incoming-share-store.js +100 -0
- package/build/core/index.d.ts +5 -2
- package/build/core/index.js +3 -1
- package/build/core/native-module.d.ts +4 -1
- package/build/core/sharing.d.ts +4 -1
- package/build/core/sharing.js +20 -0
- package/build/core/types.d.ts +41 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +2 -0
- package/build/react/use-incoming-share.d.ts +3 -0
- package/build/react/use-incoming-share.js +10 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +2 -0
- package/build/solid/use-incoming-share.d.ts +4 -0
- package/build/solid/use-incoming-share.js +5 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +2 -0
- package/build/svelte/use-incoming-share.svelte.d.ts +7 -0
- package/build/svelte/use-incoming-share.svelte.js +8 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +2 -0
- package/build/vue/use-incoming-share.d.ts +4 -0
- package/build/vue/use-incoming-share.js +5 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +1 -3
- package/build-ngc/angular/index.js.map +1 -1
- package/build-ngc/angular/inject-incoming-share.d.ts +4 -0
- package/build-ngc/angular/inject-incoming-share.js +6 -0
- package/build-ngc/angular/inject-incoming-share.js.map +1 -0
- package/build-ngc/core/incoming-share-hook.d.ts +4 -0
- package/build-ngc/core/incoming-share-hook.js +14 -0
- package/build-ngc/core/incoming-share-hook.js.map +1 -0
- package/build-ngc/core/incoming-share-result.d.ts +3 -0
- package/build-ngc/core/incoming-share-result.js +12 -0
- package/build-ngc/core/incoming-share-result.js.map +1 -0
- package/build-ngc/core/incoming-share-store.d.ts +9 -0
- package/build-ngc/core/incoming-share-store.js +101 -0
- package/build-ngc/core/incoming-share-store.js.map +1 -0
- package/build-ngc/core/index.d.ts +5 -2
- package/build-ngc/core/index.js +3 -1
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/native-module.d.ts +4 -1
- package/build-ngc/core/native-module.js.map +1 -1
- package/build-ngc/core/sharing.d.ts +4 -1
- package/build-ngc/core/sharing.js +20 -0
- package/build-ngc/core/sharing.js.map +1 -1
- package/build-ngc/core/types.d.ts +41 -0
- package/package.json +23 -23
- package/src/angular/index.ts +1 -3
- package/src/angular/inject-incoming-share.ts +13 -0
- package/src/core/incoming-share-hook.ts +28 -0
- package/src/core/incoming-share-result.ts +17 -0
- package/src/core/incoming-share-store.ts +118 -0
- package/src/core/index.ts +26 -2
- package/src/core/native-module.ts +10 -4
- package/src/core/sharing.ts +33 -1
- package/src/core/types.ts +52 -0
- package/src/react/index.ts +2 -0
- package/src/react/use-incoming-share.ts +24 -0
- package/src/solid/index.ts +2 -0
- package/src/solid/use-incoming-share.ts +13 -0
- package/src/svelte/index.ts +2 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +7 -0
- package/src/svelte/use-incoming-share.svelte.ts +22 -0
- package/src/vue/index.ts +2 -0
- 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
|
-
|
|
4
|
-
[
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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**
|
|
19
|
-
| **Incoming**
|
|
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
|
-
|
|
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
|
|
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
|
|
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`
|
|
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`
|
|
70
|
-
| iOS | `AppDelegate.swift`
|
|
71
|
-
| Android | `settings.gradle` / `app/build.gradle`
|
|
72
|
-
| Android | `MainApplication.kt`
|
|
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
|
|
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
|
|
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/
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
128
|
-
|
|
|
129
|
-
| `isAvailableAsync`
|
|
130
|
-
| `shareAsync`
|
|
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
|
|
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
|
|
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.
|
package/build/angular/index.d.ts
CHANGED
package/build/angular/index.js
CHANGED
|
@@ -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
|
+
}
|
package/build/core/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export { isAvailableAsync, shareAsync } from './sharing';
|
|
2
|
-
export
|
|
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';
|
package/build/core/index.js
CHANGED
|
@@ -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;
|
package/build/core/sharing.d.ts
CHANGED
|
@@ -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;
|
package/build/core/sharing.js
CHANGED
|
@@ -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
|
package/build/core/types.d.ts
CHANGED
|
@@ -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,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,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,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,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,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,
|
|
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>;
|