@symbiote-native/sharing 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/core/index.d.ts +2 -0
  6. package/build/core/index.js +1 -0
  7. package/build/core/native-module.d.ts +6 -0
  8. package/build/core/native-module.js +3 -0
  9. package/build/core/sharing.d.ts +18 -0
  10. package/build/core/sharing.js +40 -0
  11. package/build/core/types.d.ts +38 -0
  12. package/build/core/types.js +1 -0
  13. package/build/react/index.d.ts +1 -0
  14. package/build/react/index.js +6 -0
  15. package/build/vue/index.d.ts +1 -0
  16. package/build/vue/index.js +4 -0
  17. package/build-ngc/angular/index.d.ts +1 -0
  18. package/build-ngc/angular/index.js +5 -0
  19. package/build-ngc/angular/index.js.map +1 -0
  20. package/build-ngc/core/index.d.ts +2 -0
  21. package/build-ngc/core/index.js +2 -0
  22. package/build-ngc/core/index.js.map +1 -0
  23. package/build-ngc/core/native-module.d.ts +6 -0
  24. package/build-ngc/core/native-module.js +4 -0
  25. package/build-ngc/core/native-module.js.map +1 -0
  26. package/build-ngc/core/sharing.d.ts +18 -0
  27. package/build-ngc/core/sharing.js +41 -0
  28. package/build-ngc/core/sharing.js.map +1 -0
  29. package/build-ngc/core/types.d.ts +38 -0
  30. package/build-ngc/core/types.js +2 -0
  31. package/build-ngc/core/types.js.map +1 -0
  32. package/native-link.json +12 -0
  33. package/package.json +107 -0
  34. package/src/angular/index.ts +4 -0
  35. package/src/core/index.ts +2 -0
  36. package/src/core/native-module.ts +22 -0
  37. package/src/core/sharing.test.ts +105 -0
  38. package/src/core/sharing.ts +46 -0
  39. package/src/core/types.ts +39 -0
  40. package/src/react/index.ts +6 -0
  41. package/src/vue/index.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @symbiote-native/sharing
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, and Angular, not just
6
+ 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).
11
+
12
+ ## Scope: outgoing share only
13
+
14
+ `expo-sharing` has two halves. This package ships one of them.
15
+
16
+ | Half | Upstream API | Here |
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.
30
+
31
+ Everything below describes the outgoing half.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install @symbiote-native/sharing
37
+ ```
38
+
39
+ `expo-sharing` and `expo-modules-core` come along as regular, pinned dependencies — never install
40
+ either yourself, and never add the `expo` meta-package to this project (it bundles its own
41
+ Metro/Babel pipeline that conflicts with this project's own).
42
+
43
+ ### Required one-time step: native autolinking wiring
44
+
45
+ Unlike a plain RN native module, `expo-sharing`'s native code is discovered by
46
+ `expo-modules-autolinking` — this needs wiring into the native host app **once**, covering this
47
+ package and every other `expo-modules-core` package with zero further changes:
48
+
49
+ | Platform | Touches |
50
+ |---|---|
51
+ | iOS | `ios/Podfile` — add `use_expo_modules!` |
52
+ | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
53
+ | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
54
+ | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a native-module name map |
55
+
56
+ Full mechanics live in the `symbiote-expo-native-module` skill. The per-package half of that
57
+ table — the Gradle dependency and the module map entry — is generated by
58
+ [`@symbiote-native/expo-modules-link`](../expo-modules-link) from this package's
59
+ `native-link.json` on every install.
60
+
61
+ There is no `Info.plist` usage description to add: opening the share sheet needs no iOS
62
+ permission. On Android, the `SharingFileProvider` and the `<queries>` block the chooser needs on
63
+ API 30+ ship inside `expo-sharing`'s own `AndroidManifest.xml` and merge into your app
64
+ automatically once the Gradle project is included — nothing to declare by hand.
65
+
66
+ ## Shape
67
+
68
+ ```
69
+ src/core/ the whole API: isAvailableAsync + shareAsync. native-module.ts
70
+ resolves ExpoSharing through expo-modules-core's requireNativeModule.
71
+ src/react/ @symbiote-native/sharing/react
72
+ src/vue/ @symbiote-native/sharing/vue
73
+ src/angular/ @symbiote-native/sharing/angular
74
+ ```
75
+
76
+ All three adapter entries are plain re-exports of `core/`. Both exports are stateless free
77
+ functions — no per-instance state, no event stream — so there is nothing for a hook, composable,
78
+ or service to wrap, the same reason [`@symbiote-native/secure-store`](../secure-store) re-exports
79
+ rather than wraps. (The incoming-share half above is exactly the part that *would* have needed
80
+ one; it is the reason this package has no `hooks/`, `composables/`, or `services/` folder.)
81
+ Import from `@symbiote-native/sharing` directly if you don't care which adapter you're on; the
82
+ per-adapter subpaths exist so every wrapper package has the same import surface.
83
+
84
+ ## Use it
85
+
86
+ ```ts
87
+ import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing';
88
+
89
+ if (await isAvailableAsync()) {
90
+ await shareAsync(localFileUri, {
91
+ mimeType: 'application/pdf',
92
+ dialogTitle: 'Send the report',
93
+ });
94
+ }
95
+ ```
96
+
97
+ On iPad, iOS presents the sheet as a popover and needs somewhere to point it:
98
+
99
+ ```ts
100
+ await shareAsync(localFileUri, {
101
+ anchor: { x: 40, y: 120, width: 1, height: 1 },
102
+ });
103
+ ```
104
+
105
+ ## API
106
+
107
+ | Export | Signature | Notes |
108
+ |---|---|---|
109
+ | `isAvailableAsync` | `() => Promise<boolean>` | `true` on Android and iOS. Reports on the native module, not on any device capability. |
110
+ | `shareAsync` | `(url, options?) => Promise<void>` | Opens the share sheet for a local file. Resolves when the sheet is dismissed. |
111
+
112
+ `ISharingOptions`: `mimeType` (Android), `UTI` (iOS), `dialogTitle`, `anchor` (iOS iPad).
113
+ `ISharingAnchor`: `x`, `y`, `width`, `height`, all optional, all in points.
114
+
115
+ ## Notes
116
+
117
+ - **`url` must be local.** A `file://` URI or a path from a file-system API. A remote `http(s)`
118
+ URL is not downloaded first — fetch it to a local file yourself, then share that.
119
+ - **A resolved promise is not a delivery receipt.** Neither platform reports which app the user
120
+ picked, or whether they picked one at all: the promise resolves when the sheet closes. iOS
121
+ resolves on every dismissal path, including "picked Print, then cancelled the print dialog".
122
+ - **`UTI` is accepted but unused.** Both native modules declare the field in their options record
123
+ and neither reads it in `expo-sharing@57.0.8`. It stays in the type for forward compatibility
124
+ with upstream, not because it changes behavior.
125
+ - **Android runs one share at a time.** A second `shareAsync` while a chooser is open rejects
126
+ rather than queueing.
127
+ - **The url is validated before the native call** — non-empty string — so a bad argument fails
128
+ with a readable message instead of as an argument-conversion failure on iOS or a generic
129
+ "Failed to share the file" on Android. Upstream has no such guard.
130
+
131
+ ## Test it
132
+
133
+ ```bash
134
+ pnpm vitest run packages/sharing
135
+ ```
136
+
137
+ The core tests fake the native module in place of `requireNativeModule`'s runtime resolution —
138
+ `ExpoSharing` only exists on a device, so a headless run would otherwise throw at import. The
139
+ share sheet itself can only be verified on a device or simulator.
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,4 @@
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
+ export * from '../core';
@@ -0,0 +1,2 @@
1
+ export { isAvailableAsync, shareAsync } from './sharing';
2
+ export type { ISharingAnchor, ISharingOptions } from './types';
@@ -0,0 +1 @@
1
+ export { isAvailableAsync, shareAsync } from './sharing';
@@ -0,0 +1,6 @@
1
+ import type { ISharingOptions } from './types';
2
+ export type INativeSharingModule = {
3
+ isAvailableAsync?(): Promise<boolean>;
4
+ shareAsync?(url: string, options: ISharingOptions): Promise<void>;
5
+ };
6
+ export declare const expoSharing: INativeSharingModule;
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_SHARING_MODULE_NAME = 'ExpoSharing';
3
+ export const expoSharing = requireNativeModule(EXPO_SHARING_MODULE_NAME);
@@ -0,0 +1,18 @@
1
+ import type { ISharingOptions } from './types';
2
+ /**
3
+ * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
4
+ *
5
+ * Both native modules leave the check to the JS side, so this reports on the presence of the
6
+ * native module rather than on any device capability.
7
+ */
8
+ export declare function isAvailableAsync(): Promise<boolean>;
9
+ /**
10
+ * Open the platform share sheet for a local file, letting the user hand it to any app that can
11
+ * accept it.
12
+ *
13
+ * `url` must point at a file the app can read — a `file://` URI, or a path from a file-system
14
+ * API. Remote `http(s)` URLs are not downloaded first; share their text via the target app
15
+ * instead. Resolves once the sheet is dismissed, whether or not the user picked anything: no
16
+ * platform reports which app received the file, so a resolved promise is not a delivery receipt.
17
+ */
18
+ export declare function shareAsync(url: string, options?: ISharingOptions): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSharing } from './native-module';
3
+ const NATIVE_MODULE_NAME = 'expo-sharing';
4
+ /**
5
+ * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
6
+ *
7
+ * Both native modules leave the check to the JS side, so this reports on the presence of the
8
+ * native module rather than on any device capability.
9
+ */
10
+ export async function isAvailableAsync() {
11
+ if (expoSharing.isAvailableAsync) {
12
+ return expoSharing.isAvailableAsync();
13
+ }
14
+ return !!expoSharing.shareAsync;
15
+ }
16
+ /**
17
+ * Open the platform share sheet for a local file, letting the user hand it to any app that can
18
+ * accept it.
19
+ *
20
+ * `url` must point at a file the app can read — a `file://` URI, or a path from a file-system
21
+ * API. Remote `http(s)` URLs are not downloaded first; share their text via the target app
22
+ * instead. Resolves once the sheet is dismissed, whether or not the user picked anything: no
23
+ * platform reports which app received the file, so a resolved promise is not a delivery receipt.
24
+ */
25
+ export async function shareAsync(url, options = {}) {
26
+ ensureShareableUrl(url);
27
+ if (!expoSharing.shareAsync) {
28
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'shareAsync');
29
+ }
30
+ await expoSharing.shareAsync(url, options);
31
+ }
32
+ // Not in upstream, which hands anything straight to the native module. An empty or non-string
33
+ // url surfaces there as an argument-conversion failure on iOS and a generic "Failed to share the
34
+ // file" on Android — both far enough from the call site to be worth failing early with the
35
+ // caller's own argument named.
36
+ function ensureShareableUrl(url) {
37
+ if (typeof url !== 'string' || url.length === 0) {
38
+ throw new Error('Invalid url provided to Sharing. Pass a non-empty local file URI or path.');
39
+ }
40
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where the iPad popover points. Every field is in points, relative to the presenting view;
3
+ * omitted fields fall back to the bottom-center of that view.
4
+ * @platform ios
5
+ */
6
+ export type ISharingAnchor = {
7
+ x?: number;
8
+ y?: number;
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ export type ISharingOptions = {
13
+ /**
14
+ * MIME type of the shared file, used to pick which apps the chooser offers. Guessed from the
15
+ * file name when omitted, falling back to a wildcard type that offers every app.
16
+ * @platform android
17
+ */
18
+ mimeType?: string;
19
+ /**
20
+ * [Uniform Type Identifier](https://developer.apple.com/documentation/uniformtypeidentifiers)
21
+ * of the shared file.
22
+ *
23
+ * Both native modules accept the field, but neither reads it in `expo-sharing@57.0.8` — it is
24
+ * carried through for forward compatibility with upstream, not because it changes behavior.
25
+ * @platform ios
26
+ */
27
+ UTI?: string;
28
+ /**
29
+ * Title of the share dialog. Android renders it as the chooser's header; iOS assigns it to the
30
+ * activity controller, where most share sheets ignore it.
31
+ */
32
+ dialogTitle?: string;
33
+ /**
34
+ * Anchor rectangle for the popover iOS requires on iPad. Ignored on iPhone and on Android.
35
+ * @platform ios
36
+ */
37
+ anchor?: ISharingAnchor;
38
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,6 @@
1
+ // @symbiote-native/sharing/react: the React entry over the framework-agnostic core.
2
+ // Both exports are stateless free functions — no per-instance state, no event stream, nothing a
3
+ // hook could own or clean up — so this is a plain re-export, the same shape
4
+ // packages/secure-store's React entry has for the same reason. The one part of expo-sharing that
5
+ // WOULD need a hook (incoming share) is out of scope here; see the README.
6
+ export * from '../core';
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,4 @@
1
+ // @symbiote-native/sharing/vue: the Vue entry over the framework-agnostic core. Same reasoning
2
+ // as the React entry — no per-instance state or event stream to wire onto Vue's reactivity, so
3
+ // this is a plain re-export.
4
+ export * from '../core';
@@ -0,0 +1 @@
1
+ export * from '../core';
@@ -0,0 +1,5 @@
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
+ export * from '../core';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,2 @@
1
+ export { isAvailableAsync, shareAsync } from './sharing';
2
+ export type { ISharingAnchor, ISharingOptions } from './types';
@@ -0,0 +1,2 @@
1
+ export { isAvailableAsync, shareAsync } from './sharing';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { ISharingOptions } from './types';
2
+ export type INativeSharingModule = {
3
+ isAvailableAsync?(): Promise<boolean>;
4
+ shareAsync?(url: string, options: ISharingOptions): Promise<void>;
5
+ };
6
+ export declare const expoSharing: INativeSharingModule;
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_SHARING_MODULE_NAME = 'ExpoSharing';
3
+ export const expoSharing = requireNativeModule(EXPO_SHARING_MODULE_NAME);
4
+ //# sourceMappingURL=native-module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGxD,MAAM,wBAAwB,GAAG,aAAa,CAAC;AAkB/C,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAuB,wBAAwB,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ISharingOptions } from './types';
2
+ /**
3
+ * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
4
+ *
5
+ * Both native modules leave the check to the JS side, so this reports on the presence of the
6
+ * native module rather than on any device capability.
7
+ */
8
+ export declare function isAvailableAsync(): Promise<boolean>;
9
+ /**
10
+ * Open the platform share sheet for a local file, letting the user hand it to any app that can
11
+ * accept it.
12
+ *
13
+ * `url` must point at a file the app can read — a `file://` URI, or a path from a file-system
14
+ * API. Remote `http(s)` URLs are not downloaded first; share their text via the target app
15
+ * instead. Resolves once the sheet is dismissed, whether or not the user picked anything: no
16
+ * platform reports which app received the file, so a resolved promise is not a delivery receipt.
17
+ */
18
+ export declare function shareAsync(url: string, options?: ISharingOptions): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+ import { expoSharing } from './native-module';
3
+ const NATIVE_MODULE_NAME = 'expo-sharing';
4
+ /**
5
+ * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
6
+ *
7
+ * Both native modules leave the check to the JS side, so this reports on the presence of the
8
+ * native module rather than on any device capability.
9
+ */
10
+ export async function isAvailableAsync() {
11
+ if (expoSharing.isAvailableAsync) {
12
+ return expoSharing.isAvailableAsync();
13
+ }
14
+ return !!expoSharing.shareAsync;
15
+ }
16
+ /**
17
+ * Open the platform share sheet for a local file, letting the user hand it to any app that can
18
+ * accept it.
19
+ *
20
+ * `url` must point at a file the app can read — a `file://` URI, or a path from a file-system
21
+ * API. Remote `http(s)` URLs are not downloaded first; share their text via the target app
22
+ * instead. Resolves once the sheet is dismissed, whether or not the user picked anything: no
23
+ * platform reports which app received the file, so a resolved promise is not a delivery receipt.
24
+ */
25
+ export async function shareAsync(url, options = {}) {
26
+ ensureShareableUrl(url);
27
+ if (!expoSharing.shareAsync) {
28
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'shareAsync');
29
+ }
30
+ await expoSharing.shareAsync(url, options);
31
+ }
32
+ // Not in upstream, which hands anything straight to the native module. An empty or non-string
33
+ // url surfaces there as an argument-conversion failure on iOS and a generic "Failed to share the
34
+ // file" on Android — both far enough from the call site to be worth failing early with the
35
+ // caller's own argument named.
36
+ function ensureShareableUrl(url) {
37
+ if (typeof url !== 'string' || url.length === 0) {
38
+ throw new Error('Invalid url provided to Sharing. Pass a non-empty local file URI or path.');
39
+ }
40
+ }
41
+ //# sourceMappingURL=sharing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharing.js","sourceRoot":"","sources":["../../src/core/sharing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,WAAW,CAAC,gBAAgB,EAAE,CAAC;QACjC,OAAO,WAAW,CAAC,gBAAgB,EAAE,CAAC;IACxC,CAAC;IACD,OAAO,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;AAClC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,UAA2B,EAAE;IACzE,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACxB,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,WAAW,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,8FAA8F;AAC9F,iGAAiG;AACjG,2FAA2F;AAC3F,+BAA+B;AAC/B,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where the iPad popover points. Every field is in points, relative to the presenting view;
3
+ * omitted fields fall back to the bottom-center of that view.
4
+ * @platform ios
5
+ */
6
+ export type ISharingAnchor = {
7
+ x?: number;
8
+ y?: number;
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+ export type ISharingOptions = {
13
+ /**
14
+ * MIME type of the shared file, used to pick which apps the chooser offers. Guessed from the
15
+ * file name when omitted, falling back to a wildcard type that offers every app.
16
+ * @platform android
17
+ */
18
+ mimeType?: string;
19
+ /**
20
+ * [Uniform Type Identifier](https://developer.apple.com/documentation/uniformtypeidentifiers)
21
+ * of the shared file.
22
+ *
23
+ * Both native modules accept the field, but neither reads it in `expo-sharing@57.0.8` — it is
24
+ * carried through for forward compatibility with upstream, not because it changes behavior.
25
+ * @platform ios
26
+ */
27
+ UTI?: string;
28
+ /**
29
+ * Title of the share dialog. Android renders it as the chooser's header; iOS assigns it to the
30
+ * activity controller, where most share sheets ignore it.
31
+ */
32
+ dialogTitle?: string;
33
+ /**
34
+ * Anchor rectangle for the popover iOS requires on iPad. Ignored on iPhone and on Android.
35
+ * @platform ios
36
+ */
37
+ anchor?: ISharingAnchor;
38
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-sharing",
4
+ "modules": [
5
+ {
6
+ "importPath": "expo.modules.sharing.SharingModule",
7
+ "className": "SharingModule",
8
+ "nativeName": "ExpoSharing"
9
+ }
10
+ ]
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@symbiote-native/sharing",
3
+ "version": "0.0.1",
4
+ "description": "expo-sharing wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Opens the platform share sheet for a local file.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/sharing"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/sharing#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "type": "module",
17
+ "main": "./build/core/index.js",
18
+ "module": "./build/core/index.js",
19
+ "types": "./build/core/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./build/core/index.d.ts",
23
+ "default": "./build/core/index.js"
24
+ },
25
+ "./vue": {
26
+ "types": "./build/vue/index.d.ts",
27
+ "default": "./build/vue/index.js"
28
+ },
29
+ "./react": {
30
+ "types": "./build/react/index.d.ts",
31
+ "default": "./build/react/index.js"
32
+ },
33
+ "./angular": {
34
+ "types": "./build-ngc/angular/index.d.ts",
35
+ "react-native": "./build-ngc/angular/index.js",
36
+ "default": "./src/angular/index.ts"
37
+ }
38
+ },
39
+ "files": [
40
+ "src",
41
+ "build",
42
+ "build-ngc",
43
+ "native-link.json"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "expo-sharing": "57.0.8",
50
+ "expo-modules-core": "57.0.5"
51
+ },
52
+ "peerDependencies": {
53
+ "@symbiote-native/engine": ">=0.1.7",
54
+ "@angular/core": ">=20",
55
+ "@vue/runtime-core": "^3.5.13",
56
+ "react": ">=19.0.0",
57
+ "react-native": ">=0.86",
58
+ "vue": ">=3.5.0",
59
+ "@symbiote-native/angular": "0.6.1",
60
+ "@symbiote-native/react": "0.2.8",
61
+ "@symbiote-native/vue": "0.3.8"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@symbiote-native/angular": {
65
+ "optional": true
66
+ },
67
+ "@symbiote-native/react": {
68
+ "optional": true
69
+ },
70
+ "@symbiote-native/vue": {
71
+ "optional": true
72
+ },
73
+ "@angular/core": {
74
+ "optional": true
75
+ },
76
+ "@vue/runtime-core": {
77
+ "optional": true
78
+ },
79
+ "react": {
80
+ "optional": true
81
+ },
82
+ "vue": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "devDependencies": {
87
+ "@angular/compiler": "^22",
88
+ "@angular/compiler-cli": "^22",
89
+ "@angular/core": "^22",
90
+ "@types/node": "^26.0.0",
91
+ "@types/react": "^19.2.0",
92
+ "@vue/runtime-core": "^3.5.13",
93
+ "react": "19.2.3",
94
+ "typescript": "~6.0.0",
95
+ "@symbiote-native/test-utils": "0.1.6",
96
+ "@symbiote-native/react": "0.2.8",
97
+ "@symbiote-native/engine": "0.1.7",
98
+ "@symbiote-native/vue": "0.3.8",
99
+ "@symbiote-native/angular": "0.6.1"
100
+ },
101
+ "scripts": {
102
+ "typecheck": "tsc --build",
103
+ "clean": "rm -rf build-ngc",
104
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
105
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
106
+ }
107
+ }
@@ -0,0 +1,4 @@
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
+ export * from '../core';
@@ -0,0 +1,2 @@
1
+ export { isAvailableAsync, shareAsync } from './sharing';
2
+ export type { ISharingAnchor, ISharingOptions } from './types';
@@ -0,0 +1,22 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { ISharingOptions } from './types';
3
+
4
+ const EXPO_SHARING_MODULE_NAME = 'ExpoSharing';
5
+
6
+ // Every member is optional — each call site checks for its presence before calling through and
7
+ // throws an UnavailabilityError itself, matching upstream's own per-platform capability checks
8
+ // rather than assuming the native module implements the whole surface.
9
+ //
10
+ // `isAvailableAsync` is genuinely absent from both native modules in expo-sharing@57.0.8: only
11
+ // the web module (which this package does not ship) defines it. Presence of the module itself is
12
+ // therefore what availability is read from — see isAvailableAsync in ./sharing.
13
+ //
14
+ // The incoming-share methods (getSharedPayloads, getResolvedSharedPayloadsAsync,
15
+ // clearSharedPayloads) are deliberately absent from this type: reaching them needs an iOS Share
16
+ // Extension target this package does not ship. See the README.
17
+ export type INativeSharingModule = {
18
+ isAvailableAsync?(): Promise<boolean>;
19
+ shareAsync?(url: string, options: ISharingOptions): Promise<void>;
20
+ };
21
+
22
+ export const expoSharing = requireNativeModule<INativeSharingModule>(EXPO_SHARING_MODULE_NAME);
@@ -0,0 +1,105 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const FAKE_NATIVE_SHARING = {
4
+ shareAsync: vi.fn(async () => undefined),
5
+ };
6
+
7
+ // The real ExpoSharing native module only exists on device — resolving it via
8
+ // requireNativeModule() at import time would throw in this headless run, so the module-lookup
9
+ // file is faked in place of expo-modules-core's runtime resolution, the same pattern
10
+ // packages/secure-store/src/core/secure-store.test.ts uses.
11
+ vi.mock('./native-module', () => ({
12
+ expoSharing: FAKE_NATIVE_SHARING,
13
+ }));
14
+
15
+ // expo-modules-core's real entry transitively imports 'react-native', whose Flow-typed source
16
+ // Vitest's Oxc transform can't parse — so only the members used as values are faked.
17
+ vi.mock('expo-modules-core', () => ({
18
+ UnavailabilityError: class UnavailabilityError extends Error {
19
+ constructor(moduleName: string, propertyName: string) {
20
+ super(`${propertyName} is not available on ${moduleName}`);
21
+ }
22
+ },
23
+ }));
24
+
25
+ const { isAvailableAsync, shareAsync } = await import('./sharing');
26
+
27
+ const LOCAL_FILE_URL = 'file:///tmp/report.pdf';
28
+
29
+ afterEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ describe('isAvailableAsync', () => {
34
+ it('reports availability from the presence of the native share method', async () => {
35
+ await expect(isAvailableAsync()).resolves.toBe(true);
36
+ });
37
+
38
+ it('reports unavailable when the native module cannot share', async () => {
39
+ const { shareAsync: native } = FAKE_NATIVE_SHARING;
40
+ // @ts-expect-error -- simulating a platform where the native module has no such method
41
+ FAKE_NATIVE_SHARING.shareAsync = undefined;
42
+
43
+ await expect(isAvailableAsync()).resolves.toBe(false);
44
+
45
+ FAKE_NATIVE_SHARING.shareAsync = native;
46
+ });
47
+
48
+ it('defers to the native check when the module implements one', async () => {
49
+ const nativeCheck = vi.fn(async () => false);
50
+ Object.assign(FAKE_NATIVE_SHARING, { isAvailableAsync: nativeCheck });
51
+
52
+ await expect(isAvailableAsync()).resolves.toBe(false);
53
+ expect(nativeCheck).toHaveBeenCalled();
54
+
55
+ Reflect.deleteProperty(FAKE_NATIVE_SHARING, 'isAvailableAsync');
56
+ });
57
+ });
58
+
59
+ describe('shareAsync', () => {
60
+ it('passes the url and options straight through', async () => {
61
+ await shareAsync(LOCAL_FILE_URL, { mimeType: 'application/pdf', dialogTitle: 'Send report' });
62
+ expect(FAKE_NATIVE_SHARING.shareAsync).toHaveBeenCalledWith(LOCAL_FILE_URL, {
63
+ mimeType: 'application/pdf',
64
+ dialogTitle: 'Send report',
65
+ });
66
+ });
67
+
68
+ it('defaults the options to an empty object', async () => {
69
+ await shareAsync(LOCAL_FILE_URL);
70
+ expect(FAKE_NATIVE_SHARING.shareAsync).toHaveBeenCalledWith(LOCAL_FILE_URL, {});
71
+ });
72
+
73
+ it('forwards the iPad anchor rectangle unflattened', async () => {
74
+ const anchor = { x: 10, y: 20, width: 1, height: 1 };
75
+ await shareAsync(LOCAL_FILE_URL, { anchor });
76
+ expect(FAKE_NATIVE_SHARING.shareAsync).toHaveBeenCalledWith(LOCAL_FILE_URL, { anchor });
77
+ });
78
+
79
+ it('throws an UnavailabilityError-shaped error when the native method is absent', async () => {
80
+ const { shareAsync: native } = FAKE_NATIVE_SHARING;
81
+ // @ts-expect-error -- simulating a platform where the native module has no such method
82
+ FAKE_NATIVE_SHARING.shareAsync = undefined;
83
+
84
+ await expect(shareAsync(LOCAL_FILE_URL)).rejects.toThrow(
85
+ 'shareAsync is not available on expo-sharing',
86
+ );
87
+
88
+ FAKE_NATIVE_SHARING.shareAsync = native;
89
+ });
90
+ });
91
+
92
+ // Validation happens before the native call so a bad url fails with a readable message here
93
+ // rather than as an argument-conversion failure on iOS or a generic share failure on Android.
94
+ describe('url validation', () => {
95
+ it('rejects an empty url', async () => {
96
+ await expect(shareAsync('')).rejects.toThrow(/Invalid url provided to Sharing/);
97
+ expect(FAKE_NATIVE_SHARING.shareAsync).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('rejects a non-string url before reaching the native module', async () => {
101
+ // @ts-expect-error -- the guard exists precisely for callers without type checking
102
+ await expect(shareAsync(undefined)).rejects.toThrow(/Invalid url provided to Sharing/);
103
+ expect(FAKE_NATIVE_SHARING.shareAsync).not.toHaveBeenCalled();
104
+ });
105
+ });
@@ -0,0 +1,46 @@
1
+ import { UnavailabilityError } from 'expo-modules-core';
2
+
3
+ import { expoSharing } from './native-module';
4
+ import type { ISharingOptions } from './types';
5
+
6
+ const NATIVE_MODULE_NAME = 'expo-sharing';
7
+
8
+ /**
9
+ * Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS.
10
+ *
11
+ * Both native modules leave the check to the JS side, so this reports on the presence of the
12
+ * native module rather than on any device capability.
13
+ */
14
+ export async function isAvailableAsync(): Promise<boolean> {
15
+ if (expoSharing.isAvailableAsync) {
16
+ return expoSharing.isAvailableAsync();
17
+ }
18
+ return !!expoSharing.shareAsync;
19
+ }
20
+
21
+ /**
22
+ * Open the platform share sheet for a local file, letting the user hand it to any app that can
23
+ * accept it.
24
+ *
25
+ * `url` must point at a file the app can read — a `file://` URI, or a path from a file-system
26
+ * API. Remote `http(s)` URLs are not downloaded first; share their text via the target app
27
+ * instead. Resolves once the sheet is dismissed, whether or not the user picked anything: no
28
+ * platform reports which app received the file, so a resolved promise is not a delivery receipt.
29
+ */
30
+ export async function shareAsync(url: string, options: ISharingOptions = {}): Promise<void> {
31
+ ensureShareableUrl(url);
32
+ if (!expoSharing.shareAsync) {
33
+ throw new UnavailabilityError(NATIVE_MODULE_NAME, 'shareAsync');
34
+ }
35
+ await expoSharing.shareAsync(url, options);
36
+ }
37
+
38
+ // Not in upstream, which hands anything straight to the native module. An empty or non-string
39
+ // url surfaces there as an argument-conversion failure on iOS and a generic "Failed to share the
40
+ // file" on Android — both far enough from the call site to be worth failing early with the
41
+ // caller's own argument named.
42
+ function ensureShareableUrl(url: string): void {
43
+ if (typeof url !== 'string' || url.length === 0) {
44
+ throw new Error('Invalid url provided to Sharing. Pass a non-empty local file URI or path.');
45
+ }
46
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Where the iPad popover points. Every field is in points, relative to the presenting view;
3
+ * omitted fields fall back to the bottom-center of that view.
4
+ * @platform ios
5
+ */
6
+ export type ISharingAnchor = {
7
+ x?: number;
8
+ y?: number;
9
+ width?: number;
10
+ height?: number;
11
+ };
12
+
13
+ export type ISharingOptions = {
14
+ /**
15
+ * MIME type of the shared file, used to pick which apps the chooser offers. Guessed from the
16
+ * file name when omitted, falling back to a wildcard type that offers every app.
17
+ * @platform android
18
+ */
19
+ mimeType?: string;
20
+ /**
21
+ * [Uniform Type Identifier](https://developer.apple.com/documentation/uniformtypeidentifiers)
22
+ * of the shared file.
23
+ *
24
+ * Both native modules accept the field, but neither reads it in `expo-sharing@57.0.8` — it is
25
+ * carried through for forward compatibility with upstream, not because it changes behavior.
26
+ * @platform ios
27
+ */
28
+ UTI?: string;
29
+ /**
30
+ * Title of the share dialog. Android renders it as the chooser's header; iOS assigns it to the
31
+ * activity controller, where most share sheets ignore it.
32
+ */
33
+ dialogTitle?: string;
34
+ /**
35
+ * Anchor rectangle for the popover iOS requires on iPad. Ignored on iPhone and on Android.
36
+ * @platform ios
37
+ */
38
+ anchor?: ISharingAnchor;
39
+ };
@@ -0,0 +1,6 @@
1
+ // @symbiote-native/sharing/react: the React entry over the framework-agnostic core.
2
+ // Both exports are stateless free functions — no per-instance state, no event stream, nothing a
3
+ // hook could own or clean up — so this is a plain re-export, the same shape
4
+ // packages/secure-store's React entry has for the same reason. The one part of expo-sharing that
5
+ // WOULD need a hook (incoming share) is out of scope here; see the README.
6
+ export * from '../core';
@@ -0,0 +1,4 @@
1
+ // @symbiote-native/sharing/vue: the Vue entry over the framework-agnostic core. Same reasoning
2
+ // as the React entry — no per-instance state or event stream to wire onto Vue's reactivity, so
3
+ // this is a plain re-export.
4
+ export * from '../core';