npm.io
0.1.21 • Published 6d ago

@cherrydotfun/miniapp-sdk

Licence
MIT
Version
0.1.21
Deps
1
Size
561 kB
Vulns
0
Weekly
146

@cherrydotfun/miniapp-sdk

SDK for building mini-apps embedded in Cherry 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

npm install @cherrydotfun/miniapp-sdk

Peer dependencies — install only what you need:

# 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

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

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

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:

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):

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:

// 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:

// Next.js — next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "frame-ancestors 'self' https://chat.cherry.fun",
          },
        ],
      },
    ];
  },
};
// Express / Node.js
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://chat.cherry.fun");
  next();
});
# 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:

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');
}

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 — 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.

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:

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:

// 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 for a runnable SSR endpoint.

Vanilla JS (No React)

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/)
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';

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

createCherrySigner (kit/)
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 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):

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}</>;
}
  1. Helper — get auth config programmatically:
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 for a complete step-by-step guide.

AI-Assisted Integration

This package includes a Claude Code / Codex skill 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

Keywords