# Sharing

> Send local files to other apps and receive files shared into yours, on every SymbioteNative adapter.

Let users send a file from your app to any other app, and receive files other apps share into
yours. `@symbiote-native/sharing` wraps
[`expo-sharing`](https://github.com/expo/expo/tree/main/packages/expo-sharing) so every
SymbioteNative adapter can reach it, not just React. Outgoing share is two free functions in a
shared `core`; the incoming half ships as `useIncomingShare` on React, Vue, Svelte and Solid, and
`injectIncomingShare` on Angular.

| OS platform | Support |
| ----------- | ------- |
| iOS         | ✅ live |
| Android     | ✅ live |

| Framework adapter | Support |
| ----------------- | ------- |
| React             | ✅ live |
| Vue               | ✅ live |
| Angular           | ✅ live |
| Svelte            | ✅ live |
| Solid             | ✅ live |

## Scope: outgoing and incoming share

`expo-sharing` has two halves, and both are ported to every adapter.

| Half                                                          | Upstream API                                                                                     | Here           |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------- |
| **Outgoing** - hand a local file to another app               | `shareAsync`, `isAvailableAsync`                                                                 | ported in full |
| **Incoming** - receive files other apps share _into_ your app | `useIncomingShare`, `getSharedPayloads`, `getResolvedSharedPayloadsAsync`, `clearSharedPayloads` | ported in full |

<Aside
  type="caution"
  title="Incoming share needs a share target in the host app"
>
  The incoming functions return data only once the app carries a share target: an
  iOS Share Extension (a second Xcode target with its own `Info.plist`,
  entitlements and an App Group shared with the app) plus Android intent filters
  on the main activity. Upstream's config plugin generates it. It is native
  scaffolding this package does not generate or touch, so without it the
  incoming functions return empty arrays.
</Aside>

### Receiving shared data

```ts
import { useIncomingShare } from '@symbiote-native/sharing/react';

const { sharedPayloads, resolvedSharedPayloads, isResolving, error, clearSharedPayloads } =
  useIncomingShare();
```

The payloads are read synchronously, resolved after mount, and re-read whenever the app returns
to the foreground. `refreshSharePayloads()` forces a re-read. Resolving a shared URL may need the
network. Vue returns a `ComputedRef`, Solid an `Accessor`, Svelte a `{ current }` box, and
Angular's `injectIncomingShare()` a `Signal`.

## Installation

```sh
npm install @symbiote-native/sharing
```

Scaffolding or extending a SymbioteNative app? `npx @symbiote-native/cli new --sharing` (or
`add --sharing` in an existing app) installs and wires this for you — see
[`@symbiote-native/cli`](https://github.com/OneEyed1366/symbiote-native/tree/master/packages/cli).

`expo-sharing` and `expo-modules-core` come along as regular dependencies, pinned to exact
versions — never install either yourself, and never add the `expo` meta-package to your project
(it bundles its own Metro/Babel pipeline, which conflicts with this project's own).

<Aside type="danger" title="Native setup is required before first use">
  `expo-sharing`'s native code is discovered by `expo-modules-autolinking`, a
  different mechanism from the `react-native.config.cjs`/podspec autolinking
  every other SymbioteNative wrapper uses — and it isn't the standard Expo setup
  flow either, since this project never installs the `expo` meta-package. Follow
  [How to: wire up an Expo native
  module](/docs/howtos/expo-native-module-setup/) once per app first; it covers
  this package and every other `expo-modules-core` package with zero further
  native changes.
</Aside>

The package's `native-link.json` asks `symbiote-expo-link` for the Android Gradle dependency and
module-map entry on every install. Nothing else is needed: opening the share sheet requires no iOS
permission, so there is no usage-description string, and the `SharingFileProvider` plus the
`<queries>` block Android needs on API 30+ ship inside `expo-sharing`'s own manifest and merge
into your app automatically.

## Usage

All five adapters (React, Vue, Angular, Svelte, Solid) re-export the exact same functions; there's
no per-adapter hook/composable/service to reach for, since nothing here holds live state or a
subscription.

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    import { useState } from 'react';
    import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/react';

    export default function ShareReport({ fileUri }: { fileUri: string }) {
      const [error, setError] = useState<string | null>(null);

      async function onShare() {
        if (!(await isAvailableAsync())) {
          setError('Sharing is not available on this device');
          return;
        }
        await shareAsync(fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' });
      }

      return (
        <view>
          <button title="Share the report" onPress={onShare} />
          {error === null ? null : <text>{error}</text>}
        </view>
      );
    }
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <script setup lang="ts">
    import { ref } from 'vue';
    import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/vue';

    const props = defineProps<{ fileUri: string }>();
    const error = ref<string | null>(null);

    async function onShare() {
      if (!(await isAvailableAsync())) {
        error.value = 'Sharing is not available on this device';
        return;
      }
      await shareAsync(props.fileUri, {
        mimeType: 'application/pdf',
        dialogTitle: 'Send the report',
      });
    }
    </script>

    <template>
      <view>
        <button title="Share the report" @press="onShare" />
        <text v-if="error">{{ error }}</text>
      </view>
    </template>
    ```

  </TabItem>
  <TabItem label="Angular">
    ```ts
    import { Component, input, signal } from '@angular/core';
    import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
    import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/angular';

    @Component({
      standalone: true,
      imports: [SYMBIOTE_ELEMENTS],
      template: `
        <view>
          <button title="Share the report" (press)="onShare()" />
          @if (error(); as message) {
            <text>{{ message }}</text>
          }
        </view>
      `,
    })
    export class ShareReport {
      readonly fileUri = input.required<string>();
      readonly error = signal<string | null>(null);

      async onShare(): Promise<void> {
        if (!(await isAvailableAsync())) {
          this.error.set('Sharing is not available on this device');
          return;
        }
        await shareAsync(this.fileUri(), {
          mimeType: 'application/pdf',
          dialogTitle: 'Send the report',
        });
      }
    }
    ```

    There's no per-instance service to `inject()` here — both functions are plain exports off the
    core package, called straight from a template event binding.

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <script lang="ts">
      import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/svelte';

      let { fileUri }: { fileUri: string } = $props();
      let error = $state<string | null>(null);

      async function onShare(): Promise<void> {
        if (!(await isAvailableAsync())) {
          error = 'Sharing is not available on this device';
          return;
        }
        await shareAsync(fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' });
      }
    </script>

    <view>
      <button title="Share the report" onPress={onShare} />
      {#if error}
        <text>{error}</text>
      {/if}
    </view>
    ```

    There's no per-instance rune to reach for here either — both functions are plain exports off
    the core package, called straight from `onPress`.

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    import { createSignal } from 'solid-js';
    import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/solid';

    export function ShareReport(props: { fileUri: string }) {
      const [error, setError] = createSignal<string | null>(null);

      async function onShare() {
        if (!(await isAvailableAsync())) {
          setError('Sharing is not available on this device');
          return;
        }
        await shareAsync(props.fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' });
      }

      return (
        <view>
          <button title="Share the report" onPress={onShare} />
          {error() === null ? null : <text>{error()}</text>}
        </view>
      );
    }
    ```

    There's no per-instance primitive here either: both functions are plain exports off the core
    package, called straight from `onPress`. `props.fileUri` stays inline rather than destructured,
    since a component body runs only once and a destructured value would freeze at mount.

  </TabItem>
</Tabs>

### On iPad

iOS presents the share sheet as a popover on iPad and needs somewhere to point it. Pass the
rectangle of whatever the user tapped; without one the popover anchors to the bottom-center of the
presenting view.

```ts
await shareAsync(fileUri, { anchor: { x: 40, y: 120, width: 1, height: 1 } });
```

## API

### Functions

| Signature                                  | Description                                                                                                                                                              |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isAvailableAsync(): Promise<boolean>`     | Whether the share sheet can be opened on this device. Resolves `true` on Android and iOS — it reports on the presence of the native module, not on any device capability |
| `shareAsync(url, options?): Promise<void>` | Opens the platform share sheet for the local file at `url`. Resolves once the sheet is dismissed, whether or not the user picked anything                                |

`url` must be a non-empty string pointing at a file the app can read — a `file://` URI, or a path
from a file-system API. Anything else throws before the native call is made.

### `ISharingOptions`

| Field         | Type                          | Description                                                                                                                                                         |
| ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mimeType`    | `string \| undefined`         | MIME type of the file, deciding which apps the chooser offers. Guessed from the file name when omitted. Android only                                                |
| `UTI`         | `string \| undefined`         | Uniform Type Identifier of the file. Accepted by the native options record but unread in `expo-sharing@57.0.8`; carried through for forward compatibility. iOS only |
| `dialogTitle` | `string \| undefined`         | Title of the share dialog. Android renders it as the chooser header; iOS assigns it to the activity controller, where most share sheets ignore it                   |
| `anchor`      | `ISharingAnchor \| undefined` | Rectangle the iPad popover points at. Ignored on iPhone and on Android. iOS only                                                                                    |

### `ISharingAnchor`

| Field    | Type                  | Description                                                                                             |
| -------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
| `x`      | `number \| undefined` | Horizontal offset in points, relative to the presenting view. Defaults to that view's horizontal center |
| `y`      | `number \| undefined` | Vertical offset in points, relative to the presenting view. Defaults to that view's bottom edge         |
| `width`  | `number \| undefined` | Width of the anchor rectangle in points. Defaults to `0`                                                |
| `height` | `number \| undefined` | Height of the anchor rectangle in points. Defaults to `0`                                               |

## Notes

- **`url` has to be local.** A remote `http(s)` URL is not downloaded first — fetch it to a local
  file yourself, then share that file.
- **A resolved promise is not a delivery receipt.** Neither platform reports which app the user
  picked, or whether they picked one at all: the promise resolves when the sheet closes. iOS
  resolves on every dismissal path, including "picked Print, then cancelled the print dialog".
- **Android runs one share at a time.** Calling `shareAsync` again while a chooser is still open
  rejects rather than queueing.
- **The iOS share extension is experimental upstream.** It opens the main app target instead of
  handling the share in its own view controller, which Apple does not officially support and may
  stop working in a future iOS release.
- **The share sheet can only be verified on a device or simulator.** The headless test suite fakes
  the native module, so it proves argument marshalling and the error paths, not the sheet itself.

## Common questions

**Android says "Not allowed to read file under given URL" or "Failed to find configured root".**
Share a file that lives in the app's own document or cache directory, for example one written with
the [file system](/docs/packages/file-system/) package. A path owned by another app, such as a
media library asset, can be rejected. Copy it into `Paths.cache` first, then share the copy.

**It shares fine on iOS but the receiving app cannot open the file on Android.** The same cause:
Android hands the other app a content URI built from the file, and only files in the app's
directories are covered. Check `url` points inside them.

**Can I share a web URL or plain text?** Not with this function. `shareAsync` takes a local file.
To share a link, send it through the system's text share (React Native's `Share` module), or
download the content to a file first.

**How do I know the user actually shared it?** You cannot. The promise resolves when the sheet
closes, whichever app was picked, or none.

Sources: [Expo docs: Sharing](https://docs.expo.dev/versions/latest/sdk/sharing/),
[expo/expo#5933 can't share file on Android](https://github.com/expo/expo/issues/5933),
[expo/expo#18887 Not allowed to read file under given URL](https://github.com/expo/expo/issues/18887),
[expo/expo#9222 sharing photos from MediaLibrary throws on Android](https://github.com/expo/expo/issues/9222).

## How the wrapper works

`@symbiote-native/sharing` ships zero React/Vue/Angular/Svelte/Solid logic — `expo-sharing`'s own
JS is hand-ported into this package's `core/`, resolving the native module through
`expo-modules-core`'s `requireNativeModule` rather than the `expo` meta-package this project never
installs:

```
packages/sharing/src/
core/      framework-agnostic: shareAsync, isAvailableAsync, the payload functions and the
           incoming-share store. native-module.ts resolves ExpoSharing via requireNativeModule
react/     @symbiote-native/sharing/react    (core + useIncomingShare)
vue/       @symbiote-native/sharing/vue      (core + useIncomingShare)
svelte/    @symbiote-native/sharing/svelte   (core + useIncomingShare)
angular/   @symbiote-native/sharing/angular  (core + injectIncomingShare)
solid/     @symbiote-native/sharing/solid    (core + useIncomingShare)
```

The store owns the refresh logic (payload comparison, the foreground resync, error capture) once,
and each adapter binds it with its own event-value primitive, so the lifecycle code is a few lines
per framework. The native code itself is never
vendored or copied — `expo-modules-autolinking` resolves it straight out of `node_modules` (see
[the native setup guide](/docs/howtos/expo-native-module-setup/)).
