Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Deploy documentation

on:
pull_request:
branches:
- main
paths:
- "docs/**"
- ".github/workflows/deploy-docs.yml"
push:
branches:
- main
paths:
- "docs/**"
- ".github/workflows/deploy-docs.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: github-pages-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build documentation
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.23.2
cache: npm
cache-dependency-path: docs/package-lock.json
package-manager-cache: false

- name: Install documentation dependencies
working-directory: docs
run: npm ci

- name: Type-check documentation
working-directory: docs
run: npm run typecheck

- name: Build documentation
working-directory: docs
run: npm run build

- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: docs/build

deploy:
name: Deploy documentation
if: github.event_name != 'pull_request'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
permissions:
pages: write
id-token: write
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
4 changes: 4 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
build/
.docusaurus/
.cache-loader/
34 changes: 34 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# expo-share-content documentation

This directory contains the Docusaurus website for `expo-share-content`.

## Local development

```bash
cd docs
npm install
npm start
```

The development server opens at `http://localhost:3000/React-Native-Share-Content/`.

## Production build

```bash
npm run typecheck
npm run build
npm run serve
```

The static output is written to `docs/build/` and is configured for GitHub Pages at
`https://ngocdevv.github.io/React-Native-Share-Content/`.

## Deployment

`.github/workflows/deploy-docs.yml` validates pull requests, then builds and deploys the production
artifact when a documentation change reaches `main`. In the repository settings, configure
**Pages → Build and deployment → Source** to **GitHub Actions** before the first deployment.

Documentation source lives in `docs/docs/`. The custom landing page is implemented in
`src/pages/index.tsx`; global design tokens and Docusaurus overrides live in
`src/css/custom.css`.
119 changes: 119 additions & 0 deletions docs/docs/api-reference.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
sidebar_position: 5
title: API reference
description: Public methods, listeners, helpers, and TypeScript payload types.
---

# API reference

All share methods are available on the default export and as named exports. `dedupeShares` and `createShareContentApi` are named exports only.

```ts
import ExpoShareContent, {
getPendingSharesAsync,
type SharePayload,
} from 'expo-share-content';
```

## `getPendingSharesAsync`

```ts
getPendingSharesAsync(): Promise<SharePayload[]>
```

Reads queued payloads without removing them. Results are oldest first, and duplicate IDs inside the native response are removed.

## `getInitialShareAsync`

```ts
getInitialShareAsync(): Promise<SharePayload | null>
```

Returns the oldest pending payload without removing it.

## `clearPendingSharesAsync`

```ts
clearPendingSharesAsync(shareIds?: readonly string[]): Promise<void>
```

Pass IDs to acknowledge selected records. Omit the argument to clear the entire pending queue. Acknowledgement removes queue records but deliberately does not delete attachment files.

## `releaseSharedFilesAsync`

```ts
releaseSharedFilesAsync(shareIds: readonly string[]): Promise<void>
```

Deletes module-managed attachment directories for already acknowledged receipts. The method rejects IDs that remain pending.

## `addShareListener`

```ts
addShareListener(
listener: (payload: SharePayload) => void
): ShareSubscription
```

Subscribes to warm-start payloads. Remove the subscription when the owning component unmounts.

## `addShareErrorListener`

```ts
addShareErrorListener(
listener: (error: ShareErrorEvent) => void
): ShareSubscription
```

Subscribes to native parsing, file-copy, queue, and App Group errors.

## `dedupeShares`

```ts
dedupeShares(payloads: readonly SharePayload[]): SharePayload[]
```

Removes repeated payload IDs while preserving first-arrival order.

## Types

```ts
export type SharedContentType =
| 'text'
| 'url'
| 'image'
| 'video'
| 'audio'
| 'file';

export type SharedContentItem = {
id: string;
type: SharedContentType;
mimeType: string | null;
text?: string;
uri?: string;
fileName?: string;
size?: number;
};

export type SharePayload = {
id: string;
timestamp: number;
source: 'share-sheet';
title?: string;
items: SharedContentItem[];
};

export type ShareErrorEvent = {
code: string;
message: string;
};

export type ShareSubscription = {
remove(): void;
};
```

## Testing with an injected module

`createShareContentApi` builds the public wrapper around a compatible native module. It is useful for package tests and advanced dependency injection; most applications should use the default export.
71 changes: 71 additions & 0 deletions docs/docs/configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
sidebar_position: 4
title: Configuration
description: Configure Android MIME filters, iOS extension identifiers, activation rules, auto-open, and safety limits.
---

# Configuration

Pass options as the second item in the Expo config-plugin tuple.

```json title="app.json"
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.example.myapp"
},
"android": {
"package": "com.example.myapp"
},
"plugins": [
[
"expo-share-content",
{
"androidIntentFilters": ["text/plain", "image/*"],
"androidMultiIntentFilters": ["image/*"],
"iosShareExtensionName": "ShareExtension",
"maxSharedItems": 10,
"maxSharedFileSize": 52428800,
"maxSharedTotalSize": 157286400
}
]
]
}
}
```

## Options

| Option | Default | Description |
| --- | --- | --- |
| `androidIntentFilters` | text, image, video, audio, application wildcards | MIME types accepted for `ACTION_SEND` |
| `androidMultiIntentFilters` | image, video, audio, application wildcards | MIME types accepted for `ACTION_SEND_MULTIPLE` |
| `iosActivationRules` | text, URL, up to 10 images/movies/files | Activation-rule dictionary or predicate string |
| `iosAppGroupIdentifier` | `group.<bundleIdentifier>` | Shared container used by app and extension |
| `iosShareExtensionName` | `ShareExtension` | Xcode target and display name |
| `iosShareExtensionBundleIdentifier` | `<bundleIdentifier>.share` | Extension bundle ID |
| `iosDeploymentTarget` | `16.4` | Extension deployment target; cannot be lower than 16.4 |
| `iosOpenHostAppAfterShare` | `false` | Best-effort host opening after a successful share |
| `iosHostUrlScheme` | `expo.scheme` | URL scheme used when auto-open is enabled |
| `maxSharedItems` | `20` | Maximum item providers handled per share |
| `maxSharedFileSize` | `104857600` | Maximum bytes copied for one attachment (100 MiB) |
| `maxSharedTotalSize` | `262144000` | Aggregate bytes copied for one share (250 MiB) |

## Validation

The plugin rejects:

- malformed Android MIME types;
- empty target names;
- invalid bundle/App Group identifiers;
- non-positive or out-of-range limits;
- aggregate limits smaller than the per-file limit;
- iOS deployment targets below 16.4;
- unsafe `TRUEPREDICATE` activation rules.

An iOS bundle identifier is required because extension and App Group defaults are derived from it.

## Existing native projects

The plugin targets Expo prebuild/Continuous Native Generation. If native projects are committed and manually maintained, run prebuild in a disposable branch and review target, entitlement, build-phase, and manifest changes before adopting them.
74 changes: 74 additions & 0 deletions docs/docs/fundamentals/delivery-lifecycle.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
sidebar_position: 2
title: Delivery and file lifecycle
description: Build an idempotent at-least-once processing flow with explicit acknowledgement and file release.
---

# Delivery and file lifecycle

Delivery is **at least once**. The API favors preserving data over silently dropping a separate share operation.

## Pending and live delivery

- `getPendingSharesAsync()` peeks every queued payload, oldest first.
- `getInitialShareAsync()` peeks only the oldest payload.
- `addShareListener()` provides low-latency warm delivery.
- Pending queries and events do not consume queue records.

A live event and a pending query may expose the same stable payload ID. Maintain an in-memory or persisted handled-ID table when processing must be exactly-once at the business layer.

:::caution Android process restoration
Android has no trustworthy operation identifier for arbitrary `ACTION_SEND` callers. A task restored after process death can rarely redeliver a source intent under a new package-generated payload ID. Prefer duplicate delivery over dropping a legitimate separate share with identical content.
:::

## Acknowledge after success

```ts
await importShare(payload);
await ExpoShareContent.clearPendingSharesAsync([payload.id]);
```

Passing IDs clears only selected queue records. Omitting the argument clears every pending record:

```ts
await ExpoShareContent.clearPendingSharesAsync();
```

Do not acknowledge before your application import is durable. Leaving the record pending makes failure retryable on the next launch.

## File ownership

Binary items point to module-managed local files. The source app's temporary provider permission is no longer required, but the module still owns lifecycle cleanup.

A safe sequence is:

1. Read or copy every required attachment.
2. Persist application data and your own permanent file copies.
3. Acknowledge the queue record.
4. Release the module-managed attachment directory.

```ts
await ExpoShareContent.clearPendingSharesAsync([payload.id]);
await ExpoShareContent.releaseSharedFilesAsync([payload.id]);
```

`releaseSharedFilesAsync` rejects IDs that are still pending. This prevents a queue record from pointing to deleted files.

## Retention behavior

- Files for pending receipts are protected.
- Acknowledged but unreleased files remain eligible for automatic cleanup seven days after receipt.
- Each platform caps all module-managed attachment storage at 1 GiB.
- Your app owns permanent retention after import.

## Combining payload arrays

Use the named helper when combining event and query results:

```ts
import {dedupeShares} from 'expo-share-content';

const unique = dedupeShares([...pending, ...recentEvents]);
```

`dedupeShares` removes repeated IDs while preserving first-arrival order. It is not a replacement for persistent business-level idempotency.
Loading