# @cherrydotfun/miniapp-sdk

SDK for building mini-apps embedded in [Cherry](https://cherry.fun) messenger. Provides wallet integration, user/room context, and navigation — works in both WebView (mobile) and iframe (web).

Supports both **@solana/web3.js** (legacy wallet-adapter) and **@solana/kit** (modern TransactionSigner).

## Install

```bash
npm install @cherrydotfun/miniapp-sdk
```

Peer dependencies — install only what you need:

```bash
# For @solana/web3.js (legacy wallet-adapter)
npm install @solana/wallet-adapter-base @solana/web3.js

# For @solana/kit (modern)
npm install @solana/signers

# For React hooks
npm install react
```

## Package Exports

| Entry Point | Description | Solana Dependency |
|-------------|-------------|-------------------|
| `@cherrydotfun/miniapp-sdk` | Core client, bridge, env detection, token verification | None |
| `@cherrydotfun/miniapp-sdk/react` | React provider and hooks | None |
| `@cherrydotfun/miniapp-sdk/solana` | CherryWalletAdapter for wallet-adapter ecosystem | `@solana/web3.js` + `@solana/wallet-adapter-base` |
| `@cherrydotfun/miniapp-sdk/kit` | TransactionSigner for @solana/kit | None (structural typing) |

## Quick Start — @solana/web3.js

```tsx
import { CherryMiniAppProvider, useCherryMiniApp, useCherryWallet } from '@cherrydotfun/miniapp-sdk/react';
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';

// Drop-in for @solana/wallet-adapter-react
const wallets = [new CherryWalletAdapter()];

function MyGame() {
  const { user, room, launchToken, isReady } = useCherryMiniApp();
  const { publicKey, signTransaction, signAllTransactions, signMessage } = useCherryWallet();

  if (!isReady) return <div>Loading...</div>;

  return (
    <div>
      <p>Welcome, {user.displayName}!</p>
      <p>Room: {room.title} ({room.memberCount} members)</p>
    </div>
  );
}
```

## Quick Start — @solana/kit

```tsx
import { CherryMiniApp } from '@cherrydotfun/miniapp-sdk';
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';

const cherry = new CherryMiniApp();
await cherry.init();

// TransactionSigner — use with @solana/kit transaction builders
const signer = createCherrySigner(cherry);

// Sign transactions
const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);

// Sign messages
const [signature] = await signer.signMessages([messageBytes]);
```

## Quick Start — React + Kit

```tsx
import { CherryMiniAppProvider, useCherryApp } from '@cherrydotfun/miniapp-sdk/react';
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';

function MyGame() {
  const app = useCherryApp(); // CherryMiniApp instance

  const handleSign = async () => {
    const signer = createCherrySigner(app);
    const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);
  };
}
```

## Environment Detection

Check if running inside Cherry before initializing:

```tsx
import { isInsideCherry, getCherryEnvironment } from '@cherrydotfun/miniapp-sdk';

if (isInsideCherry()) {
  // Running inside Cherry — SDK will work
} else {
  // Standalone — show regular wallet connect
}

const env = getCherryEnvironment();
// env.platform: 'webview' | 'iframe' | 'standalone'
// env.isEmbedded: boolean
```

React hook (no provider needed):

```tsx
import { useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';

function App() {
  const { isEmbedded, platform } = useCherryEnvironment();
  if (!isEmbedded) return <StandaloneApp />;
  return <CherryMiniAppProvider><EmbeddedApp /></CherryMiniAppProvider>;
}
```

### Strict Mode

By default the SDK uses fallback heuristics for backward compatibility with older Cherry builds:

- `ReactNativeWebView` — present in any React Native WebView, not just Cherry's
- `window.parent !== window` — true inside any iframe, not just Cherry's

This can cause **false positives** (e.g. wallet in-app browsers). Once your users are on a Cherry version that injects `window.__cherry` (WebView) or appends `cherry_embed=1` (iframe), enable **strict mode** to rely only on Cherry-specific signals:

```tsx
// Standalone functions
isInsideCherry({ strict: true });
getCherryEnvironment({ strict: true });
detectPlatform({ strict: true });

// React hook
const { isEmbedded } = useCherryEnvironment({ strict: true });

// Provider (passes strict to CherryMiniApp internally)
<CherryMiniAppProvider strict={true}>...</CherryMiniAppProvider>

// CherryMiniApp
const cherry = new CherryMiniApp({ strict: true });
```

In strict mode only these signals are accepted:
- **Mobile WebView:** `window.__cherry === true` (injected by Cherry before page load)
- **Web iframe:** `cherry_embed=1` query parameter (appended by Cherry host to the URL)

## Web Embedding — CORS & CSP

When your mini-app runs inside the Cherry **web client** (iframe), the browser enforces standard cross-origin policies. Two things must be configured on your mini-app's server for the embed to work.

### 1. Allow Cherry to frame your app (`frame-ancestors`)

By default many frameworks set `X-Frame-Options: SAMEORIGIN` or a restrictive `Content-Security-Policy`, which blocks any iframe embedding. You need to explicitly allow Cherry's origin.

**Option A — CSP header (recommended):**
```
Content-Security-Policy: frame-ancestors 'self' https://chat.cherry.fun
```

**Option B — X-Frame-Options (legacy, less flexible):**
```
X-Frame-Options: ALLOW-FROM https://chat.cherry.fun
```

> `X-Frame-Options: ALLOW-FROM` is ignored by Chrome/Firefox — prefer the CSP header.

Framework examples:

```ts
// Next.js — next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "frame-ancestors 'self' https://chat.cherry.fun",
          },
        ],
      },
    ];
  },
};
```

```ts
// Express / Node.js
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://chat.cherry.fun");
  next();
});
```

```nginx
# Nginx
add_header Content-Security-Policy "frame-ancestors 'self' https://chat.cherry.fun" always;
```

### 2. CORS on your backend API

When your mini-app frontend (served from `https://yourgame.example`) calls its own backend API, the browser sends the request with `Origin: https://yourgame.example` — same as outside the iframe, so **no additional CORS config is needed** if your API already allows that origin.

The one case that requires attention: if your API validates the `Referer` header or only allows requests when `Origin` exactly matches a whitelist, make sure `https://yourgame.example` is in that list. The Cherry host page is never the origin of your API calls — the iframe is its own browsing context.

If your mini-app calls any **Cherry API endpoints** directly (not via the SDK bridge), add the appropriate `Access-Control-Allow-Origin` on your side or proxy through your own backend.

### Checklist

| | Requirement |
|---|---|
| ✅ | `Content-Security-Policy: frame-ancestors … https://chat.cherry.fun` on all HTML responses |
| ✅ | No `X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` without override |
| ✅ | Backend API allows `Origin: https://yourgame.example` (usually already true) |
| ✅ | No `Referer`-based origin checks that would break inside an iframe |

## Navigation

Open Cherry screens from your mini-app:

```tsx
import { useCherryNavigate } from '@cherrydotfun/miniapp-sdk/react';

function MyComponent() {
  const navigate = useCherryNavigate();

  // Open user profile — accepts wallet address, domain, or @handle
  await navigate.userProfile('alice.sol');
  await navigate.userProfile('@alice');

  // Open room — accepts roomId or @handle
  await navigate.openRoom('@solminer');
  await navigate.openRoom('roomId123');
}
```

## Blinks (interactive in-chat cards)

A **blink** is a mini-app rendered inline as a card inside a chat message. The
same mini-app you build can run both fullscreen and as a blink — the SDK adapts.

> 📘 **Full guide:** [BLINKS.md](./BLINKS.md) — how blinks work end-to-end,
> params, height/resize, callbacks & live updates, wallet signing gotchas,
> sharing, SSR, limits, and troubleshooting.

### Sharing Results

Hand a **read-only "result" snapshot** to the Cherry host so the user can share
it into a DM or group as an interactive blink card. The host opens a recipient
picker with a preview; on send, the result carries the new message's unique
`messageId`.

```tsx
import { useCherryShare } from '@cherrydotfun/miniapp-sdk/react';

function ShareButton() {
  const share = useCherryShare();

  const onClick = async () => {
    const res = await share({
      route: '/result',                  // route the receiver opens (default '/')
      params: { score: 9000 },           // snapshot rendered read-only (≤ 4 KB JSON, depth ≤ 8)
      height: 'medium',                  // 'compact' | 'medium' | 'tall'
      initialHeight: 180,                // optional: exact px the card opens at (≤ bucket max)
      caption: 'I scored 9000 points!',  // optional caption shown by the card
    });
    if (res.shared) {
      // res.roomId   — where it was shared
      // res.messageId — unique id of the created blink message (record it to
      //                 correlate later callbacks / bot:blink_update events)
    }
  };

  return <button onClick={onClick}>Share</button>;
}
```

Vanilla JS: `await cherry.share({ params: { score: 9000 } })`.

**How it works / guarantees**

- **A mini-app can only share *itself*.** You never name the mini-app — the host
  derives the identity from your current session's launch token. `route`,
  `params`, `height`, `initialHeight` and `caption` are the only things you control.
- **Read-only snapshot.** Shared blinks are non-interactive (no callback
  buttons) — there is no bot behind them to answer callbacks. The `params` you
  pass are the data the receiver's mini-app renders.
- **Authored by the user.** The resulting message's sender is the user's wallet
  (not a bot), with `metadata.senderType = 'user_share'`.
- The mini-app must declare the `inline:render` permission to be shareable.

## Launch Token (Backend Verification)

The SDK provides a JWT launch token signed by Cherry's server. Verify it on your backend:

```ts
import { verifyLaunchToken } from '@cherrydotfun/miniapp-sdk';

const payload = await verifyLaunchToken(token, {
  expectedAppId: 'your-app-id',
  // jwksUrl defaults to https://chat.cherry.fun/.well-known/jwks.json
});

// Always present:
// payload.sub — wallet address
// payload.room_id — room where app was opened

// Embed / fullscreen handshake token also carries:
// payload.user — { display_name, avatar_url }
// payload.room — { title, member_count }

// Inline / blink launch tokens also carry (all optional in the type):
// payload.message_id  — unique id of the blink message this token is bound to
// payload.mini_app_id — the mini-app being rendered
// payload.route       — route to open
// payload.params      — the snapshot payload (signed → tamper-proof)
// payload.height      — 'compact' | 'medium' | 'tall'
// payload.initial_height — px the card opens at (no-jump), if the sender pinned one
// payload.interactive — false for read-only shared snapshots
// payload.source      — 'user_share' for user-shared snapshots
```

### Server-Side Rendering (SSR)

The launch token rides in the launch URL's **query string**
(`/inline?token=...`), so it reaches your mini-app's server. That means you can
render a blink server-side and bind per-message state **before** the client
mounts — keyed by the token's `message_id`:

```ts
// Express-style handler for GET /inline?token=...
import { verifyLaunchToken } from '@cherrydotfun/miniapp-sdk';

app.get('/inline', async (req, res) => {
  const payload = await verifyLaunchToken(String(req.query.token), {
    expectedAppId: 'your-app-id',
  });

  const messageId = payload.message_id;      // stable key for this blink
  const params = payload.params ?? {};        // signed snapshot data
  await bindStateFor(messageId, params);      // your pre-render binding

  res.send(renderBlinkHtml(params));          // SSR the card
});
```

> Keep snapshot data **inside the signed token's `params`** — do not pass raw
> params as separate query fields, or they become forgeable. The token keeps
> them signed (RS256) and verified.

See [`example/server.ts`](./example/server.ts) for a runnable SSR endpoint.

## Vanilla JS (No React)

```ts
import { CherryMiniApp } from '@cherrydotfun/miniapp-sdk';

const cherry = new CherryMiniApp();
await cherry.init();

cherry.user.publicKey;   // wallet address
cherry.room.title;       // room name
cherry.launchToken;      // JWT for backend

const sig = await cherry.wallet.signMessage(new TextEncoder().encode('hello'));
const signed = await cherry.wallet.signAllTransactions([tx1, tx2, tx3]); // batch sign
await cherry.navigate.userProfile('alice.sol');
const res = await cherry.share({ params: { score: 9000 } }); // share a result snapshot

cherry.on('suspended', () => console.log('App suspended'));
cherry.on('resumed', () => console.log('App resumed'));
```

## API Reference

### React Hooks

| Hook | Description |
|------|-------------|
| `useCherryMiniApp()` | `{ user, room, launchToken, isReady, error }` |
| `useCherryApp()` | `CherryMiniApp` instance (for kit signer etc.) |
| `useCherryWallet()` | `{ publicKey, connected, signTransaction, signAllTransactions, signMessage, signAndSendTransaction }` |
| `useCherryNavigate()` | `{ userProfile(id), openRoom(id) }` |
| `useCherryShare()` | `(opts?) => Promise<{ shared, roomId?, messageId? }>` — share a read-only result snapshot |
| `useCherryEnvironment(opts?)` | `{ isEmbedded, platform }` — no provider needed; pass `{ strict: true }` to disable fallbacks |

### CherryMiniApp (Core)

| Property/Method | Description |
|-----------------|-------------|
| `new CherryMiniApp(opts?)` | `opts.initTimeout` (ms, default 10 000); `opts.strict` (disable fallback detection) |
| `init()` | Wait for Cherry host handshake |
| `user` | `{ publicKey, displayName, avatarUrl }` |
| `room` | `{ id, title, memberCount }` |
| `launchToken` | JWT string for backend verification |
| `wallet.signTransaction(tx)` | Sign a transaction (returns `Uint8Array`) |
| `wallet.signAllTransactions(txs)` | Sign multiple transactions in a single batch (returns `Uint8Array[]`) |
| `wallet.signMessage(msg)` | Sign an arbitrary message |
| `wallet.signAndSendTransaction(tx)` | Sign and submit transaction |
| `navigate.userProfile(id)` | Open user profile (wallet/domain/@handle) |
| `navigate.openRoom(id)` | Open room (roomId/@handle) |
| `share(opts?)` | Share a read-only result snapshot → `{ shared, roomId?, messageId? }` |
| `on(event, handler)` | Listen to `suspended`, `resumed`, `walletDisconnected` |
| `destroy()` | Cleanup listeners |

### CherryWalletAdapter (solana/)

```ts
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';
```

Drop-in `BaseWalletAdapter` for `@solana/wallet-adapter-react`. Handles connect, signTransaction, signAllTransactions, signMessage, sendTransaction.

### createCherrySigner (kit/)

```ts
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';
```

Returns a `TransactionSigner` compatible with `@solana/kit`. Supports `signTransactions` and `signMessages`.

### Bridge Protocol

The SDK communicates with Cherry via `postMessage`. The protocol is versioned (`v2`) and uses JWT launch tokens for authentication.

| Message | Direction | Description |
|---------|-----------|-------------|
| `cherry:init` | Host → App | Handshake with JWT token |
| `cherry:ready` | App → Host | App acknowledges init |
| `cherry:request` | App → Host | Wallet/navigate/share operations (e.g. `host.share`, `wallet.signTransaction`) |
| `cherry:response` | Host → App | Operation result |
| `cherry:event` | Host → App | Lifecycle events |

App→Host request methods include `wallet.signMessage`, `wallet.signTransaction`, `wallet.signAndSendTransaction`, `navigate.userProfile`, `navigate.openRoom`, and `host.share`. Prefer the typed hooks/methods above over calling the bridge directly.

## Privy Integration

If your mini-app uses [Privy](https://privy.io) for authentication or embedded wallets, you can use Cherry's launch token as a **custom auth provider** — giving users zero-click login inside Cherry.

### Setup

1. **Privy Dashboard** → Settings → Custom Auth → Add Provider:
   - JWKS URL: `https://chat.cherry.fun/.well-known/jwks.json`
   - Issuer: `https://chat.cherry.fun`
   - User ID field: `sub`

2. **Code** — dual-mode login (Cherry + standalone):

```tsx
import { CherryMiniAppProvider, useCherryApp, useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
import { usePrivy } from '@privy-io/react-auth';

function AuthGate({ children }) {
  const { isEmbedded } = useCherryEnvironment();
  const cherry = useCherryApp();
  const { loginWithCustomAccessToken, authenticated, ready } = usePrivy();

  useEffect(() => {
    if (!ready || authenticated) return;
    if (isEmbedded && cherry?.launchToken) {
      loginWithCustomAccessToken(cherry.launchToken); // transparent login
    }
  }, [ready, authenticated, isEmbedded, cherry]);

  if (!authenticated && !isEmbedded) return <PrivyLoginButton />;
  return <>{children}</>;
}
```

3. **Helper** — get auth config programmatically:

```ts
import { getCherryCustomAuthConfig } from '@cherrydotfun/miniapp-sdk';

const { token, jwksUrl, issuer } = getCherryCustomAuthConfig(cherry);
```

| Environment | Login Method | User Action |
|-------------|-------------|-------------|
| Inside Cherry | `loginWithCustomAccessToken(launchToken)` | None — automatic |
| Standalone | Standard Privy UI (email, social, wallet) | User clicks login |

See the [integration skill](./skills/cherry-miniapp-integration/SKILL.md) for a complete step-by-step guide.

## AI-Assisted Integration

This package includes a [Claude Code / Codex skill](./skills/README.md) that automates SDK integration into existing web3 apps. After installing the SDK, copy the skill to your AI assistant and say "Integrate Cherry Mini-App SDK" — it will analyze your codebase and guide you step by step.

## License

MIT
