diff --git a/packages/thirdweb/src/react/providers/wallet-ui-states-provider.tsx b/packages/thirdweb/src/react/providers/wallet-ui-states-provider.tsx index efb1381cc8d..c01c80b037c 100644 --- a/packages/thirdweb/src/react/providers/wallet-ui-states-provider.tsx +++ b/packages/thirdweb/src/react/providers/wallet-ui-states-provider.tsx @@ -23,6 +23,7 @@ export type ModalConfig = { isEmbed?: boolean; onConnect?: () => void; chainId?: bigint; + chains?: bigint[]; }; const WalletModalOpen = /* @__PURE__ */ createContext(false); @@ -61,6 +62,7 @@ export const WalletUIStatesProvider = ( }; onConnect?: () => void; chainId?: bigint; + chains?: bigint[]; }>, ) => { const [isWalletModalOpen, setIsWalletModalOpen] = useState(false); @@ -80,6 +82,7 @@ export const WalletUIStatesProvider = ( auth: props.auth, onConnect: props.onConnect, chainId: props.chainId, + chains: props.chains, }); return ( @@ -235,6 +238,9 @@ export type ModalConfigOptions = { * Note that this does not include the sign in, If you want to call a callback after user connects AND signs in with their wallet, use `auth.onLogin` instead */ onConnect?: () => void; + + chainId?: bigint; + chains?: bigint[]; }; /** diff --git a/packages/thirdweb/src/react/types/wallets.ts b/packages/thirdweb/src/react/types/wallets.ts index c177daca3f2..86a0b0cb0a5 100644 --- a/packages/thirdweb/src/react/types/wallets.ts +++ b/packages/thirdweb/src/react/types/wallets.ts @@ -35,7 +35,7 @@ export type WalletConfig = { * * This is only required if your wallet requires a personal wallet to be connected such as a Safe Wallet or Smart Wallet * - * providing the `personalWallets` ensures that autoconnect and ConnectWallet modal works properly for your wallet. + * providing the `personalWallets` ensures that auto connect and ConnectWallet modal works properly for your wallet. * auto-connect will connect the last connected personal wallet first and then connect your wallet * ConnectWallet modal will reopen once the personal wallet is connected so that you can render UI for connecting your wallet as the next step */ @@ -112,6 +112,11 @@ export type ConnectUIProps = { */ chainId?: bigint; + /** + * List of all chains supported by the app + */ + chains?: bigint[]; + /** * Create a wallet instance * Calling this function uses calls the `WalletConfig.create` method with the required options diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/ConnectWallet.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/ConnectWallet.tsx index 3f2405e3282..a6a4fd3d052 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/ConnectWallet.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/ConnectWallet.tsx @@ -28,8 +28,16 @@ import { } from "../../providers/wallet-ui-states-provider.js"; export type ConnectWalletProps = { + /** + * If specified, Wallet will be prompted to switch to this chain after connecting if it is not already connected to this chain. + */ chainId?: bigint | number; + /** + * Array of chain ids that the app supports + */ + chains?: (bigint | number)[]; + /** * CSS class to apply to the button element * @@ -670,6 +678,7 @@ export function ConnectWallet(props: ConnectWalletProps) { auth: props.auth, onConnect: props.onConnect, chainId: props.chainId ? BigInt(props.chainId) : undefined, + chains: props.chains?.map(BigInt), }); setIsWalletModalOpen(true); }} @@ -748,7 +757,7 @@ export function ConnectWallet(props: ConnectWalletProps) { }} hideSwitchToPersonalWallet={props.hideSwitchToPersonalWallet} hideDisconnect={props.hideDisconnect} - chainIds={props.networkSelector?.chains || []} + chains={props?.chains?.map(BigInt) || []} /> ); })()} diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx index 819007a604f..83ec5eccad0 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx @@ -76,7 +76,7 @@ export const ConnectedWalletDetails: React.FC<{ displayBalanceToken?: Record; hideSwitchToPersonalWallet?: boolean; hideDisconnect?: boolean; - chainIds: number[]; + chains: bigint[]; }> = (props) => { const locale = useTWLocale().connectWallet; const activeAccount = useActiveAccount(); @@ -564,8 +564,8 @@ export const ConnectedWalletDetails: React.FC<{ { // no op }} - chainId={props.chainId ? BigInt(props.chainId) : undefined} /> ); } @@ -375,6 +379,7 @@ const ConnectEmbedContent = ( auth: props.auth, onConnect: props.onConnect, chainId: props.chainId ? BigInt(props.chainId) : undefined, + chains: props.chains, }; return ( diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModal.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModal.tsx index 24ab231386b..5aba6bc9eda 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModal.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModal.tsx @@ -15,7 +15,7 @@ import { ConnectModalContent } from "./ConnectModalContent.js"; * @internal */ export const ConnectModal = () => { - const { theme, modalSize, chainId } = useContext(ModalConfigCtx); + const { theme, modalSize } = useContext(ModalConfigCtx); const { screen, setScreen, initialScreen } = useScreen(); const isWalletModalOpen = useIsWalletModalOpen(); @@ -127,7 +127,6 @@ export const ConnectModal = () => { onShow={onShow} isOpen={isWalletModalOpen} onClose={closeModal} - chainId={chainId} /> diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx index 323a993b0f9..ff38e59c223 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx @@ -35,7 +35,6 @@ export const ConnectModalContent = (props: { onShow: () => void; isOpen: boolean; onClose: () => void; - chainId?: bigint; }) => { const { screen, setScreen, initialScreen, onHide, onShow, onClose } = props; const { wallets, client, dappMetadata } = useThirdwebProviderProps(); @@ -171,13 +170,15 @@ export const ConnectModalContent = (props: { // activeWalletConnectionStatus={activeWalletConnectionStatus} // setActiveWalletConnectionStatus={setActiveWalletConnectionStatus} // connect={walletConfig.connect} + createInstance={() => { return walletConfig.create({ client, dappMetadata, }); }} - chainId={props.chainId} + chains={modalConfig.chains} + chainId={modalConfig.chainId} /> ); }; @@ -221,7 +222,7 @@ export const ConnectModalContent = (props: { * @internal */ export const ConnectModal = () => { - const { theme, modalSize, chainId } = useContext(ModalConfigCtx); + const { theme, modalSize } = useContext(ModalConfigCtx); const { screen, setScreen, initialScreen } = useScreen(); const isWalletModalOpen = useIsWalletModalOpen(); @@ -333,7 +334,6 @@ export const ConnectModal = () => { onShow={onShow} isOpen={isWalletModalOpen} onClose={closeModal} - chainId={chainId} /> diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalInline.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalInline.tsx index ab7b999a9c5..c35b27d2211 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalInline.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalInline.tsx @@ -35,6 +35,11 @@ export type ConnectModalInlineProps = { */ chainId?: bigint; + /** + * List of all chains that the app supports + */ + chains?: (bigint | number)[]; + className?: string; theme?: "dark" | "light" | Theme; @@ -108,7 +113,6 @@ export const ConnectModalInline = (props: ConnectModalInlineProps) => { onShow={() => { // no op }} - chainId={props.chainId} /> {/* close icon */} @@ -135,6 +139,7 @@ export const ConnectModalInline = (props: ConnectModalInlineProps) => { welcomeScreen: props.welcomeScreen, titleIconUrl: props.modalTitleIconUrl, chainId: props.chainId, + chains: props.chains?.map(BigInt), }; return ( diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/NetworkSelector.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/NetworkSelector.tsx index 62b863ccfd2..bfae1a72c07 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/NetworkSelector.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/NetworkSelector.tsx @@ -96,16 +96,16 @@ export type NetworkSelectorProps = { */ open: boolean; - chains?: number[]; + chains?: (number | bigint)[]; /** * Array of chains to be displayed under "Popular" section */ - popularChains?: number[]; + popularChains?: (number | bigint)[]; /** * Array of chains to be displayed under "Recent" section */ - recentChains?: number[]; + recentChains?: (number | bigint)[]; /** * Override how the chain button is rendered in the Modal */ diff --git a/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx b/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx index 25156a10f49..016b9812b22 100644 --- a/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx +++ b/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx @@ -33,6 +33,7 @@ export const WalletConnectConnection: React.FC<{ const { client, dappMetadata } = useThirdwebProviderProps(); const [qrCodeUri, setQrCodeUri] = useState(); const [errorConnecting, setErrorConnecting] = useState(false); + const optionalChains = connectUIProps.chains; const connect = useCallback(() => { const wallet = walletConnect({ @@ -71,6 +72,7 @@ export const WalletConnectConnection: React.FC<{ } }, onSessionRequestSent, + optionalChains, }) .then((account) => { done(account); @@ -87,6 +89,7 @@ export const WalletConnectConnection: React.FC<{ platformUris, projectId, walletConfig.metadata, + optionalChains, ]); const scanStarted = useRef(false); diff --git a/packages/thirdweb/src/wallets/manager/index.ts b/packages/thirdweb/src/wallets/manager/index.ts index 3e82a730946..1eecc83ee89 100644 --- a/packages/thirdweb/src/wallets/manager/index.ts +++ b/packages/thirdweb/src/wallets/manager/index.ts @@ -71,6 +71,7 @@ export function createConnectionManager(options?: ConnectionManagerOptions) { const disconnect = (account: Account) => { onAccountDisconnect(account); + account.wallet.disconnect(); }; const setActiveAccount = (account: Account) => { diff --git a/packages/thirdweb/src/wallets/manager/storage.ts b/packages/thirdweb/src/wallets/manager/storage.ts index 4e8ae5afc53..76da5aa2c16 100644 --- a/packages/thirdweb/src/wallets/manager/storage.ts +++ b/packages/thirdweb/src/wallets/manager/storage.ts @@ -29,9 +29,10 @@ const CONNECT_PARAMS_MAP_KEY = "tw:connected-wallet-params"; * @param params * @internal */ -export async function saveConnectParamsToStorage< - T extends WalletConnectionOptions, ->(walletId: string, params: T) { +export async function saveConnectParamsToStorage( + walletId: string, + params: T, +) { // params must be stringifiable if (!isStringifiable(params)) { console.debug("params", params); @@ -89,9 +90,9 @@ export async function deleteConnectParamsFromStorage(walletId: string) { * Get the saved connection params from storage for given wallet id * @internal */ -export async function getSavedConnectParamsFromStorage< - T extends WalletConnectionOptions, ->(walletId: string): Promise { +export async function getSavedConnectParamsFromStorage( + walletId: string, +): Promise { const valueStr = await walletStorage.get(CONNECT_PARAMS_MAP_KEY); if (!valueStr) { @@ -102,7 +103,7 @@ export async function getSavedConnectParamsFromStorage< const value = JSON.parse(valueStr); if (value && value[walletId]) { - return value[walletId] as T; + return value[walletId]; } return null; diff --git a/packages/thirdweb/src/wallets/wallet-connect/index.ts b/packages/thirdweb/src/wallets/wallet-connect/index.ts index 4b7c6e1715d..05be1c098c2 100644 --- a/packages/thirdweb/src/wallets/wallet-connect/index.ts +++ b/packages/thirdweb/src/wallets/wallet-connect/index.ts @@ -51,9 +51,15 @@ const storageKeys = { lastUsedChainId: "tw.wc.lastUsedChainId", }; -const isNewChainsStale = false; // do we need to make this configurable? +const isNewChainsStale = true; const defaultShowQrModal = true; -const defaultChainId = 1; +const defaultChainId = /* @__PURE__ */ BigInt(1); + +type SavedConnectParams = { + optionalChains?: string[]; + chainId: string; + pairingTopic?: string; +}; /** * Connect to a wallet using WalletConnect protocol. @@ -110,12 +116,20 @@ export class WalletConnect implements Wallet { * @returns A Promise that resolves to the connected wallet address. */ async autoConnect(): Promise { - const savedOptions = await getSavedConnectParamsFromStorage( - this.metadata.id, + const savedConnectParams: SavedConnectParams | null = + await getSavedConnectParamsFromStorage(this.metadata.id); + + const provider = await this.initProvider( + true, + savedConnectParams + ? { + chainId: BigInt(savedConnectParams.chainId), + pairingTopic: savedConnectParams.pairingTopic, + optionalChains: savedConnectParams.optionalChains?.map(BigInt), + } + : undefined, ); - const provider = await this.initProvider(true, savedOptions || undefined); - const address = provider.accounts[0]; if (!address) { @@ -176,8 +190,12 @@ export class WalletConnect implements Wallet { */ async connect(options?: WalletConnectConnectionOptions): Promise { const provider = await this.initProvider(false, options); - const isStale = await this.isChainsStale(provider.chainId); - const targetChainId = Number(options?.chainId || defaultChainId); + + const isChainsState = await this.isChainsStale( + [provider.chainId, ...(options?.optionalChains || [])].map(BigInt), + ); + + const targetChainId = BigInt(options?.chainId || defaultChainId); const rpc = getRpcUrlForChain({ chain: targetChainId, @@ -189,9 +207,6 @@ export class WalletConnect implements Wallet { if (onDisplayUri || onSessionRequestSent) { if (onDisplayUri) { provider.events.addListener("display_uri", onDisplayUri); - provider.events.addListener("disconnect", () => { - provider.events.removeListener("display_uri", onDisplayUri); - }); } if (onSessionRequestSent) { @@ -206,12 +221,12 @@ export class WalletConnect implements Wallet { } // If there no active session, or the chain is state, force connect. - if (!provider.session || isStale) { + if (!provider.session || isChainsState) { await provider.connect({ pairingTopic: options?.pairingTopic, - chains: [targetChainId], + chains: [Number(targetChainId)], rpcMap: { - [targetChainId]: rpc, + [targetChainId.toString()]: rpc, }, }); @@ -228,7 +243,17 @@ export class WalletConnect implements Wallet { this.chainId = normalizeChainId(provider.chainId); if (options) { - saveConnectParamsToStorage(this.metadata.id, options); + const savedParams: SavedConnectParams = { + optionalChains: options.optionalChains?.map(String), + chainId: String(options.chainId), + pairingTopic: options.pairingTopic, + }; + + saveConnectParamsToStorage(this.metadata.id, savedParams); + } + + if (options?.onDisplayUri) { + provider.events.removeListener("display_uri", options.onDisplayUri); } return this.onConnect(address); @@ -245,8 +270,8 @@ export class WalletConnect implements Wallet { const provider = this.provider; if (provider) { this.onDisconnect(); - await provider.disconnect(); deleteConnectParamsFromStorage(this.metadata.id); + provider.disconnect(); } } @@ -260,11 +285,11 @@ export class WalletConnect implements Wallet { */ async switchChain(chainId: number | bigint) { const provider = this.assertProvider(); - const chainIdNum = Number(chainId); + const chainIdBigInt = BigInt(chainId); try { - const namespaceChains = this.getNamespaceChainsIds(); + const namespaceChains = this.getNamespaceChainsIds().map(BigInt); const namespaceMethods = this.getNamespaceMethods(); - const isChainApproved = namespaceChains.includes(chainIdNum); + const isChainApproved = namespaceChains.includes(chainIdBigInt); if (!isChainApproved && namespaceMethods.includes(ADD_ETH_CHAIN_METHOD)) { const chain = await getChainDataForChainId(BigInt(chainId)); @@ -284,8 +309,10 @@ export class WalletConnect implements Wallet { }, ], }); - const requestedChains = await this.getRequestedChainsIds(); - requestedChains.push(chainIdNum); + const requestedChains = (await this.getRequestedChainsIds()).map( + BigInt, + ); + requestedChains.push(chainIdBigInt); this.setRequestedChainsIds(requestedChains); } await provider.request({ @@ -315,7 +342,7 @@ export class WalletConnect implements Wallet { isAutoConnect: boolean, connectionOptions?: WalletConnectConnectionOptions, ) { - const targetChainId = Number(connectionOptions?.chainId || 1); + const targetChainId = BigInt(connectionOptions?.chainId || defaultChainId); const rpc = getRpcUrlForChain({ chain: targetChainId, @@ -330,8 +357,7 @@ export class WalletConnect implements Wallet { projectId: this.options?.projectId || defaultWCProjectId, optionalMethods: OPTIONAL_METHODS, optionalEvents: OPTIONAL_EVENTS, - chains: [targetChainId], - // optionalChains: [], + optionalChains: [Number(targetChainId)], metadata: { name: this.options.dappMetadata?.name || defaultDappMetadata.name, description: @@ -343,17 +369,23 @@ export class WalletConnect implements Wallet { ], }, rpcMap: { - [targetChainId]: rpc, + [targetChainId.toString()]: rpc, }, qrModalOptions: connectionOptions?.qrModalOptions, + disableProviderPing: true, }); + provider.events.setMaxListeners(Infinity); this.provider = provider; - // const isStale = await this.#isChainsStale(Number(this.chainId)); - // isStale && if (!isAutoConnect) { - if (provider.session) { + const chains = [ + targetChainId, + ...(connectionOptions?.optionalChains || []), + ].map(BigInt); + + const isStale = await this.isChainsStale(chains); + if (isStale && provider.session) { await provider.disconnect(); } } @@ -416,9 +448,9 @@ export class WalletConnect implements Wallet { * Get the last requested chains from the storage. * @internal */ - private async getRequestedChainsIds(): Promise { + private async getRequestedChainsIds(): Promise { const data = await walletStorage.get(storageKeys.requestedChains); - return data ? JSON.parse(data) : []; + return (data ? JSON.parse(data) : []).map(BigInt); } /** @@ -442,44 +474,51 @@ export class WalletConnect implements Wallet { * @param connectToChainId * @internal */ - private async isChainsStale(connectToChainId: number) { + private async isChainsStale(chains: bigint[]) { const namespaceMethods = this.getNamespaceMethods(); + // if chain adding method is available, then chains are not stale if (namespaceMethods.includes(ADD_ETH_CHAIN_METHOD)) { return false; } + // if new chains are considered stale, then return true if (!isNewChainsStale) { return false; } const requestedChains = await this.getRequestedChainsIds(); - const connectorChains = [connectToChainId]; - const namespaceChains = this.getNamespaceChainsIds(); + const namespaceChains = this.getNamespaceChainsIds().map(BigInt); + // if any of the requested chains are not in the namespace chains, then they are stale if ( namespaceChains.length && - !namespaceChains.some((id) => connectorChains.includes(id)) + !namespaceChains.some((id) => chains.includes(id)) ) { return false; } - return !connectorChains.every((id) => requestedChains.includes(Number(id))); + // if chain was requested earlier, then they are not stale + return !chains.every((id) => requestedChains.includes(id)); } /** * Set the requested chains to the storage. * @internal */ - private setRequestedChainsIds(chains: number[]) { - walletStorage.set(storageKeys.requestedChains, JSON.stringify(chains)); + private setRequestedChainsIds(chains: bigint[]) { + walletStorage.set( + storageKeys.requestedChains, + JSON.stringify(chains.map(Number)), + ); } /** * Disconnect the wallet and clear the session and perform cleanup. + * Note: must use arrow function to preserve `this` when it's passed down as a callback to the provider. * @internal */ - private onDisconnect() { + private onDisconnect = () => { this.setRequestedChainsIds([]); walletStorage.remove(storageKeys.lastUsedChainId); @@ -489,14 +528,15 @@ export class WalletConnect implements Wallet { provider.removeListener("disconnect", this.onDisconnect); provider.removeListener("session_delete", this.onDisconnect); } - } + }; /** * Update the `chainId` on chainChanged event. + * Note: must use arrow function to preserve `this` when it's passed down as a callback to the provider. * @internal */ - private onChainChanged(chainId: number | string) { + private onChainChanged = (chainId: number | string) => { this.chainId = normalizeChainId(chainId); walletStorage.set(storageKeys.lastUsedChainId, String(chainId)); - } + }; } diff --git a/packages/thirdweb/src/wallets/wallet-connect/types.ts b/packages/thirdweb/src/wallets/wallet-connect/types.ts index 26469de5b50..cb8dd03b35c 100644 --- a/packages/thirdweb/src/wallets/wallet-connect/types.ts +++ b/packages/thirdweb/src/wallets/wallet-connect/types.ts @@ -27,6 +27,7 @@ export type WalletConnectCreationOptions = { export type WalletConnectConnectionOptions = { chainId?: number | bigint; + optionalChains?: (number | bigint)[]; showQrModal?: boolean; pairingTopic?: string; qrModalOptions?: QRCodeModalOptions;