> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.23.0/readme/sharing.md).

# Sharing API

Store shared content and create share parameters. Recipients can jump straight into your content.

{% hint style="warning" %}
All SDK methods can reject. Unhandled rejections crash the app. Always wrap SDK calls in `try/catch` or attach a `.catch()` handler. See [Error Handling](/rundot-docs/v5.23.0/readme/error-handling.md) for details.
{% endhint %}

Here are some examples for what you might use it for:

* Store a user-created level and share a link to it, allowing anyone to play it
* Generate a challenge, like "Can you beat my score?"
* Generate a QR code for in-person content sharing.

## Quick Start

```typescript
import RundotGameAPI from '@series-inc/rundot-game-sdk/api'

const { shareUrl, shareLinkId } = await RundotGameAPI.social.shareLinkAsync({
  shareParams: {
    challengeType: 'highscore',
    scoreToBeat: '1500',
    challengerId: RundotGameAPI.getProfile().id,
  },
  metadata: {
    title: 'Beat my score!',
    description: 'Can you top 1500 points?',
    imageUrl: 'https://cdn.example.com/share-highscore.png',
  },
})

console.log('Shared link:', shareUrl)
console.log('Share handle:', shareLinkId)
```

`shareLinkAsync` resolves to `{ shareUrl, shareLinkId }`. On mobile (iOS/Android) it opens the native share sheet; on web it copies the share URL to the clipboard (with a fallback toast). Use `shareUrl` for telemetry or custom UI; no additional modal call is required. Keep `shareLinkId` around if you want to attach or read click data later (see [Click Tracking](#click-tracking-beta)).

### Share metadata

The optional `metadata` object holds OpenGraph values that social networks use to render rich link previews (Twitter, iMessage, etc.). The same shape is accepted by both `shareLinkAsync` and `createQRCodeAsync`. Every field is optional.

| Field         | Type     | Required | Description                                                                           |
| ------------- | -------- | -------- | ------------------------------------------------------------------------------------- |
| `title`       | `string` | No       | Preview title shown in link cards.                                                    |
| `description` | `string` | No       | Preview description shown under the title.                                            |
| `imageUrl`    | `string` | No       | Preview image URL. Must be an HTTPS URL for most social platforms to render the card. |

{% hint style="info" %}
`metadata.imageUrl` must be served over HTTPS. Most social platforms drop the preview image for non-HTTPS URLs.
{% endhint %}

### Pretty link slugs

Share links look like `https://w.run/s/k7Qp2x9`. You can append an optional, human-readable `slug` to the end — `https://w.run/s/k7Qp2x9/boss-level` — by passing it to `shareLinkAsync` or `createQRCodeAsync`:

```typescript
const { shareUrl } = await RundotGameAPI.social.shareLinkAsync({
  shareParams: { level: 'boss' },
  slug: 'Boss Level',
})
// shareUrl → https://w.run/s/k7Qp2x9/boss-level
```

The slug is **purely cosmetic**:

* It is sanitized server-side (lowercased, spaces/punctuation collapsed to hyphens, truncated). `"Boss Level"` becomes `boss-level`.
* It does **not** need to be unique — the same slug may appear on many links.
* It has **no** effect on resolution. The short code (`k7Qp2x9`) alone identifies the share; the slug is ignored when the link is opened.
* If it sanitizes to empty or is rejected by moderation, it is silently dropped and the link is returned without a slug segment (the share is still created).

Previously issued links remain valid; this only changes the shape of newly created ones.

## QR Codes

```typescript
const { shareUrl, qrCode, shareLinkId } = await RundotGameAPI.social.createQRCodeAsync({
  shareParams: {
    challengeType: 'daily',
    dailyPuzzleId: '2024-11-04',
  },
  metadata: {
    title: 'Daily Challenge',
    description: 'Scan to play today!',
  },
  qrOptions: {
    size: 512,
    margin: 4,
    format: 'png',
  },
})

document.querySelector('#qr').src = qrCode
```

`createQRCodeAsync` resolves to `{ shareUrl, qrCode, shareLinkId }`, so you get the underlying link alongside the QR image and don't need a second call to reuse it. It only returns data; display or share the image yourself. Pair it with an on-screen `<img>` element, print asset, or custom UI element.

### QR options

| Field    | Type             | Required | Default | Description                                                                                    |
| -------- | ---------------- | -------- | ------- | ---------------------------------------------------------------------------------------------- |
| `size`   | `number`         | No       | `256`   | QR image size in pixels.                                                                       |
| `margin` | `number`         | No       | `2`     | Quiet-zone margin around the QR code, in pixels.                                               |
| `format` | `'png' \| 'svg'` | No       | `'png'` | Output format. `png` returns a `data:image/png` URL; `svg` returns a `data:image/svg+xml` URL. |

## Interpreting share intent on Game Start

The data stored when creating a share link is delivered to your game via [`RundotGameAPI.app.resolveLaunchIntent()`](/rundot-docs/v5.23.0/readme/app.md#launch-intent). Read it on boot — this is the supported path and, unlike the deprecated `context.shareParams` snapshot, it correctly handles **deferred** share links (fresh installs whose attribution resolves asynchronously).

```typescript
const intent = await RundotGameAPI.app.resolveLaunchIntent({ maxWaitMs: 800 })
if (intent.kind === 'share' && intent.params.challengeType === 'highscore') {
  startHighScoreChallenge(parseInt(intent.params.scoreToBeat, 10), intent.params.challengerId)
}
```

Share params are fully custom. Define the schema your game needs (`challengeType`, `dailyPuzzleId`, `referredBy`, etc.) and decode when the experience boots.

{% hint style="warning" %}
Share params are typed `Record<string, string>`: every value must be a string. To carry structured data (objects, arrays, numbers), `JSON.stringify` it before sharing and `JSON.parse` it back from `intent.params` on launch.
{% endhint %}

For example, say you have a game where users can share user-generated content with one another in the form of custom levels. Here is how you might handle share links:

```typescript
const onShare = async (levelData, levelName) => {
  const { shareUrl } = await RundotGameAPI.social.shareLinkAsync({
    shareParams: {
      levelData: JSON.stringify(levelData),
      levelName: levelName
    },
  })
  return shareUrl
}

const onLaunch = async () => {
  const intent = await RundotGameAPI.app.resolveLaunchIntent({ maxWaitMs: 800 })
  if (intent.kind === 'share' && 'levelData' in intent.params) {
    loadLevel(JSON.parse(intent.params.levelData), intent.params.levelName)
  } else {
    loadBlankLevel()
  }
}
```

> A fresh share install on iOS may first return `{ kind: 'timed_out' }` while attribution resolves behind the ATT prompt. Re-call `resolveLaunchIntent` to keep waiting — see the [re-call pattern](/rundot-docs/v5.23.0/readme/app.md#canonical-pattern-handles-deferred-deep-links).

## Click Tracking (BETA)

Every share (`shareLinkAsync` or `createQRCodeAsync`) returns a `shareLinkId`. Use it to record metadata when a recipient clicks through, then read those clicks back for referral and attribution flows.

{% hint style="warning" %}
Click metadata is readable by any authenticated user who has the `shareLinkId`. Never write PII or sensitive user data. Values are returned verbatim, so escape them before rendering in HTML.
{% endhint %}

### `addShareClickDataAsync(options): Promise<void>`

Write click metadata to a share as the current user (the clicker). Metadata is a full replacement: each call overwrites the entire metadata object for that user, so send the complete set of keys every time you want to preserve prior values.

| Field         | Type                     | Required | Description                                                                              |
| ------------- | ------------------------ | -------- | ---------------------------------------------------------------------------------------- |
| `shareLinkId` | `string`                 | Yes      | The share to attach metadata to (from `shareLinkAsync` / `createQRCodeAsync`).           |
| `metadata`    | `Record<string, string>` | Yes      | String key-value pairs. Limits: max 20 keys, 64-char keys, 1024-char values, 4 KB total. |

```typescript
// shareLinkId comes from the share you created, or from a share param your
// game stored in shareParams when generating the link.
await RundotGameAPI.social.addShareClickDataAsync({
  shareLinkId,
  metadata: { source: 'qr', referredScore: '1500' },
})
```

### `getShareClicksAsync(options): Promise<{ clicks: ShareClickData[]; truncated: boolean }>`

Query all click data for a share. Returns up to 200 click records ordered by `createdAt` ascending; `truncated` is `true` when that cap was hit.

| Field         | Type     | Required | Description         |
| ------------- | -------- | -------- | ------------------- |
| `shareLinkId` | `string` | Yes      | The share to query. |

```typescript
const { clicks, truncated } = await RundotGameAPI.social.getShareClicksAsync({
  shareLinkId,
})
console.log(`${clicks.length} clicks${truncated ? ' (capped at 200)' : ''}`)
```

### `getMyShareClickDataAsync(options): Promise<ShareClickData | null>`

Get the current user's click data for a specific share, or `null` if they haven't clicked it. Useful for one-time referral rewards and "you already redeemed this" checks.

| Field         | Type     | Required | Description         |
| ------------- | -------- | -------- | ------------------- |
| `shareLinkId` | `string` | Yes      | The share to query. |

```typescript
const myClick = await RundotGameAPI.social.getMyShareClickDataAsync({
  shareLinkId,
})
if (myClick) {
  console.log('Already clicked at', myClick.createdAt)
}
```

### `ShareClickData`

The return shape for the click-tracking methods.

| Field              | Type                     | Description                                                      |
| ------------------ | ------------------------ | ---------------------------------------------------------------- |
| `clickerProfileId` | `string`                 | Profile ID of the user who recorded the click.                   |
| `metadata`         | `Record<string, string>` | The string key-value pairs written via `addShareClickDataAsync`. |
| `createdAt`        | `number`                 | When the click was first recorded, in epoch milliseconds.        |
| `updatedAt`        | `number`                 | When the click metadata was last updated, in epoch milliseconds. |

## File Sharing (BETA)

Share locally-created files (recorded gameplay clips, screenshots, generated images) directly to native share targets (TikTok, Instagram, Messages, etc.) via the Web Share API.

{% hint style="info" %}
File sharing works on **both mobile web and native iOS/Android**. On web, it uses the Web Share API (iOS Safari 15+, Chrome 89+, Edge 89+). On native, it opens the system share sheet via `expo-sharing`. Use `canShareFileAsync` to feature-detect before recording or generating shareable content.

**Native limitation:** `cancelled` is always `false` on iOS/Android; the native share sheet does not report whether the user dismissed it.
{% endhint %}

### Feature Detection

```typescript
const { supported } = await RundotGameAPI.social.canShareFileAsync()
if (supported) {
  showRecordButton()
}
```

### Sharing a File

```typescript
// Share a screenshot as PNG
const pngBlob = canvas.toBlob('image/png')
const result = await RundotGameAPI.social.shareFileAsync({
  data: pngBlob,
  filename: 'screenshot.png',
  mimeType: 'image/png',
  title: 'Check out my score!',
  text: 'I just scored 1500 points!',
})

if (result.cancelled) {
  console.log('User dismissed the share sheet')
}
```

{% hint style="warning" %}
`shareFileAsync` rejects (throws) when the platform does not support file sharing, or when the file fails MIME/size validation. It does not return a soft failure for those cases; `cancelled` only reports user dismissal. Gate calls behind `canShareFileAsync()` and wrap them in `try/catch`.
{% endhint %}

### Share file options

| Field      | Type                            | Required | Description                                                                                                                                    |
| ---------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`     | `Blob \| ArrayBuffer \| string` | Yes      | File bytes. `Blob` / `ArrayBuffer` are base64-encoded automatically. A `string` must be raw base64 of the file bytes (see Data formats below). |
| `filename` | `string`                        | Yes      | Filename presented to the share target.                                                                                                        |
| `mimeType` | `ShareFileMimeType`             | Yes      | One of the [accepted file types](#accepted-file-types). Any other value is a compile-time error and a runtime rejection.                       |
| `title`    | `string`                        | No       | Title passed alongside the file to the native share sheet.                                                                                     |
| `text`     | `string`                        | No       | Text passed alongside the file to the native share sheet.                                                                                      |

### Accepted File Types

| MIME Type    | Extension | Notes                          |
| ------------ | --------- | ------------------------------ |
| `video/mp4`  | `.mp4`    | Recorded gameplay clips        |
| `image/png`  | `.png`    | Screenshots, generated images  |
| `image/jpeg` | `.jpeg`   | Photos, compressed screenshots |
| `image/webp` | `.webp`   | Modern image format            |
| `image/gif`  | `.gif`    | Animated images                |

### Limits

* **Max file size:** 10 MB. Files exceeding this are rejected before encoding.
* **Rate limit:** One share per 3 seconds per game instance.
* **Data formats:** Pass a `Blob`, `ArrayBuffer`, or pre-encoded base64 string. Blob/ArrayBuffer inputs are encoded to base64 automatically. A pre-encoded `string` must be raw base64 of the file bytes, NOT a data URI: strip any `data:image/png;base64,` prefix first.

## API Reference

| Method                              | Returns                                      | Description                                                                                    |
| ----------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `shareLinkAsync(options)`           | `Promise<{ shareUrl, shareLinkId }>`         | Create and share a link with custom parameters; `shareLinkId` is the handle for click tracking |
| `createQRCodeAsync(options)`        | `Promise<{ shareUrl, qrCode, shareLinkId }>` | Generate a QR code data URL plus the underlying share URL and `shareLinkId`                    |
| `addShareClickDataAsync(options)`   | `Promise<void>`                              | Record click metadata on a share as the current user (BETA)                                    |
| `getShareClicksAsync(options)`      | `Promise<{ clicks, truncated }>`             | Read all click records for a share, up to 200 (BETA)                                           |
| `getMyShareClickDataAsync(options)` | `Promise<ShareClickData \| null>`            | Read the current user's click data for a share, or `null` (BETA)                               |
| `shareFileAsync(options)`           | `Promise<{ cancelled }>`                     | Share a local file to native share targets (BETA)                                              |
| `canShareFileAsync()`               | `Promise<{ supported }>`                     | Check if file sharing is available (BETA)                                                      |

## Payload Guidelines

* Keep `shareParams` under \~100 KB (share payloads are subject to a server-side size cap).
* Use compact identifiers (IDs, short strings) and fetch bulky data from your backend.
* Sanitize user-provided metadata before sharing publicly.
* Fallback when social APIs are unavailable (desktop browsers without share support).

## Best Practices

* Use `shareLinkAsync` for instant share sheets; reserve `createQRCodeAsync` for in-person or kiosk flows.
* Inspect the launch intent via [`RundotGameAPI.app.resolveLaunchIntent()`](/rundot-docs/v5.23.0/readme/app.md#launch-intent) on boot and branch gameplay early—players expect to land in the invited context immediately.
* Reward referrers only after validating signatures or payloads on your backend to prevent spoofing.
* Provide manual copy buttons for desktop browsers without native sharing.
