Skip to content

Commit bb20c71

Browse files
authored
Fix double client init (#873)
1 parent 6df9e8a commit bb20c71

17 files changed

+137
-142
lines changed

apps/xmtp.chat/src/components/App/Actions.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { Button, Flex, useMatches } from "@mantine/core";
22
import { useEffect, useRef } from "react";
33
import { useNavigate } from "react-router";
44
import { useRefManager } from "@/contexts/RefManager";
5-
import { useClient } from "@/hooks/useClient";
5+
import { useXMTP } from "@/contexts/XMTPContext";
66
import { IconMessagePlus } from "@/icons/IconMessagePlus";
77

88
export const Actions: React.FC = () => {
9-
const { client } = useClient();
9+
const { client } = useXMTP();
1010
const navigate = useNavigate();
1111
const { setRef } = useRefManager();
1212
const ref = useRef<HTMLButtonElement>(null);

apps/xmtp.chat/src/components/App/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import { AppFooter } from "@/components/App/AppFooter";
88
import { AppHeader } from "@/components/App/AppHeader";
99
import { ErrorModal } from "@/components/App/ErrorModal";
1010
import { useAppState } from "@/contexts/AppState";
11+
import { useXMTP } from "@/contexts/XMTPContext";
1112
import { createEphemeralSigner } from "@/helpers/createSigner";
1213
import { useAnalytics } from "@/hooks/useAnalytics";
13-
import { useClient } from "@/hooks/useClient";
1414
import { useRedirects } from "@/hooks/useRedirects";
1515
import { Main } from "@/routes/Main";
1616
import { Navbar } from "@/routes/Navbar";
@@ -20,7 +20,7 @@ export const App: React.FC = () => {
2020
useRedirects();
2121
useAnalytics();
2222
const [opened, { toggle }] = useDisclosure();
23-
const { initialize, initializing } = useClient();
23+
const { initialize, initializing } = useXMTP();
2424
const [env] = useLocalStorage<XmtpEnv>({
2525
key: "XMTP_NETWORK",
2626
defaultValue: "dev",

apps/xmtp.chat/src/components/App/AppHeader.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Burger, Button, Flex, Skeleton } from "@mantine/core";
22
import { useEffect, useState } from "react";
33
import { useNavigate } from "react-router";
4+
import { useXMTP } from "@/contexts/XMTPContext";
45
import { shortAddress } from "@/helpers/address";
5-
import { useClient } from "@/hooks/useClient";
66
import { Actions } from "./Actions";
77
import classes from "./AppHeader.module.css";
88
import { Connection } from "./Connection";
@@ -18,7 +18,7 @@ export const AppHeader: React.FC<AppHeaderProps> = ({
1818
opened,
1919
toggle,
2020
}) => {
21-
const { client } = useClient();
21+
const { client } = useXMTP();
2222
const navigate = useNavigate();
2323
const [accountIdentifier, setAccountIdentifier] = useState<string | null>(
2424
null,

apps/xmtp.chat/src/components/App/Connection.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import { useConnect, useDisconnect, useWalletClient } from "wagmi";
99
import { injected } from "wagmi/connectors";
1010
import { Settings } from "@/components/Settings/Settings";
1111
import { useRefManager } from "@/contexts/RefManager";
12+
import { useXMTP } from "@/contexts/XMTPContext";
1213
import { createEphemeralSigner, createSigner } from "@/helpers/createSigner";
13-
import { useClient } from "@/hooks/useClient";
1414
import { IconLogout } from "@/icons/IconLogout";
1515
import { IconUser } from "@/icons/IconUser";
1616

1717
export const Disconnect: React.FC = () => {
1818
const { disconnect } = useDisconnect();
19-
const { disconnect: disconnectClient } = useClient();
19+
const { disconnect: disconnectClient } = useXMTP();
2020
const label: React.ReactNode = useMatches({
2121
base: <IconLogout size={24} />,
2222
sm: "Disconnect",
@@ -43,7 +43,7 @@ export const Disconnect: React.FC = () => {
4343
export const Connect: React.FC = () => {
4444
const { setRef } = useRefManager();
4545
const ref = useRef<HTMLButtonElement>(null);
46-
const { initialize, initializing } = useClient();
46+
const { initialize, initializing } = useXMTP();
4747
const [encryptionKey] = useLocalStorage({
4848
key: "XMTP_ENCRYPTION_KEY",
4949
defaultValue: uint8ArrayToHex(crypto.getRandomValues(new Uint8Array(32))),
@@ -125,7 +125,7 @@ export const Connect: React.FC = () => {
125125
};
126126

127127
export const Connection: React.FC = () => {
128-
const { client } = useClient();
128+
const { client } = useXMTP();
129129
return (
130130
<Flex align="center" gap="xs" ml="auto">
131131
{!client && <Connect />}

apps/xmtp.chat/src/components/Conversation/LoadDM.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { useNavigate, useParams, useSearchParams } from "react-router";
66
import { LoadingMessage } from "@/components/LoadingMessage";
77
import { useAppState } from "@/contexts/AppState";
88
import { useRefManager } from "@/contexts/RefManager";
9+
import { useXMTP } from "@/contexts/XMTPContext";
910
import { useBodyClass } from "@/hooks/useBodyClass";
10-
import { useClient } from "@/hooks/useClient";
1111

1212
export const LoadDM: React.FC = () => {
1313
useBodyClass("main-flex-layout");
@@ -23,7 +23,7 @@ export const LoadDM: React.FC = () => {
2323
});
2424
const { address } = useParams();
2525
const [searchParams] = useSearchParams();
26-
const { client } = useClient();
26+
const { client } = useXMTP();
2727
const navigate = useNavigate();
2828

2929
useEffect(() => {

apps/xmtp.chat/src/components/Conversation/ManageGroup.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import {
2727
} from "@xmtp/browser-sdk";
2828
import { useEffect, useMemo, useRef, useState } from "react";
2929
import { useNavigate } from "react-router";
30+
import { useXMTP } from "@/contexts/XMTPContext";
3031
import { isValidLongWalletAddress } from "@/helpers/address";
3132
import { useBodyClass } from "@/hooks/useBodyClass";
32-
import { useClient } from "@/hooks/useClient";
3333
import { BadgeWithCopy } from "../BadgeWithCopy";
3434

3535
type AnyFn = (...args: unknown[]) => unknown;
@@ -66,7 +66,7 @@ export type ManageGroupProps = {
6666

6767
export const ManageGroup: React.FC<ManageGroupProps> = ({ group }) => {
6868
useBodyClass("main-flex-layout");
69-
const { client } = useClient();
69+
const { client } = useXMTP();
7070
const navigate = useNavigate();
7171
const [isLoading, setIsLoading] = useState(false);
7272
const [address, setAddress] = useState("");

apps/xmtp.chat/src/components/Identity/Identity.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ import {
1313
import { useEffect, useState } from "react";
1414
import { BadgeWithCopy } from "@/components/BadgeWithCopy";
1515
import { useAppState } from "@/contexts/AppState";
16+
import { useXMTP } from "@/contexts/XMTPContext";
1617
import { useBodyClass } from "@/hooks/useBodyClass";
17-
import { useClient } from "@/hooks/useClient";
1818
import { useIdentity } from "@/hooks/useIdentity";
1919
import { InstallationTable } from "./InstallationTable";
2020

2121
export const Identity: React.FC = () => {
2222
useBodyClass("main-flex-layout");
2323
const { setNavbar } = useAppState();
24-
const { client } = useClient();
24+
const { client } = useXMTP();
2525
const {
2626
installations,
2727
revokeAllOtherInstallations,

apps/xmtp.chat/src/components/Messages/Message.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { Box, Flex, Paper, Stack, Text } from "@mantine/core";
22
import type { DecodedMessage } from "@xmtp/browser-sdk";
33
import { intlFormat } from "date-fns";
44
import { useNavigate } from "react-router";
5+
import { useXMTP } from "@/contexts/XMTPContext";
56
import { shortAddress } from "@/helpers/address";
67
import { nsToDate } from "@/helpers/date";
7-
import { useClient } from "@/hooks/useClient";
88
import classes from "./Message.module.css";
99
import { MessageContent } from "./MessageContent";
1010

@@ -13,7 +13,7 @@ export type MessageProps = {
1313
};
1414

1515
export const Message: React.FC<MessageProps> = ({ message }) => {
16-
const { client } = useClient();
16+
const { client } = useXMTP();
1717
const isSender = client?.inboxId === message.senderInboxId;
1818
const align = isSender ? "right" : "left";
1919
const navigate = useNavigate();

apps/xmtp.chat/src/components/Settings/LoggingSelect.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { Flex, NativeSelect, Text } from "@mantine/core";
22
import { useLocalStorage } from "@mantine/hooks";
33
import { type ClientOptions } from "@xmtp/browser-sdk";
44
import { useDisconnect } from "wagmi";
5-
import { useClient } from "@/hooks/useClient";
5+
import { useXMTP } from "@/contexts/XMTPContext";
66

77
export const LoggingSelect: React.FC = () => {
88
const { disconnect } = useDisconnect();
9-
const { disconnect: disconnectClient } = useClient();
9+
const { disconnect: disconnectClient } = useXMTP();
1010
const [logging, setLogging] = useLocalStorage<ClientOptions["loggingLevel"]>({
1111
key: "XMTP_LOGGING_LEVEL",
1212
defaultValue: "off",

apps/xmtp.chat/src/components/Settings/NetworkSelect.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { Flex, NativeSelect, Text, Tooltip } from "@mantine/core";
22
import { useLocalStorage } from "@mantine/hooks";
33
import { ApiUrls, type XmtpEnv } from "@xmtp/browser-sdk";
44
import { useDisconnect } from "wagmi";
5-
import { useClient } from "@/hooks/useClient";
5+
import { useXMTP } from "@/contexts/XMTPContext";
66

77
export const NetworkSelect: React.FC = () => {
88
const { disconnect } = useDisconnect();
9-
const { disconnect: disconnectClient } = useClient();
9+
const { disconnect: disconnectClient } = useXMTP();
1010
const [network, setNetwork] = useLocalStorage<XmtpEnv>({
1111
key: "XMTP_NETWORK",
1212
defaultValue: "dev",

apps/xmtp.chat/src/components/Settings/UseEphemeralAccountOption.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Box, Flex, Switch, Text, Tooltip } from "@mantine/core";
22
import { useLocalStorage } from "@mantine/hooks";
33
import React from "react";
44
import { useDisconnect } from "wagmi";
5-
import { useClient } from "@/hooks/useClient";
5+
import { useXMTP } from "@/contexts/XMTPContext";
66
import { IconInfoCircle } from "@/icons/IconInfoCircle";
77

88
const UseEphemeralAccountLabel = () => {
@@ -25,7 +25,7 @@ const UseEphemeralAccountLabel = () => {
2525

2626
export const UseEphemeralAccountOption: React.FC = () => {
2727
const { disconnect } = useDisconnect();
28-
const { disconnect: disconnectClient } = useClient();
28+
const { disconnect: disconnectClient } = useXMTP();
2929
const [checked, setChecked] = useLocalStorage({
3030
key: "XMTP_USE_EPHEMERAL_ACCOUNT",
3131
defaultValue: false,
+104-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
import type { Client } from "@xmtp/browser-sdk";
2-
import { createContext, useMemo, useState } from "react";
1+
import { Client, type ClientOptions, type Signer } from "@xmtp/browser-sdk";
2+
import { ReactionCodec } from "@xmtp/content-type-reaction";
3+
import { RemoteAttachmentCodec } from "@xmtp/content-type-remote-attachment";
4+
import { ReplyCodec } from "@xmtp/content-type-reply";
5+
import {
6+
createContext,
7+
useCallback,
8+
useContext,
9+
useMemo,
10+
useRef,
11+
useState,
12+
} from "react";
13+
14+
export type InitializeClientOptions = {
15+
encryptionKey: Uint8Array;
16+
env?: ClientOptions["env"];
17+
loggingLevel?: ClientOptions["loggingLevel"];
18+
signer: Signer;
19+
};
320

421
export type XMTPContextValue = {
522
/**
@@ -10,15 +27,18 @@ export type XMTPContextValue = {
1027
* Set the XMTP client instance
1128
*/
1229
setClient: React.Dispatch<React.SetStateAction<Client | undefined>>;
13-
/**
14-
* Set whether to show confetti
15-
*/
16-
setConfetti: React.Dispatch<React.SetStateAction<boolean>>;
30+
initialize: (options: InitializeClientOptions) => Promise<Client | undefined>;
31+
initializing: boolean;
32+
error: Error | null;
33+
disconnect: () => void;
1734
};
1835

1936
export const XMTPContext = createContext<XMTPContextValue>({
2037
setClient: () => {},
21-
setConfetti: () => {},
38+
initialize: () => Promise.reject(new Error("XMTPProvider not available")),
39+
initializing: false,
40+
error: null,
41+
disconnect: () => {},
2242
});
2343

2444
export type XMTPProviderProps = React.PropsWithChildren & {
@@ -33,18 +53,91 @@ export const XMTPProvider: React.FC<XMTPProviderProps> = ({
3353
client: initialClient,
3454
}) => {
3555
const [client, setClient] = useState<Client | undefined>(initialClient);
36-
const [confetti, setConfetti] = useState(false);
56+
57+
const [initializing, setInitializing] = useState(false);
58+
const [error, setError] = useState<Error | null>(null);
59+
// client is initializing
60+
const initializingRef = useRef(false);
61+
62+
/**
63+
* Initialize an XMTP client
64+
*/
65+
const initialize = useCallback(
66+
async ({
67+
encryptionKey,
68+
env,
69+
loggingLevel,
70+
signer,
71+
}: InitializeClientOptions) => {
72+
// only initialize a client if one doesn't already exist
73+
if (!client) {
74+
// if the client is already initializing, don't do anything
75+
if (initializingRef.current) {
76+
return undefined;
77+
}
78+
79+
// flag the client as initializing
80+
initializingRef.current = true;
81+
82+
// reset error state
83+
setError(null);
84+
// reset initializing state
85+
setInitializing(true);
86+
87+
let xmtpClient: Client;
88+
89+
try {
90+
// create a new XMTP client
91+
xmtpClient = await Client.create(signer, encryptionKey, {
92+
env,
93+
loggingLevel,
94+
codecs: [
95+
new ReactionCodec(),
96+
new ReplyCodec(),
97+
new RemoteAttachmentCodec(),
98+
],
99+
});
100+
setClient(xmtpClient);
101+
} catch (e) {
102+
setClient(undefined);
103+
setError(e as Error);
104+
// re-throw error for upstream consumption
105+
throw e;
106+
} finally {
107+
initializingRef.current = false;
108+
setInitializing(false);
109+
}
110+
111+
return xmtpClient;
112+
}
113+
return client;
114+
},
115+
[client],
116+
);
117+
118+
const disconnect = useCallback(() => {
119+
if (client) {
120+
client.close();
121+
setClient(undefined);
122+
}
123+
}, [client, setClient]);
37124

38125
// memo-ize the context value to prevent unnecessary re-renders
39126
const value = useMemo(
40127
() => ({
41128
client,
42129
setClient,
43-
confetti,
44-
setConfetti,
130+
initialize,
131+
initializing,
132+
error,
133+
disconnect,
45134
}),
46-
[client, confetti],
135+
[client, initialize, initializing, error, disconnect],
47136
);
48137

49138
return <XMTPContext.Provider value={value}>{children}</XMTPContext.Provider>;
50139
};
140+
141+
export const useXMTP = () => {
142+
return useContext(XMTPContext);
143+
};

0 commit comments

Comments
 (0)