Skip to content

Commit

Permalink
Add Browser SDK example
Browse files Browse the repository at this point in the history
  • Loading branch information
rygine committed Oct 28, 2024
1 parent c9faee9 commit 54632f1
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 0 deletions.
28 changes: 28 additions & 0 deletions examples/react-vite-browser-sdk/README.md
Original file line number Diff line number Diff line change
@@ -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`
12 changes: 12 additions & 0 deletions examples/react-vite-browser-sdk/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XMTP V3 Browser SDK Example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions examples/react-vite-browser-sdk/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
5 changes: 5 additions & 0 deletions examples/react-vite-browser-sdk/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"postcss-preset-env": {},
},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
253 changes: 253 additions & 0 deletions examples/react-vite-browser-sdk/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<Client | undefined>(undefined);
const [conversations, setConversations] = useState<Conversation[]>([]);
const [messages, setMessages] = useState<Map<string, DecodedMessage[]>>(
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 (
<div className="App">
<h1>XMTP V3</h1>
<div className="Actions">
{!client && (
<button onClick={() => void handleCreateClient()} type="button">
Create client
</button>
)}
{client && (
<>
<button
onClick={() => {
handleResetClient();
}}
type="button">
Reset client
</button>
<button onClick={() => void handleListGroups()} type="button">
List groups
</button>
</>
)}
</div>
{client && (
<>
<div className="Client">
<h2>Client details</h2>
<div className="ClientDetail">
<div>Address:</div>
<div>{client.address}</div>
</div>
<div className="ClientDetail">
<div>Inbox ID:</div>
<div>{client.inboxId}</div>
</div>
<div className="ClientDetail">
<div>Installation ID:</div>
<div>{client.installationId}</div>
</div>
</div>
<div className="ConversationActions">
<div className="ConversationAction">
<input id="create-group-name" type="text" />
<button onClick={() => void handleCreateGroup()} type="button">
Create group
</button>
</div>
</div>
</>
)}
{conversations.length > 0 && (
<div className="Conversations">
<h2>Conversations</h2>
<div className="ConversationWrapper">
{conversations.map((conversation) => (
<div className="Conversation" key={conversation.id}>
<h3>{conversation.id}</h3>
<div className="ConversationActions">
<div className="ConversationAction">
<input id={`group-name-${conversation.id}`} type="text" />
<button
onClick={() =>
void handleUpdateGroupName(
conversation.id,
`group-name-${conversation.id}`,
)
}
type="button">
Update group name
</button>
</div>
<div className="ConversationAction">
<input
id={`group-description-${conversation.id}`}
type="text"
/>
<button
onClick={() =>
void handleUpdateGroupDescription(
conversation.id,
`group-description-${conversation.id}`,
)
}
type="button">
Update group description
</button>
</div>
<div className="ConversationAction">
<button
onClick={() => void handleSyncGroup(conversation.id)}
type="button">
Sync group
</button>
<button
onClick={() =>
void handleListGroupMessages(conversation.id)
}
type="button">
List messages
</button>
</div>
<div className="ConversationAction">
<input
id={`group-send-message-${conversation.id}`}
type="text"
/>
<button
onClick={() =>
void handleSendGroupMessage(
conversation.id,
`group-send-message-${conversation.id}`,
)
}
type="button">
Send message
</button>
</div>
</div>
<div className="ConversationDetail">
<div>Name:</div>
<div>{conversation.name}</div>
</div>
<div className="ConversationDetail">
<div>Description:</div>
<div>{conversation.description}</div>
</div>
{messages.get(conversation.id) && (
<div className="ConversationMessages">
<h3>Messages</h3>
{messages.get(conversation.id)?.map((message) => (
<div className="ConversationMessage" key={message.id}>
<pre>{JSON.stringify(message.content, null, 2)}</pre>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
);
};
35 changes: 35 additions & 0 deletions examples/react-vite-browser-sdk/src/createClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Client, WasmSignatureRequestType } from "@xmtp/browser-sdk";
import { toBytes } from "viem/utils";
import { createWallet } from "./wallets";

type Wallet = ReturnType<typeof createWallet>;

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;
};
15 changes: 15 additions & 0 deletions examples/react-vite-browser-sdk/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 54632f1

Please sign in to comment.