Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/components/wallet-connection/WalletConnectorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down
136 changes: 95 additions & 41 deletions src/hooks/wallet/useAutoReconnect.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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]);
}
84 changes: 45 additions & 39 deletions src/hooks/wallet/useWalletConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*
Expand All @@ -51,42 +79,36 @@ 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({
connector,
...(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
Expand All @@ -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);
Expand All @@ -142,7 +155,6 @@ async function waitForAccountSync(
accountStatus: string;
chainId: number | undefined;
}>,
connectorName: string,
): Promise<void> {
const maxWaitTime = 5000;
const pollInterval = 100;
Expand All @@ -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);
}
}
15 changes: 8 additions & 7 deletions src/utils/wagmi/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() });

Expand Down Expand Up @@ -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;
};
Expand Down
Loading