From 0b6e19c9f01fd46b8aad32800cc0b6012f4d7439 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Wed, 19 Nov 2025 19:28:50 +0100 Subject: [PATCH] Refactor ConnectWallet and RootLayout components for improved state management and user experience - Integrated new hooks for network and asset management in ConnectWallet, streamlining wallet connection and asset processing. - Removed unnecessary state management and retry logic, simplifying the component's structure. - Enhanced RootLayout to synchronize user address from the wallet, ensuring accurate user data handling during wallet initialization. - Improved error handling and logging for wallet operations, providing clearer feedback during user interactions. --- .../common/cardano-objects/connect-wallet.tsx | 295 +++--------------- .../common/overall-layout/layout.tsx | 118 ++----- 2 files changed, 69 insertions(+), 344 deletions(-) diff --git a/src/components/common/cardano-objects/connect-wallet.tsx b/src/components/common/cardano-objects/connect-wallet.tsx index c4679b0..02597c2 100644 --- a/src/components/common/cardano-objects/connect-wallet.tsx +++ b/src/components/common/cardano-objects/connect-wallet.tsx @@ -8,13 +8,12 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { useWallet, useWalletList } from "@meshsdk/react"; +import { useWallet, useWalletList, useNetwork, useAssets } from "@meshsdk/react"; import { useSiteStore } from "@/lib/zustand/site"; -import { useEffect, useRef, useState } from "react"; +import { useEffect } from "react"; import useUser from "@/hooks/useUser"; import { useUserStore } from "@/lib/zustand/user"; import { getProvider } from "@/utils/get-provider"; -import { Asset } from "@meshsdk/core"; export default function ConnectWallet() { const setNetwork = useSiteStore((state) => state.setNetwork); @@ -28,277 +27,69 @@ export default function ConnectWallet() { const wallets = useWalletList(); const { connect, connected, wallet, name } = useWallet(); + const networkId = useNetwork(); + const assets = useAssets(); const network = useSiteStore((state) => state.network); - const connectingRef = useRef(false); - const fetchingAssetsRef = useRef(false); - const lastWalletIdRef = useRef(null); - const fetchingNetworkRef = useRef(false); - const lastNetworkWalletRef = useRef(null); - const userAssets = useUserStore((state) => state.userAssets); - const [walletsLoading, setWalletsLoading] = useState(true); - const [isMounted, setIsMounted] = useState(false); - const walletsRetryTimeoutRef = useRef(null); - - // Ensure component only runs on client side (important for SSR/production) - useEffect(() => { - setIsMounted(true); - }, []); async function connectWallet(walletId: string) { setPastWallet(walletId); await connect(walletId); } - // Monitor wallet list loading state and retry if empty + // Auto-connect if user had connected before useEffect(() => { - // Only run on client side - if (!isMounted) return; - - if (wallets.length > 0) { - setWalletsLoading(false); - // Clear any pending retry timeout - if (walletsRetryTimeoutRef.current) { - clearTimeout(walletsRetryTimeoutRef.current); - walletsRetryTimeoutRef.current = null; - } - } else if (!connected) { - // Only show loading state if not connected (wallets might load after connection) - // If wallets are empty, wait a bit and check again - // This handles cases where wallet extensions load asynchronously - if (walletsRetryTimeoutRef.current === null) { - setWalletsLoading(true); - let retryCount = 0; - const maxRetries = 10; // Try for up to 10 seconds in production (longer timeout) - - const checkWallets = () => { - retryCount++; - // Re-check wallets array length (it might have updated) - if (wallets.length > 0) { - setWalletsLoading(false); - walletsRetryTimeoutRef.current = null; - return; - } - - if (retryCount < maxRetries) { - walletsRetryTimeoutRef.current = setTimeout(checkWallets, 1000); // Check every second - } else { - console.warn("Wallet list still empty after retries, wallets may not be available"); - setWalletsLoading(false); // Stop showing loading state - walletsRetryTimeoutRef.current = null; - } - }; - - walletsRetryTimeoutRef.current = setTimeout(checkWallets, 1000); - } - } else { - // If connected but no wallets, they're probably not needed - setWalletsLoading(false); - } - - return () => { - if (walletsRetryTimeoutRef.current) { - clearTimeout(walletsRetryTimeoutRef.current); - walletsRetryTimeoutRef.current = null; - } - }; - }, [wallets, connected, isMounted]); - - /** - * Try to connect the wallet when the user loads the application, if user had connected before, - * but only if: - * 1. Component is mounted (client-side only) - * 2. The wallet list has been loaded (wallets.length > 0) - * 3. The pastWallet exists in the available wallets - * 4. We're not already connected - * 5. We're not already attempting to connect - */ - useEffect(() => { - // Only run on client side - if (!isMounted) return; - - async function handleAutoWalletConnect() { - // Don't attempt if already connected or already connecting - if (connected || connectingRef.current) { - return; - } - - // Don't attempt if no pastWallet is stored - if (!pastWallet) { - return; - } - - // Wait for wallet list to be available - // If wallets array is empty, wallets might still be loading - // The effect will re-run when wallets become available - if (wallets.length === 0) { - console.log("Waiting for wallets to load..."); - return; - } - - // Check if the pastWallet exists in the available wallets + if (pastWallet && !connected && wallets.length > 0) { const walletExists = wallets.some((w) => w.id === pastWallet); - if (!walletExists) { - console.warn( - `Stored wallet "${pastWallet}" not found in available wallets. Clearing stored wallet.`, - ); + if (walletExists) { + connect(pastWallet).catch(() => { + setPastWallet(undefined); + }); + } else { setPastWallet(undefined); - return; - } - - // Attempt to connect - connectingRef.current = true; - try { - console.log(`Attempting to auto-connect wallet: ${pastWallet}`); - await connect(pastWallet); - console.log(`Successfully auto-connected wallet: ${pastWallet}`); - } catch (e) { - console.error( - `Failed to auto-connect wallet "${pastWallet}":`, - e instanceof Error ? e.message : e, - ); - setPastWallet(undefined); - } finally { - connectingRef.current = false; } } + }, [pastWallet, connected, wallets, connect, setPastWallet]); - handleAutoWalletConnect(); - }, [pastWallet, connected, wallets, connect, setPastWallet, isMounted]); - + // Sync network from hook to store useEffect(() => { - async function lookupWalletAssets() { - if (!wallet) return; - - // Prevent multiple simultaneous calls - if (fetchingAssetsRef.current) { - console.log("Assets fetch already in progress, skipping..."); - return; - } - - // Use wallet name as identifier (doesn't require API call) - const walletId = name || "unknown"; - - // Skip if we've already fetched for this wallet and have assets - if (lastWalletIdRef.current === walletId && userAssets.length > 0) { - console.log("Assets already loaded for this wallet, skipping fetch"); - return; - } + if (networkId !== undefined && networkId !== null) { + setNetwork(networkId); + } + }, [networkId, setNetwork]); - fetchingAssetsRef.current = true; - lastWalletIdRef.current = walletId; + // Process assets and fetch metadata + useEffect(() => { + if (!connected || !assets || assets.length === 0 || networkId === undefined || networkId === null) return; + async function processAssets() { + if (!assets || networkId === undefined || networkId === null) return; + try { - console.log("Fetching wallet balance..."); - const assets = await wallet.getBalance(); - // Use network from store if available, otherwise fetch it - let networkId = network; - if (!networkId) { - try { - networkId = await wallet.getNetworkId(); - setNetwork(networkId); - } catch (networkError) { - console.error("Error getting network ID for provider:", networkError); - // Use default network if we can't get it - networkId = 0; // Mainnet default - } - } const provider = getProvider(networkId); - const fetchedAssets: Asset[] = []; - if (assets) { - for (const asset of assets) { - fetchedAssets.push({ - unit: asset.unit, - quantity: asset.quantity, - }); - if (asset.unit === "lovelace") continue; - try { - const assetInfo = await provider.get(`/assets/${asset.unit}`); - setUserAssetMetadata( + setUserAssets(assets); + + for (const asset of assets) { + if (asset.unit === "lovelace") continue; + try { + const assetInfo = await provider.get(`/assets/${asset.unit}`); + setUserAssetMetadata( + asset.unit, + assetInfo?.metadata?.name || + assetInfo?.onchain_metadata?.name || asset.unit, - assetInfo?.metadata?.name || - assetInfo?.onchain_metadata?.name || - asset.unit, - assetInfo?.metadata?.decimals || 0, - ); - } catch (assetError) { - // If asset metadata fetch fails, continue with other assets - console.warn(`Failed to fetch metadata for asset ${asset.unit}:`, assetError); - } + assetInfo?.metadata?.decimals || 0, + ); + } catch (error) { + // Continue if asset metadata fetch fails } - setUserAssets(fetchedAssets); - console.log("Successfully fetched wallet assets"); - } - // Reset the fetching flag after successful fetch - fetchingAssetsRef.current = false; - } catch (error) { - console.error("Error looking up wallet assets:", error); - // If it's a rate limit error, don't clear the ref immediately - // to prevent rapid retries - wait 5 seconds before allowing retry - if (error instanceof Error && error.message.includes("too many requests")) { - console.warn("Rate limit hit, will retry after delay"); - setTimeout(() => { - fetchingAssetsRef.current = false; - }, 5000); - return; } - // For other errors, reset immediately so we can retry - fetchingAssetsRef.current = false; - } - } - - async function handleNetworkChange() { - if (!connected || !wallet) return; - - // Prevent multiple simultaneous network ID fetches - if (fetchingNetworkRef.current) { - console.log("Network ID fetch already in progress, skipping..."); - return; - } - - // Use wallet name as identifier (doesn't require API call) - const walletId = name || "unknown"; - - // Skip if we've already fetched network for this wallet - if (lastNetworkWalletRef.current === walletId && network !== undefined) { - console.log("Network ID already fetched for this wallet, skipping"); - return; - } - - fetchingNetworkRef.current = true; - lastNetworkWalletRef.current = walletId; - - try { - console.log("Fetching network ID..."); - const networkId = await wallet.getNetworkId(); - setNetwork(networkId); - console.log("Successfully fetched network ID:", networkId); - fetchingNetworkRef.current = false; } catch (error) { - console.error("Error getting network ID:", error); - // If rate limited, wait before retry - if (error instanceof Error && error.message.includes("too many requests")) { - console.warn("Rate limit hit for network ID, will retry after delay"); - setTimeout(() => { - fetchingNetworkRef.current = false; - }, 5000); - return; - } - fetchingNetworkRef.current = false; + console.error("Error processing assets:", error); } } - async function getWalletAssets() { - if (wallet && connected) { - await lookupWalletAssets(); - } - } - - // Only run if wallet and connected state are available, and component is mounted - if (isMounted && wallet && connected) { - handleNetworkChange(); - getWalletAssets(); - } - }, [connected, wallet, name, setNetwork, setUserAssets, setUserAssetMetadata, isMounted]); + processAssets(); + }, [assets, connected, networkId, setUserAssets, setUserAssetMetadata]); return ( @@ -313,11 +104,7 @@ export default function ConnectWallet() { Select Wallet - {walletsLoading && wallets.length === 0 ? ( - - Loading wallets... - - ) : wallets.length === 0 ? ( + {wallets.length === 0 ? ( No wallets available. Please install a Cardano wallet extension. diff --git a/src/components/common/overall-layout/layout.tsx b/src/components/common/overall-layout/layout.tsx index 23f05d3..65a0390 100644 --- a/src/components/common/overall-layout/layout.tsx +++ b/src/components/common/overall-layout/layout.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, Component, ReactNode } from "react"; +import React, { useEffect, Component, ReactNode } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { useNostrChat } from "@jinglescode/nostr-chat-plugin"; -import { useWallet } from "@meshsdk/react"; +import { useWallet, useAddress } from "@meshsdk/react"; import { publicRoutes } from "@/data/public-routes"; import { api } from "@/utils/api"; import useUser from "@/hooks/useUser"; @@ -72,6 +72,7 @@ export default function RootLayout({ children: React.ReactNode; }) { const { connected, wallet } = useWallet(); + const address = useAddress(); const { user, isLoading } = useUser(); const router = useRouter(); const { appWallet } = useAppWallet(); @@ -80,8 +81,6 @@ export default function RootLayout({ const userAddress = useUserStore((state) => state.userAddress); const setUserAddress = useUserStore((state) => state.setUserAddress); const ctx = api.useUtils(); - const initializingWalletRef = useRef(false); - const lastInitializedWalletRef = useRef(null); // Global error handler for unhandled promise rejections useEffect(() => { @@ -127,79 +126,38 @@ export default function RootLayout({ }, }); - // Single effect for address + user creation + // Sync address from hook to store useEffect(() => { - (async () => { - if (!connected || !wallet) return; - - // Don't run if user is already loaded (to avoid unnecessary re-runs) - if (user) return; - - // Prevent multiple simultaneous initializations - if (initializingWalletRef.current) { - console.log("Layout: Wallet initialization already in progress, skipping..."); - return; - } - - // Skip if we've already initialized this wallet and have userAddress - if (userAddress && lastInitializedWalletRef.current === userAddress) { - console.log("Layout: Wallet already initialized, skipping"); - return; - } + if (address) { + setUserAddress(address); + } + }, [address, setUserAddress]); - initializingWalletRef.current = true; + // Initialize wallet and create user when connected + useEffect(() => { + if (!connected || !wallet || user || !address) return; + async function initializeWallet() { + if (!address) return; + try { - console.log("Layout: Starting wallet initialization"); - - // 1) Set user address in store - let address: string | undefined; - try { - const usedAddresses = await wallet.getUsedAddresses(); - address = usedAddresses[0]; - } catch (e) { - // If used addresses fail, try unused addresses - try { - const unusedAddresses = await wallet.getUnusedAddresses(); - address = unusedAddresses[0]; - } catch (e2) { - console.error("Layout: Could not get addresses:", e2); - initializingWalletRef.current = false; - return; - } - } - - if (address) { - console.log("Layout: Setting user address:", address); - setUserAddress(address); - lastInitializedWalletRef.current = address; - } else { - console.error("Layout: No address found from wallet"); - initializingWalletRef.current = false; - return; - } - - // 2) Get stake address - const stakeAddress = (await wallet.getRewardAddresses())[0]; - if (!stakeAddress || !address) { - console.error("Layout: No stake address or payment address found"); - return; - } + // Get stake address + const stakeAddresses = await wallet.getRewardAddresses(); + const stakeAddress = stakeAddresses[0]; + if (!stakeAddress) return; - // 3) Get DRep key hash (optional) + // Get DRep key hash (optional) let drepKeyHash = ""; try { const dRepKey = await wallet.getDRep(); - if (dRepKey && dRepKey.publicKeyHash) { + if (dRepKey?.publicKeyHash) { drepKeyHash = dRepKey.publicKeyHash; } - } catch (error) { - // DRep key is optional, so we can ignore errors + } catch { + // DRep key is optional } - // 4) Create or update user (upsert pattern handles both cases) - // Remove the isLoading check - we should create user regardless - console.log("Layout: Creating/updating user"); + // Create or update user const nostrKey = generateNsec(); createUser({ address, @@ -207,36 +165,16 @@ export default function RootLayout({ drepKeyHash, nostrKey: JSON.stringify(nostrKey), }); - console.log("Layout: Wallet initialization completed successfully"); } catch (error) { - console.error("Layout: Error in wallet initialization effect:", error); - - // If we get an "account changed" error, reload the page + console.error("Error initializing wallet:", error); if (error instanceof Error && error.message.includes("account changed")) { - console.log("Layout: Account changed detected, reloading page..."); window.location.reload(); - return; - } - - // If rate limited, wait before allowing retry - if (error instanceof Error && error.message.includes("too many requests")) { - console.warn("Layout: Rate limit hit, will retry after delay"); - setTimeout(() => { - initializingWalletRef.current = false; - }, 5000); - return; - } - - // For other errors, reset so we can retry - initializingWalletRef.current = false; - } finally { - // Reset if not rate limited (rate limit errors return early) - if (initializingWalletRef.current) { - initializingWalletRef.current = false; } } - })(); - }, [connected, wallet, user, userAddress, createUser, generateNsec, setUserAddress]); + } + + initializeWallet(); + }, [connected, wallet, user, address, createUser, generateNsec]); const isWalletPath = router.pathname.includes("/wallets/[wallet]"); const walletPageRoute = router.pathname.split("/wallets/[wallet]/")[1];