diff --git a/src/components/wallet-connection/WalletConnectorList.tsx b/src/components/wallet-connection/WalletConnectorList.tsx index a4eba5f1..d1455392 100644 --- a/src/components/wallet-connection/WalletConnectorList.tsx +++ b/src/components/wallet-connection/WalletConnectorList.tsx @@ -3,7 +3,7 @@ import { useConnect } from "wagmi"; import type { Connector } from "wagmi"; import { WalletConnectorButton } from "./WalletConnectorButton"; -// import coinbaseIcon from "@/assets/wallets/coinbase.png"; +import coinbaseIcon from "@/assets/wallets/coinbase.png"; import metamaskIcon from "@/assets/wallets/metamask.png"; import rabbyIcon from "@/assets/wallets/rabby.png"; import walletconnectIcon from "@/assets/wallets/walletconnect.png"; @@ -38,17 +38,17 @@ const WALLET_ORDER: WalletConfig[] = [ icon: walletconnectIcon, checkInstalled: () => true, // WalletConnect is always available (QR code) }, - // { - // id: "coinbase", - // name: "Coinbase Wallet", - // icon: coinbaseIcon, - // checkInstalled: () => true, // Coinbase Wallet can use QR code if extension not installed - // }, + { + id: "coinbase", + name: "Coinbase Wallet", + icon: coinbaseIcon, + checkInstalled: () => true, // Coinbase Wallet can use QR code if extension not installed + }, ]; /** * List of wallet connectors in a fixed order - * Shows: Rabby, MetaMask, WalletConnect (Coinbase Wallet disabled) + * Shows: Rabby, MetaMask, WalletConnect, Coinbase Wallet */ export function WalletConnectorList({ onConnect }: WalletConnectorListProps) { const { connectors } = useConnect(); diff --git a/src/hooks/wallet/useAutoReconnect.ts b/src/hooks/wallet/useAutoReconnect.ts index df398eb7..d602c93c 100644 --- a/src/hooks/wallet/useAutoReconnect.ts +++ b/src/hooks/wallet/useAutoReconnect.ts @@ -1,21 +1,57 @@ +import { isCoinbaseWalletConnector } from "@/utils/wagmi/connectorFilters"; import { useEffect, useRef } from "react"; -import { useAccount, useConnect, useConnections } from "wagmi"; +import { useAccount, useConnect, useReconnect } from "wagmi"; + +const COINBASE_STORAGE_KEY = "cbwsdk.store"; + +/** + * Clears Coinbase Wallet localStorage + */ +function clearCoinbaseStorage(): void { + if (typeof window === "undefined") return; + try { + localStorage.removeItem(COINBASE_STORAGE_KEY); + } catch (error) { + // localStorage might be unavailable (private browsing, etc.) + console.debug("Failed to clear Coinbase storage:", error); + } +} + +/** + * Checks if there's a previous Coinbase Wallet session in localStorage + * by looking for the account object in cbwsdk.store + */ +function checkCoinbaseLocalStorage(): boolean { + if (typeof window === "undefined") return false; + + try { + const stored = localStorage.getItem(COINBASE_STORAGE_KEY); + if (!stored) return false; + + const parsed = JSON.parse(stored); + // Check if account exists in the stored state + return !!parsed?.state?.account; + } catch (error) { + // Failed to parse Coinbase storage, assume no session + console.debug("Failed to parse Coinbase storage:", error); + return false; + } +} /** * Automatically reconnects to previously connected wallets on page refresh * * This hook: - * 1. Checks for saved connections from previous session - * 2. Attempts to reconnect to non-Privy connectors - * 3. Checks injected wallets for authorization - * 4. Only attempts reconnection once per session + * 1. Uses wagmi's built-in reconnect mechanism + * 2. Falls back to manual reconnect for specific connectors (injected + Coinbase) + * 3. Only attempts reconnection once per session * * Must be used inside WagmiProvider */ export function useAutoReconnect(): void { const { isConnected } = useAccount(); const { connectors, connectAsync, status } = useConnect(); - const connections = useConnections(); + const { reconnectAsync } = useReconnect(); const hasAttemptedReconnect = useRef(false); useEffect(() => { @@ -24,67 +60,85 @@ export function useAutoReconnect(): void { } const timeoutId = setTimeout(async () => { - // Double check connection status before attempting if (isConnected) { hasAttemptedReconnect.current = true; return; } - // Try to reconnect to saved connection first - if (connections.length > 0) { - const savedConnection = connections[0]; - const connector = connectors.find((c) => c.id === savedConnection.connector.id); + hasAttemptedReconnect.current = true; + + // Check wagmi storage for last connected wallet + let lastConnectorId: string | undefined; + try { + const wagmiStore = localStorage.getItem("wagmi.store"); + if (wagmiStore) { + const parsed = JSON.parse(wagmiStore); + lastConnectorId = parsed?.state?.connections?.value?.[0]?.[0]; + } + } catch (error) { + // Failed to parse wagmi storage, continue without last connector info + console.debug("Failed to parse wagmi storage:", error); + } + + const hasCoinbaseSession = checkCoinbaseLocalStorage(); + const hasOtherWalletConnected = lastConnectorId && !lastConnectorId.toLowerCase().includes("coinbase"); + + // If another wallet was last connected, clear Coinbase storage to prevent popup + if (hasCoinbaseSession && hasOtherWalletConnected) { + clearCoinbaseStorage(); + } - // Only reconnect to non-Privy connectors (Privy is handled by usePrivySync) - if (connector && connector.id !== "privy" && !connector.id.includes("privy")) { - hasAttemptedReconnect.current = true; + // If Coinbase session exists and no other wallet is connected, try Coinbase + if (hasCoinbaseSession && !hasOtherWalletConnected) { + const coinbaseConnector = connectors.find((c) => isCoinbaseWalletConnector(c)); + if (coinbaseConnector) { try { - await connectAsync({ connector }); + await connectAsync({ connector: coinbaseConnector }); + return; } catch (error) { - // Ignore "already connected" errors if (error instanceof Error && error.message.includes("already connected")) { - console.log("useAutoReconnect - Connector already connected, skipping"); - } else { - console.error("useAutoReconnect - Failed to reconnect:", error); - hasAttemptedReconnect.current = false; + return; } + // Coinbase connect failed, clear the stale session + clearCoinbaseStorage(); } - return; } } - // If no saved connection, check for authorized injected wallets + // Clear Coinbase storage if another wallet was last connected + if (hasCoinbaseSession && !hasOtherWalletConnected) { + clearCoinbaseStorage(); + } + + // Try wagmi's built-in reconnect for other wallets + try { + await reconnectAsync(); + return; + } catch (error) { + // Built-in reconnect failed, try manual approaches + console.debug("Wagmi reconnect failed:", error); + } + + // Manual reconnect for injected wallets if (typeof window !== "undefined" && window.ethereum) { const injectedConnectors = connectors.filter((c) => c.type === "injected" && !c.id.includes("privy")); for (const connector of injectedConnectors) { try { const isAuthorized = await connector.isAuthorized(); - - if (isAuthorized && !hasAttemptedReconnect.current) { - hasAttemptedReconnect.current = true; - try { - await connectAsync({ connector }); - return; - } catch (error) { - // Ignore "already connected" errors - if (error instanceof Error && error.message.includes("already connected")) { - console.log(`useAutoReconnect - ${connector.id} already connected, skipping`); - return; - } - console.error(`useAutoReconnect - Failed to reconnect to ${connector.id}:`, error); - hasAttemptedReconnect.current = false; - } + if (isAuthorized) { + await connectAsync({ connector }); + return; } } catch (error) { - console.error(`useAutoReconnect - Error checking authorization for ${connector.id}:`, error); + if (error instanceof Error && error.message.includes("already connected")) { + return; + } } } } - - hasAttemptedReconnect.current = true; }, 500); return () => clearTimeout(timeoutId); - }, [isConnected, status, connectors, connectAsync, connections]); + }, [isConnected, status, connectors, connectAsync, reconnectAsync]); } diff --git a/src/hooks/wallet/useWalletConnection.ts b/src/hooks/wallet/useWalletConnection.ts index f635def6..8919b871 100644 --- a/src/hooks/wallet/useWalletConnection.ts +++ b/src/hooks/wallet/useWalletConnection.ts @@ -2,10 +2,40 @@ import { ANALYTICS_EVENTS } from "@/constants/analytics-events"; import { withTracking } from "@/utils/analytics"; import { isDev, isLocalhost, isNetlifyPreview, isProd } from "@/utils/utils"; import { TENDERLY_RPC_URL, baseNetwork as base, tenderlyTestnetNetwork as testnet } from "@/utils/wagmi/chains"; -import { /* isCoinbaseWalletConnector, */ isWalletConnectConnector } from "@/utils/wagmi/connectorFilters"; +import { isCoinbaseWalletConnector, isWalletConnectConnector } from "@/utils/wagmi/connectorFilters"; import { useCallback, useEffect, useRef, useState } from "react"; import { useAccount, useConnect } from "wagmi"; -import type { Connector } from "wagmi"; + +const COINBASE_STORAGE_KEY = "cbwsdk.store"; + +/** + * Determines the chain ID to connect to based on environment + */ +function getConnectChainId(): number | undefined { + if (isProd()) { + return base.id; + } + if (isNetlifyPreview()) { + return TENDERLY_RPC_URL ? testnet.id : base.id; + } + if (!isLocalhost()) { + return base.id; + } + return undefined; +} + +/** + * Clears Coinbase Wallet localStorage to prevent popup issues + * when connecting to a different wallet + */ +function clearCoinbaseStorage(): void { + if (typeof window === "undefined") return; + try { + localStorage.removeItem(COINBASE_STORAGE_KEY); + } catch { + // Storage errors are non-critical, silently ignore + } +} export interface UseWalletConnectionOptions { onSuccess?: () => void; @@ -18,15 +48,13 @@ export interface UseWalletConnectionReturn { connectingWalletId: string | null; } -//TODO: Fix the Coinbase Wallet reconnect issue and add it as a connector. For reference: https://github.com/wevm/wagmi/issues/4375 - /** * Hook for managing wallet connections with analytics and error handling * * Features: * - Connects to wallets with proper chain selection based on environment * - Tracks analytics events for connections - * - Handles WalletConnect special cases (Coinbase Wallet disabled) + * - Handles WalletConnect and Coinbase Wallet special cases * - Waits for account sync when needed * - Provides error handling with callbacks * @@ -51,31 +79,21 @@ export function useWalletConnection(options?: UseWalletConnectionOptions): UseWa const connector = connectors.find((c) => c.id === connectorId); if (!connector) { - const error = new Error(`Connector not found: ${connectorId}`); - console.error(error.message); - onError?.(error); + onError?.(new Error(`Connector not found: ${connectorId}`)); return; } const isWalletConnect = isWalletConnectConnector(connector); - // const isCoinbaseWallet = isCoinbaseWalletConnector(connector); + const isCoinbaseWallet = isCoinbaseWalletConnector(connector); setConnectingWalletId(connectorId); try { - await withTracking( + withTracking( ANALYTICS_EVENTS.WALLET.CONNECT_BUTTON_CLICK, async () => { // Determine chain ID based on environment - let connectChainId: number | undefined = undefined; - - if (isProd()) { - connectChainId = base.id; - } else if (isNetlifyPreview()) { - connectChainId = !!TENDERLY_RPC_URL ? testnet.id : base.id; - } else if (!isLocalhost()) { - connectChainId = base.id; - } + const connectChainId = getConnectChainId(); // Connect to wallet await connectAsync({ @@ -83,10 +101,14 @@ export function useWalletConnection(options?: UseWalletConnectionOptions): UseWa ...(connectChainId ? { chainId: connectChainId } : {}), }); - // Wait for account sync if needed (Coinbase Wallet disabled) - const shouldWaitForAccountSync = /* isCoinbaseWallet || */ isDev(); - if (shouldWaitForAccountSync) { - await waitForAccountSync(accountStateRef, connector.name); + // Clear Coinbase storage if connecting to a different wallet + if (!isCoinbaseWallet) { + clearCoinbaseStorage(); + } + + // Wait for account sync when needed + if (isCoinbaseWallet || isDev()) { + await waitForAccountSync(accountStateRef); } // Track successful connection @@ -107,15 +129,6 @@ export function useWalletConnection(options?: UseWalletConnectionOptions): UseWa onSuccess?.(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); - console.error(`Connection error for ${connector.name}:`, error); - - // if (isCoinbaseWallet) { - // console.warn("Coinbase Wallet connection failed. Possible reasons:"); - // console.warn("1. Extension not installed"); - // console.warn("2. QR modal couldn't open"); - // console.warn("3. User cancelled connection"); - // } - onError?.(err); } finally { setConnectingWalletId(null); @@ -142,7 +155,6 @@ async function waitForAccountSync( accountStatus: string; chainId: number | undefined; }>, - connectorName: string, ): Promise { const maxWaitTime = 5000; const pollInterval = 100; @@ -155,10 +167,4 @@ async function waitForAccountSync( } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } - - const finalState = accountStateRef.current; - if (!finalState.isConnected || !finalState.address) { - console.warn(`${connectorName} account did not sync within timeout`); - console.warn("Current state:", finalState); - } } diff --git a/src/utils/wagmi/config.ts b/src/utils/wagmi/config.ts index 70c3db02..d2293530 100644 --- a/src/utils/wagmi/config.ts +++ b/src/utils/wagmi/config.ts @@ -3,7 +3,7 @@ import { getEnvEnabledChains, localhostNetwork as localhost } from "@/utils/wagm import { Chain, Transport, createTestClient } from "viem"; import { http, createStorage } from "wagmi"; import type { CreateConnectorFn } from "wagmi"; -import { /* coinbaseWallet, */ injected, walletConnect } from "wagmi/connectors"; +import { coinbaseWallet, injected, walletConnect } from "wagmi/connectors"; export const anvilTestClient = createTestClient({ mode: "anvil", chain: localhost, transport: http() }); @@ -61,12 +61,13 @@ export const getBaseConnectors = (): CreateConnectorFn[] => { ); } - // connectors.push( - // coinbaseWallet({ - // appName: "Pinto", - // appLogoUrl: PintoIcon, - // }) as CreateConnectorFn, - // ); + connectors.push( + coinbaseWallet({ + appName: "Pinto", + appLogoUrl: PintoIcon, + preference: "smartWalletOnly", // Support both extension and smart wallet + }) as CreateConnectorFn, + ); return connectors; }; diff --git a/src/utils/wagmi/connectorFilters.ts b/src/utils/wagmi/connectorFilters.ts index d92f08c9..56e49b02 100644 --- a/src/utils/wagmi/connectorFilters.ts +++ b/src/utils/wagmi/connectorFilters.ts @@ -58,9 +58,9 @@ export function isWalletConnectConnector(connector: Connector): boolean { * @param connector - The wagmi connector to check * @returns True if connector is Coinbase Wallet */ -// export function isCoinbaseWalletConnector(connector: Connector): boolean { -// const connectorId = connector.id.toLowerCase(); -// const connectorName = connector.name.toLowerCase(); +export function isCoinbaseWalletConnector(connector: Connector): boolean { + const connectorId = connector.id.toLowerCase(); + const connectorName = connector.name.toLowerCase(); -// return connector.type === "coinbaseWallet" || connectorId.includes("coinbase") || connectorName.includes("coinbase"); -// } + return connector.type === "coinbaseWallet" || connectorId.includes("coinbase") || connectorName.includes("coinbase"); +}