---
name: cherry-miniapp-integration
description: "Use when integrating @cherrydotfun/miniapp-sdk into an existing web3 application — adding Cherry embed support, wallet bridge, environment detection, and conditional UI rendering for apps that run both standalone and inside Cherry messenger."
---
# Cherry Mini-App SDK Integration
## Overview
Step-by-step guide for integrating `@cherrydotfun/miniapp-sdk` into an existing web3 application so it works both as a standalone app and embedded inside Cherry messenger (WebView on mobile, iframe on web).
The SDK supports two Solana integration paths:
- **@solana/web3.js** — legacy wallet-adapter (`sdk/solana`)
- **@solana/kit** — modern TransactionSigner (`sdk/kit`)
## When to Use
- Adding Cherry embed support to an existing Solana dApp
- Making a game or utility embeddable inside Cherry chat rooms
- Letting users share results from the app into chats as inline blink cards (Step 7b)
- Migrating from the legacy `cherry-wallet-bridge` protocol to SDK v2
- Setting up conditional UI (hide/show elements) based on embed context
## Prerequisites
The target app must be:
- A web application (React, Vue, vanilla JS, etc.)
- Using Solana (either @solana/web3.js or @solana/kit)
- Deployable as a static site or SPA accessible via URL
## Integration Checklist
Follow these steps IN ORDER. Mark each as completed before moving to the next.
### Step 1: Discovery — Understand the Target App
Research the codebase to answer:
1. **Framework**: React? Vue? Vanilla? Next.js? Vite?
2. **Solana SDK**: `@solana/web3.js` (legacy) or `@solana/kit` (modern)?
3. **Wallet integration**: wallet-adapter? custom? Which wallets?
4. **UI elements to hide in embed**: What should NOT appear inside Cherry?
- Wallet connect button (Cherry provides the wallet)
- App header/navigation bar (Cherry has its own)
- Footer, sidebar, landing/splash screen
5. **Entry point**: Where does the app mount? (`main.tsx`, `App.tsx`)
6. **Build system**: Vite? Webpack?
ASK THE USER:
```
When this app runs inside Cherry, which UI elements should be hidden?
Common choices:
- Wallet connect button/modal
- Top navigation/header
- Footer
- Sidebar
- Landing/splash screen
Which Solana SDK does this project use?
- @solana/web3.js (with wallet-adapter)
- @solana/kit (modern)
```
### Step 2: Install SDK
```bash
npm install @cherrydotfun/miniapp-sdk
```
Install peer deps based on Solana SDK choice:
```bash
# For @solana/web3.js projects
npm install @solana/wallet-adapter-base @solana/web3.js
# For @solana/kit projects — no additional deps needed
```
### Step 2b: Server Configuration — CORS & CSP (Web iframe only)
> Skip if the mini-app will only be embedded in Cherry **mobile** (WebView). Required for Cherry **web** (iframe).
The browser blocks iframes and API calls unless the mini-app's server explicitly allows them.
#### Allow Cherry to embed the app (`frame-ancestors`)
Add to **every HTML response** from the mini-app's server:
```
Content-Security-Policy: frame-ancestors 'self' https://chat.cherry.fun
```
Make sure there is **no** `X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` without override (these silently block the iframe in Chrome/Firefox).
Framework-specific examples:
```ts
// Next.js — next.config.ts
async headers() {
return [{
source: '/(.*)',
headers: [{ key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://chat.cherry.fun" }],
}];
}
```
```ts
// Express
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;
```
#### Backend API CORS
Usually **no changes needed** — the iframe runs on the mini-app's own origin, so API calls are same-origin. Only act if:
- The backend has an explicit `Origin` allowlist → make sure `https://yourgame.example` is in it
- The backend checks `Referer` for CSRF protection → verify iframe requests still pass
#### Checklist
| | Requirement |
|---|---|
| ✅ | `frame-ancestors … https://chat.cherry.fun` on all HTML responses |
| ✅ | No `X-Frame-Options: DENY/SAMEORIGIN` without override |
| ✅ | Backend `Origin` allowlist includes the mini-app's own domain |
### Step 3: Environment Detection — Conditional Rendering
Add environment detection at the TOP of the component tree, BEFORE any provider:
```tsx
import { isInsideCherry, getCherryEnvironment } from '@cherrydotfun/miniapp-sdk';
const embedded = isInsideCherry();
const { platform } = getCherryEnvironment();
// platform: 'webview' (mobile) | 'iframe' (web) | 'standalone'
```
React hook (no provider needed):
```tsx
import { useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
function App() {
const { isEmbedded, platform } = useCherryEnvironment();
}
```
**Strict mode** — opt in once all Cherry hosts your users run inject `window.__cherry` (WebView) or append `cherry_embed=1` (iframe). Prevents false positives in wallet in-app browsers:
```tsx
// All detection APIs accept { strict: true }
isInsideCherry({ strict: true });
const { isEmbedded } = useCherryEnvironment({ strict: true });
...
new CherryMiniApp({ strict: true });
```
Without strict mode, fallbacks (`ReactNativeWebView`, `window.parent !== window`) are active for backward compatibility with older Cherry builds.
### Step 4a: Wallet — @solana/web3.js Path
For projects using `@solana/wallet-adapter-react`:
```tsx
import { CherryWalletAdapter } from '@cherrydotfun/miniapp-sdk/solana';
import { isInsideCherry } from '@cherrydotfun/miniapp-sdk';
function getWallets() {
if (isInsideCherry()) {
return [new CherryWalletAdapter()];
}
return [new PhantomWalletAdapter(), new SolflareWalletAdapter()];
}
const wallets = useMemo(() => getWallets(), []);
const embedded = isInsideCherry();
{embedded ? {children} : children}
```
Auto-select helper:
```tsx
function AutoSelectCherry({ children }: { children: ReactNode }) {
const { select, wallets } = useWallet();
useEffect(() => {
const cherry = wallets.find(w => w.adapter.name === 'Cherry');
if (cherry) select(cherry.adapter.name);
}, [wallets, select]);
return <>{children}>;
}
```
### Step 4b: Wallet — @solana/kit Path
For projects using `@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]);
```
With React:
```tsx
import { CherryMiniAppProvider, useCherryApp } from '@cherrydotfun/miniapp-sdk/react';
import { createCherrySigner } from '@cherrydotfun/miniapp-sdk/kit';
function MyComponent() {
const app = useCherryApp();
const handleSign = async () => {
const signer = createCherrySigner(app);
const [signed] = await signer.signTransactions([{ messageBytes, signatures: {} }]);
};
}
```
### Step 5: Cherry Provider — User & Room Context
Wrap the embedded app with `CherryMiniAppProvider`:
```tsx
import { CherryMiniAppProvider } from '@cherrydotfun/miniapp-sdk/react';
function Root() {
const embedded = isInsideCherry();
if (embedded) {
return (
);
}
return ; // standalone mode
}
```
Access context in components:
```tsx
import { useCherryMiniApp, useCherryWallet } from '@cherrydotfun/miniapp-sdk/react';
function GameUI() {
const { user, room, launchToken, isReady } = useCherryMiniApp();
const { publicKey, signTransaction, signAllTransactions, signMessage } = useCherryWallet();
}
```
### Step 6: Hide Standalone-Only UI
```tsx
function Header() {
const { isEmbedded } = useCherryEnvironment();
if (isEmbedded) return null;
return ;
}
function WalletButton() {
const { isEmbedded } = useCherryEnvironment();
if (isEmbedded) return null;
return ;
}
```
### Step 7: Navigation — Cherry Host Integration
```tsx
import { useCherryNavigate } from '@cherrydotfun/miniapp-sdk/react';
function UserList() {
const navigate = useCherryNavigate();
const { isEmbedded } = useCherryEnvironment();
const handleUserClick = (address: string) => {
if (isEmbedded) {
navigate.userProfile(address); // wallet, domain, or @handle
} else {
router.push(`/profile/${address}`);
}
};
}
```
### Step 7b: Sharing Results (Optional)
Let the user share a read-only "result" snapshot from your app into a DM or
group as an inline blink card. Only available inside Cherry.
```tsx
import { useCherryShare, useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
function ShareResult({ score }: { score: number }) {
const share = useCherryShare();
const { isEmbedded } = useCherryEnvironment();
if (!isEmbedded) return null; // host-only feature
const onShare = async () => {
const res = await share({
route: '/result',
params: { score }, // ≤ 4 KB JSON, depth ≤ 8; rendered read-only
caption: `I scored ${score}!`,
});
if (res.shared) {
// res.messageId — unique id of the created blink message; record it to
// correlate later (e.g. bind backend state to this blink).
}
};
return ;
}
```
Notes:
- The app can only share **itself** — the host derives the mini-app identity
from the session token; you only control `route`/`params`/`height`/`caption`.
- Shared blinks are **read-only** (no callback buttons). Put the data to render
in `params`.
- Requires the `inline:render` permission on the mini-app.
### Step 8: Backend Token Verification (Optional)
```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
});
// payload.sub — verified wallet address
// For inline / blink launch tokens, the payload also carries (all optional):
// payload.message_id — unique id of the blink message (stable per-message key)
// payload.params — signed snapshot data
// payload.route, payload.mini_app_id, payload.interactive, payload.source
// The token is in the launch URL query (`/inline?token=...`), so you can verify
// it and render the blink server-side (SSR) — binding state by `message_id`
// before the client mounts. Keep snapshot data inside the signed token's
// `params`; never trust unsigned query fields.
```
### Step 9: Verify Integration
Test both modes:
**Standalone:**
- App works normally with standard wallets
- No Cherry-specific UI appears
**Embedded (in Cherry):**
- Wallet auto-connects
- User/room context available
- signTransaction/signMessage work
- Hidden UI elements don't appear
- Navigation opens Cherry screens
**Web iframe specifically:**
- App loads inside Cherry web (not blocked by `X-Frame-Options` / CSP)
- Browser DevTools show no `Refused to frame` errors
- API calls from within the iframe succeed (check Network tab for CORS errors)
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Using `Buffer` in browser code | Use `btoa()`/`atob()` or SDK helpers |
| `require()` in ESM context | Use top-level `import` |
| Importing `CherryWalletAdapter` from root | Use `from '@cherrydotfun/miniapp-sdk/solana'` |
| Checking `isInsideCherry()` after async init | Call synchronously at module load |
| Not auto-selecting Cherry wallet | Add `AutoSelectCherry` component |
| Showing wallet connect in embed | Wrap with `if (!isEmbedded)` |
| App doesn't load in Cherry web (iframe) | Missing `frame-ancestors https://chat.cherry.fun` CSP header |
| `X-Frame-Options: SAMEORIGIN` blocks embed | Remove or override with CSP `frame-ancestors` |
| `isInsideCherry()` false in Cherry web | Cherry web adds `cherry_embed=1` to URL — verify the param reaches the app; check if SPA strips query params |
| False positive in wallet in-app browser | Enable strict mode: `isInsideCherry({ strict: true })` |
## Privy Integration — Transparent Login
For miniapps using Privy for authentication and embedded wallets, Cherry launch tokens can be used as a **custom auth provider** in Privy. This gives users a zero-click login experience inside Cherry, with standard Privy login (email/social/wallet) as fallback in standalone mode.
### Prerequisites
- Privy account with app created at [console.privy.io](https://console.privy.io)
- Cherry miniapp registered with `wallet:connect` permission
### Step 1: Configure Privy Dashboard
In Privy Dashboard → Settings → Custom Auth:
1. Add a new provider
2. **JWKS URL**: `https://chat.cherry.fun/.well-known/jwks.json`
3. **Issuer**: `https://chat.cherry.fun`
4. **User ID field**: `sub` (this maps to the user's Solana wallet address)
### Step 2: Dual-Mode Login
```tsx
import { CherryMiniAppProvider, useCherryApp, useCherryEnvironment } from '@cherrydotfun/miniapp-sdk/react';
import { usePrivy } from '@privy-io/react-auth';
function AuthGate({ children }: { children: ReactNode }) {
const { isEmbedded } = useCherryEnvironment();
const cherry = useCherryApp();
const { loginWithCustomAccessToken, authenticated, ready } = usePrivy();
useEffect(() => {
if (!ready || authenticated) return;
if (isEmbedded && cherry?.launchToken) {
// Inside Cherry — transparent login via launch token
loginWithCustomAccessToken(cherry.launchToken);
}
// Outside Cherry — Privy shows standard login UI
}, [ready, authenticated, isEmbedded, cherry]);
if (!ready) return ;
if (!authenticated && !isEmbedded) return ;
if (!authenticated) return ; // waiting for custom auth
return <>{children}>;
}
```
### Step 3: Provider Setup
```tsx
import { PrivyProvider } from '@privy-io/react-auth';
import { CherryMiniAppProvider } from '@cherrydotfun/miniapp-sdk/react';
import { isInsideCherry } from '@cherrydotfun/miniapp-sdk';
const embedded = isInsideCherry();
function App() {
return (
{embedded ? (
) : (
)}
);
}
```
### How It Works
| Environment | Login Method | User Action |
|-------------|-------------|-------------|
| Inside Cherry (WebView/iframe) | `loginWithCustomAccessToken(launchToken)` | None — automatic |
| Standalone (browser) | Standard Privy UI (email, social, wallet) | User clicks login |
- Cherry launch token JWT contains `sub` (wallet address), `iss` (cherry.fun), signed RS256
- Privy validates token against Cherry JWKS endpoint
- Privy creates or finds user by `sub` claim
- Embedded wallet auto-provisioned if user is new
- Same user recognized across both login methods (wallet address match)
### Important Notes
- Cherry launch token TTL is 5 minutes — Privy must validate it promptly after injection
- The `sub` claim contains the Solana wallet address, not a Privy DID
- Privy embedded wallet is separate from the Cherry host wallet — choose which to use for transactions
- For wallet operations, you can use either:
- Cherry wallet (via `useCherryWallet()`) — signs through Cherry host
- Privy embedded wallet — signs locally via Privy iframe
- If using Cherry wallet for signing, you don't need Privy embedded wallets at all — Privy becomes auth-only
## Lifecycle Events
```tsx
cherry.on('suspended', () => { /* user left chat — pause game */ });
cherry.on('resumed', () => { /* user came back — resume */ });
cherry.on('walletDisconnected', () => { /* handle gracefully */ });
```