Chat SDK React Native and Flutter with wallet auth

Run Cherry chat inside a React Native or Flutter app: a WebView host page, native wallet signing on Android and iOS, and runnable examples.

Cherry chat runs inside a React Native or Flutter app through a WebView, with the wallet signature handled by the native layer. There is no native chat SDK React Native and Flutter can import directly: the Chat Embed SDK builds an iframe and talks to it over browser message events, and a mobile JS runtime has neither. Cherry is a wallet-to-wallet messenger and community app for crypto, and the Chat Embed SDK drops one of its rooms into your product with wallet auth, theming, unread badges and moderation already in the widget. This guide is for a mobile developer who wants that room on a screen, signed by the user’s real wallet.

Before you start

You need five things in place.

  • An embed, created self-serve in Cherry Portal: sign in with your wallet, create a Project, open Chat embeds and make a New embed. The embed ID it gives you is your appId.
  • Your host page origin listed under the embed’s Allowed origins. localhost is always allowed while you develop.
  • A room id: any public Cherry room, or one your backend creates for a match, order or community.
  • A WebView package, and the signing dependencies for the wallet-backed modes.
  • A decision on the auth mode, which is what the next step is about.
npm install react-native-webview                                    # React Native
npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js @solana/web3.js

flutter pub add webview_flutter solana_mobile_client app_links      # Flutter

How the mobile embed fits together

Four layers, each talking only to its neighbour.

Native app (wallet: MWA on Android / deeplink on iOS)
   |  inject JS  <->  message channel
   v
WebView -> host page (runs the Chat Embed SDK, creates the iframe)
   v
iframe -> embed.cherry.fun (the chat)

The host page is a few lines of HTML that load the SDK bundle and construct the embed. Its sign handler signs nothing itself: it forwards the challenge bytes down to native and waits for the signature to come back. The native wrapper sends config up, catches sign requests, and injects results back.

Three walls break a naive port, in the order people hit them. The SDK reaches for document and an iframe, so importing it into React Native and calling mount() throws. The iframe-side bridge refuses to start signing when the embed is the top-level document, so pointing the WebView straight at embed.cherry.fun produces a chat that never authenticates. And a mobile WebView has no injected browser wallet, so the signature has to cross into native code.

Step 1: Create the embed and choose the auth mode

Three modes ship, and the mobile rule for the wallet-backed two is the same: never lean on the iframe’s own web wallet adapter, drive signing through the bridge.

ModeBackendWhat the app passes
wallet-onlynoneappId, roomId, walletAddress, a sign callback
app-trusted+walletmints a short-lived embed tokenthe token as well as the wallet address and the sign callback
app-trustedmints the token, owns the identitythe token alone

app-trusted deletes the entire wallet layer from a mobile integration: no signing dependencies, no Android or iOS setup for wallet intents or URL schemes, no sign callback, and no wallet address in the app at all, because the token carries it. The WebView host page is still required, because the SDK is still browser-only.

You know it worked when the embed shows as enabled in the portal with your origin listed.

Step 2: Ship the host page

One host page serves React Native and Flutter alike: it detects which bridge it is sitting on. You can deploy it or bundle it.

ConcernHostedBundled
Where the page livesyour web servera string inside the app binary
WebView sourcea URLraw HTML
Changing the pageredeploy web, no app releaseship an app release
Page originyour https originnone, so the allowed-origins entry is null

Either way the SDK bundle and the chat iframe come over the network. The bundle is the SDK’s IIFE build, about 10 KB, and Cherry serves a rolling copy at https://embed.cherry.fun/cherry-embed.js from the same origin as the chat, so there is nothing to host yourself unless you want a frozen filename on your own CDN.

You know it worked when the WebView reports the page ready and your native code answers with the config.

Step 3: Wire the React Native WebView

The example component wraps react-native-webview with the handshake, the config push, event forwarding and the sign relay. Copy it in and render it.

import { CherryChatWebView, type CherryChatWebViewRef } from './CherryChatWebView';
import { connectWallet, signMessageWithWallet } from './wallet';

function ChatScreen() {
  const chatRef = useRef<CherryChatWebViewRef>(null);
  const [walletAddress, setWalletAddress] = useState<string>();

  return (
    <CherryChatWebView
      ref={chatRef}
      source={{ uri: 'https://yoursite.com/cherry-host.html' }}
      config={{
        appId: 'YOUR_EMBED_ID',
        roomId: 'YOUR_ROOM_ID',
        walletAddress,
        theme: { mode: 'dark', primaryColor: '#FF5BA8' },
      }}
      onSign={signMessageWithWallet}
      onWalletConnectRequested={async () => {
        const address = await connectWallet();
        setWalletAddress(address);
      }}
      onEvent={(event, data) => {
        if (event === 'authStateChange') console.log('authenticated:', data);
      }}
    />
  );
}

Setting the wallet address re-sends the config, and the iframe starts the challenge. For the bundled variant, build the page as a string with the helper in the same example and pass it as source={{ html }}.

You know it worked when authStateChange arrives with true and the room renders messages.

Step 4: Wire the Flutter WebView

Same host page, same messages. Only the plumbing changes: webview_flutter replaces react-native-webview, runJavaScript replaces injectJavaScript, and a JavaScriptChannel replaces onMessage.

CherryChatWebView(
  source: CherryChatSource.url('https://yoursite.com/cherry-host.html'),
  config: CherryChatConfig(
    appId: 'YOUR_EMBED_ID',
    roomId: 'YOUR_ROOM_ID',
    walletAddress: walletAddress,
  ),
  onSign: signMessageWithWallet,
  onWalletConnectRequested: connect,
  onEvent: (event, data) {
    if (event == 'authStateChange') debugPrint('authenticated: $data');
  },
)

Three Flutter rules decide whether this works at all. Name the channel exactly CherryNative, because the shared page tries the React Native bridge first and that name second. Turn on JavaScriptMode.unrestricted. And when you load bundled HTML, pass a baseUrl so the null-origin document on iOS is allowed to fetch the remote SDK script; the SDK origin is the sensible value.

You know it worked when the widget renders the room and the first challenge reaches your wallet app.

Step 5: Sign with the native wallet

The native layer owes the host page two functions. connectWallet() resolves to the base58 public key. signMessageWithWallet(bytes) resolves to the raw 64-byte Ed25519 signature over the challenge bytes as they arrived, with no re-hashing, no prefix and no re-encoding, because the server verifies exactly that.

Android signs through Mobile Wallet Adapter, which may hand back the message with the signature appended, so slice the last 64 bytes when the result is longer than 64. iOS signs through a deeplink wallet: the app switches away, the wallet returns a base58 signature, and your universal-link handler resolves the pending promise. The bridge fails a challenge after 60 seconds, which is generous for an app switch and unforgiving if you park your own confirmation dialog in front of it.

Note: In app-trusted mode this step disappears. Fetch the token from your backend right before the chat is shown, pass it in the config, and the sign callback never fires.

You know it worked when the room accepts a message you type from the device.

Troubleshooting

  • Chat renders, signing never starts: the WebView is loading the embed as its top document. Put the host page back in front of it.
  • The widget ignores commands from native: the host page origin is missing from Allowed origins. A bundled page has no origin, so the entry is null.
  • The session is lost between screens: DOM storage is disabled in the WebView. The chat session lives in the iframe’s storage.
  • The signature is rejected: something re-hashed or prefixed the challenge, or an Android result longer than 64 bytes was passed through whole.
  • A 403 or a 429 in app-trusted mode: that is the mode’s room allowlist and message rate limits answering, not a broken integration.
  • Flutter gets no messages from the page: the channel is named something other than CherryNative.

FAQ

Is there a chat SDK for React Native? Not as a native package. The Chat Embed SDK is browser-only, so in React Native you run it inside a WebView on a small host page and bridge wallet signing to the native layer. Cherry publishes a runnable React Native example that implements the whole bridge.

How do I embed chat in a Flutter app? Load the same host page in a webview_flutter WebView, name the JavaScript channel CherryNative, and wire the native wallet with Mobile Wallet Adapter on Android and a deeplink wallet on iOS. The architecture and the bridge messages match the React Native ones; only the WebView plumbing differs.

Is there a Cherry SDK for Unity? No. Cherry publishes the Chat Embed SDK for web pages and the Mini App SDK for apps that run inside Cherry chats, and neither ships a Unity package. A Unity game can still create and drive Cherry rooms from its own backend through the Cherry API.

Do I need a backend for chat in a mobile app? No, in wallet-only mode. The app passes an embed ID, a room id and a wallet address, and the user signs the challenge with their own wallet. A backend is needed only for the token modes, where it mints a short-lived embed token for the connected wallet.

What does the Chat Embed SDK cost? Free. The Chat Embed SDK, the Mini App SDK, the Cherry API, bots and Cherry Portal are free to use.

The full mobile write-up, with the bridge message tables and the Android and iOS setup, lives at portal.cherry.fun/docs/embed/mobile .

Sources

Build on Cherry

Add chat to your dApp, ship a mini app, or run bots and rooms through the API. Self-serve at the developer portal.