diff --git a/examples/react-vite-browser-sdk/README.md b/examples/react-vite-browser-sdk/README.md
new file mode 100644
index 000000000..3795dcc31
--- /dev/null
+++ b/examples/react-vite-browser-sdk/README.md
@@ -0,0 +1,28 @@
+# React Vite example app
+
+Use this React Vite example app as a tool to start building an app with XMTP. This basic messaging app has an intentionally unopinionated UI to help make it easier for you to build with.
+
+The app is built using the [React XMTP client SDK](/packages/react-sdk/README.md), [React](https://react.dev/), [Vite](https://vitejs.dev/), and [RainbowKit](https://www.rainbowkit.com/).
+
+To keep up with the latest example app developments, see the [Issues tab](https://github.com/xmtp/xmtp-web/issues) in this repo.
+
+To learn more about XMTP and get answers to frequently asked questions, see the [XMTP documentation](https://xmtp.org/docs).
+
+## Limitations
+
+This example app isn't a complete solution. For example, the list of conversations doesn't update when new messages arrive in existing conversations.
+
+## Developing
+
+1. In `packages/react-sdk`, run `yarn build` to build the React SDK.
+2. In `examples/react-vite`, run `yarn dev` to start the development server.
+
+## Useful commands
+
+- `yarn build`: Builds the example app
+- `yarn clean`: Removes `node_modules`, `dist`, and `.turbo` folders
+- `yarn dev`: Launches the example app and watches for changes, which will trigger a rebuild
+- `yarn format`: Runs prettier format and write changes
+- `yarn format:check`: Runs prettier format check
+- `yarn lint`: Runs ESLint
+- `yarn typecheck`: Runs `tsc`
diff --git a/examples/react-vite-browser-sdk/index.html b/examples/react-vite-browser-sdk/index.html
new file mode 100644
index 000000000..31cf5d729
--- /dev/null
+++ b/examples/react-vite-browser-sdk/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ XMTP V3 Browser SDK Example
+
+
+
+
+
+
diff --git a/examples/react-vite-browser-sdk/package.json b/examples/react-vite-browser-sdk/package.json
new file mode 100644
index 000000000..102500c6b
--- /dev/null
+++ b/examples/react-vite-browser-sdk/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "@xmtp/react-vite-browser-sdk-example",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "clean": "rm -rf .turbo && rm -rf node_modules && yarn clean:dist",
+ "clean:dist": "rm -rf dist",
+ "dev": "vite",
+ "quickstart": "yarn dev",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@rainbow-me/rainbowkit": "^2.1.3",
+ "@tanstack/react-query": "^5.51.1",
+ "@wagmi/core": "^2.11.7",
+ "@xmtp/browser-sdk": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "viem": "^2.17.4",
+ "wagmi": "^2.10.10"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "postcss": "^8.4.39",
+ "postcss-preset-env": "^9.6.0",
+ "tsconfig": "workspace:*",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.9"
+ }
+}
diff --git a/examples/react-vite-browser-sdk/postcss.config.cjs b/examples/react-vite-browser-sdk/postcss.config.cjs
new file mode 100644
index 000000000..eaccdea29
--- /dev/null
+++ b/examples/react-vite-browser-sdk/postcss.config.cjs
@@ -0,0 +1,5 @@
+module.exports = {
+ plugins: {
+ "postcss-preset-env": {},
+ },
+};
diff --git a/examples/react-vite-browser-sdk/public/xmtp-icon.png b/examples/react-vite-browser-sdk/public/xmtp-icon.png
new file mode 100644
index 000000000..6c610d7d8
Binary files /dev/null and b/examples/react-vite-browser-sdk/public/xmtp-icon.png differ
diff --git a/examples/react-vite-browser-sdk/src/App.tsx b/examples/react-vite-browser-sdk/src/App.tsx
new file mode 100644
index 000000000..ea6e39b27
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/App.tsx
@@ -0,0 +1,253 @@
+import {
+ Conversation,
+ type Client,
+ type DecodedMessage,
+} from "@xmtp/browser-sdk";
+import { useState } from "react";
+import { createClient } from "./createClient";
+
+export const App = () => {
+ const [client, setClient] = useState(undefined);
+ const [conversations, setConversations] = useState([]);
+ const [messages, setMessages] = useState>(
+ new Map(),
+ );
+
+ const handleCreateClient = async () => {
+ setClient(await createClient("key1"));
+ };
+
+ const handleResetClient = () => {
+ if (client) {
+ client.close();
+ }
+ setClient(undefined);
+ setConversations([]);
+ setMessages(new Map());
+ };
+
+ const handleListGroups = async () => {
+ if (client) {
+ const groups = await client.conversations.list();
+ setConversations(groups);
+ }
+ };
+
+ const handleUpdateGroupName = async (groupId: string, elementId: string) => {
+ if (client) {
+ const conversation = new Conversation(client, groupId);
+ await conversation.sync();
+ const element = document.getElementById(elementId) as HTMLInputElement;
+ const name = element.value;
+ await conversation.updateName(name);
+ element.value = "";
+ await handleListGroups();
+ }
+ };
+
+ const handleUpdateGroupDescription = async (
+ groupId: string,
+ elementId: string,
+ ) => {
+ if (client) {
+ const conversation = new Conversation(client, groupId);
+ await conversation.sync();
+ const element = document.getElementById(elementId) as HTMLInputElement;
+ const description = element.value;
+ await conversation.updateDescription(description);
+ element.value = "";
+ await handleListGroups();
+ }
+ };
+
+ const handleListGroupMessages = async (groupId: string) => {
+ if (client) {
+ const conversation = new Conversation(client, groupId);
+ await conversation.sync();
+ const groupMessages = await conversation.messages();
+ setMessages((prevMessages) => {
+ const newMessages = new Map(prevMessages);
+ newMessages.set(groupId, groupMessages);
+ return newMessages;
+ });
+ }
+ };
+
+ const handleSendGroupMessage = async (groupId: string, elementId: string) => {
+ if (client) {
+ const conversation = new Conversation(client, groupId);
+ await conversation.sync();
+ const element = document.getElementById(elementId) as HTMLInputElement;
+ const message = element.value;
+ await conversation.send(message);
+ element.value = "";
+ }
+ };
+
+ const handleCreateGroup = async () => {
+ if (client) {
+ const element = document.getElementById(
+ "create-group-name",
+ ) as HTMLInputElement;
+ const name = element.value;
+ const group = await client.conversations.newGroup([]);
+ await group.sync();
+ await group.updateName(name);
+ element.value = "";
+ await handleListGroups();
+ }
+ };
+
+ const handleSyncGroup = async (groupId: string) => {
+ if (client) {
+ const conversation = new Conversation(client, groupId);
+ await conversation.sync();
+ await handleListGroupMessages(groupId);
+ }
+ };
+
+ return (
+
+
XMTP V3
+
+ {!client && (
+ void handleCreateClient()} type="button">
+ Create client
+
+ )}
+ {client && (
+ <>
+ {
+ handleResetClient();
+ }}
+ type="button">
+ Reset client
+
+ void handleListGroups()} type="button">
+ List groups
+
+ >
+ )}
+
+ {client && (
+ <>
+
+
Client details
+
+
Address:
+
{client.address}
+
+
+
Inbox ID:
+
{client.inboxId}
+
+
+
Installation ID:
+
{client.installationId}
+
+
+
+
+
+ void handleCreateGroup()} type="button">
+ Create group
+
+
+
+ >
+ )}
+ {conversations.length > 0 && (
+
+
Conversations
+
+ {conversations.map((conversation) => (
+
+
{conversation.id}
+
+
+
+
+ void handleUpdateGroupName(
+ conversation.id,
+ `group-name-${conversation.id}`,
+ )
+ }
+ type="button">
+ Update group name
+
+
+
+
+
+ void handleUpdateGroupDescription(
+ conversation.id,
+ `group-description-${conversation.id}`,
+ )
+ }
+ type="button">
+ Update group description
+
+
+
+ void handleSyncGroup(conversation.id)}
+ type="button">
+ Sync group
+
+
+ void handleListGroupMessages(conversation.id)
+ }
+ type="button">
+ List messages
+
+
+
+
+
+ void handleSendGroupMessage(
+ conversation.id,
+ `group-send-message-${conversation.id}`,
+ )
+ }
+ type="button">
+ Send message
+
+
+
+
+
Name:
+
{conversation.name}
+
+
+
Description:
+
{conversation.description}
+
+ {messages.get(conversation.id) && (
+
+
Messages
+ {messages.get(conversation.id)?.map((message) => (
+
+
{JSON.stringify(message.content, null, 2)}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/examples/react-vite-browser-sdk/src/createClient.ts b/examples/react-vite-browser-sdk/src/createClient.ts
new file mode 100644
index 000000000..c9b362f5b
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/createClient.ts
@@ -0,0 +1,35 @@
+import { Client, WasmSignatureRequestType } from "@xmtp/browser-sdk";
+import { toBytes } from "viem/utils";
+import { createWallet } from "./wallets";
+
+type Wallet = ReturnType;
+
+export const getSignature = async (client: Client, wallet: Wallet) => {
+ const signatureText = await client.getCreateInboxSignatureText();
+ if (signatureText) {
+ const signature = await wallet.signMessage({
+ message: signatureText,
+ });
+ return toBytes(signature);
+ }
+ return null;
+};
+
+export const createClient = async (walletKey: string) => {
+ const wallet = createWallet(walletKey);
+ const client = await Client.create(wallet.account.address, {
+ env: "local",
+ });
+ const isRegistered = await client.isRegistered();
+ if (!isRegistered) {
+ const signature = await getSignature(client, wallet);
+ if (signature) {
+ await client.addSignature(
+ WasmSignatureRequestType.CreateInbox,
+ signature,
+ );
+ }
+ await client.registerIdentity();
+ }
+ return client;
+};
diff --git a/examples/react-vite-browser-sdk/src/globals.d.ts b/examples/react-vite-browser-sdk/src/globals.d.ts
new file mode 100644
index 000000000..5b948c9b6
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/globals.d.ts
@@ -0,0 +1,15 @@
+interface ImportMeta {
+ env: {
+ VITE_PROJECT_ID: string;
+ };
+}
+
+declare module "*.module.css" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+
+declare module "*.png" {
+ const src: string;
+ export default src;
+}
diff --git a/examples/react-vite-browser-sdk/src/index.css b/examples/react-vite-browser-sdk/src/index.css
new file mode 100644
index 000000000..cf988d17b
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/index.css
@@ -0,0 +1,111 @@
+:root {
+ font-family: "SF Pro Rounded", Inter, system-ui, Avenir, Helvetica, Arial,
+ sans-serif;
+ line-height: 1.3;
+ font-weight: 400;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ color: #213547;
+ background-color: #fff;
+}
+
+#root {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+#root > [data-rk] {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.App {
+ width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.App h1 {
+ text-align: center;
+ margin-top: 0;
+}
+
+.Actions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ padding: 20px;
+}
+
+.ClientDetail {
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+}
+
+.ConversationActions {
+ display: flex;
+ gap: 10px;
+ justify-content: center;
+ align-items: center;
+ padding: 10px;
+}
+
+.ConversationAction {
+ padding: 6px;
+ border: 1px solid #213547;
+ border-radius: 6px;
+ background-color: #f0f0f0;
+ display: flex;
+ gap: 2px;
+ flex-direction: column;
+}
+
+.ConversationWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.Conversation {
+ border: 1px solid #213547;
+ border-radius: 10px;
+ padding: 10px;
+}
+
+.Conversation > h3 {
+ margin-top: 0;
+}
+
+.ConversationDetail {
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+}
+
+.ConversationMessages {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.ConversationMessage {
+ border: 1px solid #213547;
+ border-radius: 6px;
+ padding: 4px;
+ background-color: #f0f0f0;
+ text-wrap: pretty;
+}
diff --git a/examples/react-vite-browser-sdk/src/main.tsx b/examples/react-vite-browser-sdk/src/main.tsx
new file mode 100644
index 000000000..c4833794e
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/main.tsx
@@ -0,0 +1,33 @@
+import "@rainbow-me/rainbowkit/styles.css";
+import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { http } from "@wagmi/core";
+import { mainnet } from "@wagmi/core/chains";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { WagmiProvider } from "wagmi";
+import { App } from "./App";
+import "./index.css";
+
+export const config = getDefaultConfig({
+ appName: "XMTP V3 Browser SDK Example",
+ projectId: import.meta.env.VITE_PROJECT_ID,
+ chains: [mainnet],
+ transports: {
+ [mainnet.id]: http(),
+ },
+});
+
+const queryClient = new QueryClient();
+
+createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+
+
+
+
+
+
+ ,
+);
diff --git a/examples/react-vite-browser-sdk/src/wallets.ts b/examples/react-vite-browser-sdk/src/wallets.ts
new file mode 100644
index 000000000..a2b438a7c
--- /dev/null
+++ b/examples/react-vite-browser-sdk/src/wallets.ts
@@ -0,0 +1,31 @@
+import {
+ createWalletClient,
+ http,
+ type PrivateKeyAccount,
+ type Transport,
+ type WalletClient,
+} from "viem";
+import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
+import { mainnet } from "viem/chains";
+
+const keys: Record = {
+ // address: 0xb879f1d8FD73EC057c02D681880169e5721a6d7F
+ key1: "0xf8ced372cdb9a67bed1843650a89a59859369bf9900c0bc75741f2740e93cb04",
+ // address: 0x7ad9d3892D5EEC0586920D3E0ac813aaeF881488
+ key2: "0xb562d61bc9fe203a639dfc0c3f875b3411fe8ae211c5722ab9124a1009bda32a",
+ // address: 0x73655B77df59d378d396918C3426cc5219EfB3c8
+ key3: "0x724028dcbf931ff1f2730ad76c0b7b8b07dbf7f0a56408be3e305be1b81edfe0",
+ // address: 0x5aB557A6b8FF7D7a9A42F223fAA376A4732Eb15a
+ key4: "0x4420cde3d475a038739d1d47cfd690799c0f2e1b84d871c24f221c2dee4e4121",
+ // address: 0x38F966794cf349f2c91116e94f587Fc3aafDC3F4
+ key5: "0xd34cc37587785349013f3f10cadbe7bf8dfeb8a95c86724887e58816b734fcfb",
+};
+
+export const createWallet = (
+ key: keyof typeof keys,
+): WalletClient =>
+ createWalletClient({
+ account: privateKeyToAccount(keys[key] ?? generatePrivateKey()),
+ chain: mainnet,
+ transport: http(),
+ });
diff --git a/examples/react-vite-browser-sdk/tsconfig.json b/examples/react-vite-browser-sdk/tsconfig.json
new file mode 100644
index 000000000..ce1c7e0fd
--- /dev/null
+++ b/examples/react-vite-browser-sdk/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "tsconfig/react-app.json",
+ "include": ["src", "vite.config.ts", "postcss.config.cjs"]
+}
diff --git a/examples/react-vite-browser-sdk/vite.config.ts b/examples/react-vite-browser-sdk/vite.config.ts
new file mode 100644
index 000000000..3d7f96fe2
--- /dev/null
+++ b/examples/react-vite-browser-sdk/vite.config.ts
@@ -0,0 +1,16 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ optimizeDeps: {
+ exclude: ["@xmtp/wasm-bindings"],
+ },
+ server: {
+ headers: {
+ "Cross-Origin-Embedder-Policy": "require-corp",
+ "Cross-Origin-Opener-Policy": "same-origin",
+ },
+ },
+});