From 1291736e5859e73384158b6e88e6374bd600b6c5 Mon Sep 17 00:00:00 2001 From: Adefokun Adeoluwa Israel Date: Wed, 23 Jul 2025 11:02:30 +0100 Subject: [PATCH 1/2] Fix: Persist wallet connection across page navigation - Implement global wallet context for state management - Store connection state in sessionStorage for persistence - Remove duplicate wallet connection logic from individual pages - Handle wallet events (account changes, disconnections) - Ensure single wallet connection flow across the application Closes #[127] --- src/app/authentication/page.tsx | 51 ++++- src/components/StarknetProvider.tsx | 18 +- src/components/WalletErrorBoundary.tsx | 119 ++++++++++ src/components/WalletLoader.tsx | 63 ++++++ src/components/WalletModal.tsx | 211 ++++++++++++------ .../anonymous-profile/wallet-card.tsx | 137 +++++++----- src/context/WalletContext.tsx | 161 +++++++++++++ 7 files changed, 624 insertions(+), 136 deletions(-) create mode 100644 src/components/WalletErrorBoundary.tsx create mode 100644 src/components/WalletLoader.tsx create mode 100644 src/context/WalletContext.tsx diff --git a/src/app/authentication/page.tsx b/src/app/authentication/page.tsx index dc3c31e..5f82c73 100644 --- a/src/app/authentication/page.tsx +++ b/src/app/authentication/page.tsx @@ -1,12 +1,15 @@ +// / 3. Updated Authentication component "use client"; import React, { useState } from "react"; import { WalletModal } from "@/components/WalletModal"; import { Button } from "@/components/ui/button"; -import { Wallet } from "lucide-react"; +import { Wallet, CheckCircle } from "lucide-react"; +import { useWalletContext } from "@/context/WalletContext"; export default function Authentication() { const [isModalOpen, setIsModalOpen] = useState(false); + const { isConnected, address, connectorName } = useWalletContext(); return (
@@ -19,24 +22,48 @@ export default function Authentication() {

- Connect Your Wallet + {isConnected ? "Wallet Connected" : "Connect Your Wallet"}

- Securely log in using your Starknet wallet for a privacy-focused experience. + {isConnected + ? `Successfully connected with ${connectorName}` + : "Securely log in using your Starknet wallet for a privacy-focused experience." + }

{/* Wallet Connect Button */}
- + {isConnected ? ( +
+
+ + Wallet Connected +
+

+ {address} +

+ +
+ ) : ( + + )} +
); -} +} \ No newline at end of file diff --git a/src/components/StarknetProvider.tsx b/src/components/StarknetProvider.tsx index 310dd9e..ad4a265 100644 --- a/src/components/StarknetProvider.tsx +++ b/src/components/StarknetProvider.tsx @@ -1,12 +1,22 @@ +// providers/StarknetProvider.tsx "use client"; import { StarknetConfig, publicProvider } from "@starknet-react/core"; import { sepolia } from "@starknet-react/chains"; +import { WalletProvider } from "@/context/WalletContext"; +import { WalletLoader } from "@/components/WalletLoader"; +import { WalletErrorBoundary } from "@/components/WalletErrorBoundary"; export default function StarknetProvider({ children }: { children: React.ReactNode }) { return ( - - {children} - + + + + + {children} + + + + ); -} +} \ No newline at end of file diff --git a/src/components/WalletErrorBoundary.tsx b/src/components/WalletErrorBoundary.tsx new file mode 100644 index 0000000..82ce0b5 --- /dev/null +++ b/src/components/WalletErrorBoundary.tsx @@ -0,0 +1,119 @@ +// components/WalletErrorBoundary.tsx +"use client"; + +import React from 'react'; +import { Button } from '@/components/ui/button'; + +interface WalletErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +interface WalletErrorBoundaryProps { + children: React.ReactNode; +} + +class WalletErrorBoundary extends React.Component< + WalletErrorBoundaryProps, + WalletErrorBoundaryState +> { + constructor(props: WalletErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): WalletErrorBoundaryState { + // Check if it's a wallet-related error + const isWalletError = + error.message?.includes('toLowerCase') || + error.message?.includes('WalletAccount') || + error.message?.includes('Account') || + error.stack?.includes('starknet'); + + return { + hasError: !!isWalletError, + error: isWalletError ? error : undefined, + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + // Only log wallet-related errors + if (this.state.hasError) { + console.error('Wallet Error:', error); + console.error('Error Info:', errorInfo); + + // Clear any potentially corrupted wallet data + try { + localStorage.removeItem('wallet_connection'); + } catch (e) { + console.error('Failed to clear wallet connection:', e); + } + } + } + + handleRetry = () => { + // Clear the error state and try to reload + this.setState({ hasError: false, error: undefined }); + + // Clear localStorage wallet data + try { + localStorage.removeItem('wallet_connection'); + } catch (e) { + console.error('Failed to clear wallet connection:', e); + } + + // Optionally reload the page for a fresh start + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+
+
⚠️
+

+ Wallet Connection Error +

+

+ There was an issue connecting to your wallet. This might be due to: +

+
    +
  • • Wallet extension not properly installed
  • +
  • • Network connectivity issues
  • +
  • • Corrupted wallet data
  • +
  • • Browser compatibility issues
  • +
+
+ +

+ This will clear your wallet connection data and refresh the page +

+
+ {process.env.NODE_ENV === 'development' && this.state.error && ( +
+ + Show error details (Development) + +
+                  {this.state.error.message}
+                  {'\n\n'}
+                  {this.state.error.stack}
+                
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} + +export { WalletErrorBoundary }; \ No newline at end of file diff --git a/src/components/WalletLoader.tsx b/src/components/WalletLoader.tsx new file mode 100644 index 0000000..f31731f --- /dev/null +++ b/src/components/WalletLoader.tsx @@ -0,0 +1,63 @@ +// components/WalletLoader.tsx +"use client"; + +import React, { useEffect, useState } from 'react'; +import { useWalletContext } from '@/context/WalletContext'; +import { useAccount } from '@starknet-react/core'; + +interface WalletLoaderProps { + children: React.ReactNode; +} + +const WalletLoader = ({ children }: WalletLoaderProps) => { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const { restoreConnection } = useWalletContext(); + const { address } = useAccount(); + + useEffect(() => { + const initializeWallet = async () => { + try { + setError(null); + await restoreConnection(); + } catch (error: any) { + console.error('Failed to restore wallet connection:', error); + setError(error?.message || 'Failed to initialize wallet connection'); + + // Clear potentially corrupted data + try { + localStorage.removeItem('wallet_connection'); + } catch (e) { + console.error('Failed to clear wallet connection:', e); + } + } finally { + // Give a small delay to ensure everything is initialized + setTimeout(() => { + setIsLoading(false); + }, 500); + } + }; + + initializeWallet(); + }, [restoreConnection]); + + if (isLoading) { + return ( +
+
+
+

Initializing wallet connection...

+ {error && ( +

+ {error} +

+ )} +
+
+ ); + } + + return <>{children}; +}; + +export { WalletLoader }; \ No newline at end of file diff --git a/src/components/WalletModal.tsx b/src/components/WalletModal.tsx index c6c28e7..ccd9847 100644 --- a/src/components/WalletModal.tsx +++ b/src/components/WalletModal.tsx @@ -1,6 +1,7 @@ +// components/WalletModal.tsx "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import Image from "next/image"; import { @@ -19,6 +20,7 @@ import { useDisconnect, useAccount, } from "@starknet-react/core"; +import { useWalletContext } from "@/context/WalletContext"; interface WalletModalProps { isOpen: boolean; @@ -29,112 +31,181 @@ const WalletModal = ({ isOpen, onOpenChange }: WalletModalProps) => { const { toast, dismiss } = useToast(); const router = useRouter(); - const { connect, isSuccess, error, } = useConnect(); + const { connect, isSuccess, error } = useConnect(); const { disconnect } = useDisconnect(); - const { address, connector, } = useAccount(); + const { address, connector } = useAccount(); + + // Use global wallet context + const { + isConnected, + setWalletConnected, + persistConnection, + clearPersistedConnection + } = useWalletContext(); const [walletName, setWalletName] = useState(""); + const [hasProcessedSuccess, setHasProcessedSuccess] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); useEffect(() => { if (connector?.id) { setWalletName(connector.id); } - }, [connector]); - - + }, [connector?.id]); + // Handle success state - prevent infinite loops useEffect(() => { - if (isSuccess && walletName) { + if (isSuccess && walletName && address && !hasProcessedSuccess) { + setHasProcessedSuccess(true); + setIsConnecting(false); toast.success(`Connected to ${walletName}!`); - router.push("/"); + persistConnection(); // Persist the connection + onOpenChange(false); // Close modal + + // Only navigate if not already on home page + if (window.location.pathname !== "/") { + router.push("/"); + } + } else if (error && !hasProcessedSuccess) { + setIsConnecting(false); + toast.error("Connection aborted"); } - else if (error) { - toast.error("Connection aborted") + }, [isSuccess, walletName, address, error, hasProcessedSuccess, persistConnection, onOpenChange, router, toast]); + + // Reset processed state when modal opens + useEffect(() => { + if (isOpen) { + setHasProcessedSuccess(false); + setIsConnecting(false); } - }, [walletName, error, isSuccess, router]); + }, [isOpen]); - const handleConnect = async (wallet: "argent" | "braavos") => { - onOpenChange(false); + const handleConnect = useCallback(async (wallet: "argent" | "braavos") => { + if (isConnecting) return; // Prevent multiple connection attempts + + setIsConnecting(true); const toastId = toast.loading(`Connecting to ${wallet}...`); try { const connectorToUse = wallet === "argent" ? argent() : braavos(); + + // Validate connector before attempting connection + if (!connectorToUse) { + throw new Error(`${wallet} connector not available`); + } + + // Check if wallet is installed/available + const isAvailable = await connectorToUse.available(); + if (!isAvailable) { + throw new Error(`${wallet} wallet is not installed or available`); + } + await connect({ connector: connectorToUse }); dismiss(toastId); - } catch (err) { + } catch (err: unknown) { + setIsConnecting(false); dismiss(toastId); console.error("Connection failed:", err); - toast.error(`Failed to connect to ${wallet}. Try again.`); - } - }; - - + // More specific error messages + let errorMessage = `Failed to connect to ${wallet}. Please try again.`; + if (typeof err === "object" && err !== null && "message" in err && typeof (err as { message: unknown }).message === "string") { + const message = (err as { message: string }).message; + if (message.includes('not installed')) { + errorMessage = `${wallet} wallet is not installed. Please install it first.`; + } else if (message.includes('rejected')) { + errorMessage = 'Connection was rejected by the wallet.'; + } + } + + toast.error(errorMessage); + } + }, [connect, toast, dismiss, isConnecting]); - const handleDisconnect = () => { - disconnect(); + const handleDisconnect = useCallback(() => { + setWalletConnected(false); // Update global state + clearPersistedConnection(); // Clear persisted data + disconnect(); // Disconnect from Starknet setWalletName(""); + setHasProcessedSuccess(false); + setIsConnecting(false); toast.success("Wallet disconnected successfully!"); - }; + onOpenChange(false); // Close modal + }, [setWalletConnected, clearPersistedConnection, disconnect, toast, onOpenChange]); return ( - Connect Wallet + + {isConnected ? "Wallet Connected" : "Connect Wallet"} + - Choose your preferred wallet to connect + {isConnected + ? "Your wallet is connected and ready to use" + : "Choose your preferred wallet to connect" + }
- - - - - + {!isConnected ? ( + <> + + + + + ) : ( +
+
+

+ Connected with {connector?.id} +

+

+ {address} +

+
+ + +
+ )}
- {address && ( -
- Connected: {address} -
- )} - {error && (
{error.message} @@ -145,4 +216,4 @@ const WalletModal = ({ isOpen, onOpenChange }: WalletModalProps) => { ); }; -export { WalletModal }; +export { WalletModal }; \ No newline at end of file diff --git a/src/components/anonymous-profile/wallet-card.tsx b/src/components/anonymous-profile/wallet-card.tsx index 902c83c..6519147 100644 --- a/src/components/anonymous-profile/wallet-card.tsx +++ b/src/components/anonymous-profile/wallet-card.tsx @@ -4,74 +4,111 @@ import { Copy, LogOut, Wallet } from "lucide-react"; import { motion } from "motion/react"; import { useState } from "react"; import { Walletvariant } from "./motion"; +import { useWalletContext } from "@/context/WalletContext"; +import { WalletModal } from "@/components/WalletModal"; +import { useToast } from "@/components/ui/toast"; function WalletCard() { - const [walletConnected, setWalletConnected] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const { isConnected, address, connectorName, setWalletConnected } = useWalletContext(); + const { toast } = useToast(); - const walletAddress = "0x742d35Cc6634C0532925a3b8D4C2C4e"; + // Function to truncate address for display + const truncateAddress = (address: string) => { + if (!address) return ""; + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast.success("Address copied to clipboard!"); + } catch (error) { + console.error("Failed to copy address:", error); + toast.error("Failed to copy address"); + } }; const disconnectWallet = () => { setWalletConnected(false); + toast.success("Wallet disconnected successfully!"); + }; + + const openWalletModal = () => { + setIsModalOpen(true); }; return ( - -
-
- - Wallet Connection + <> + +
+
+ + Wallet Connection +
-
-
- {walletConnected ? ( -
-
-
-
-
-

Connected Wallet

-
-
-

{walletAddress}...

+
+ {isConnected && address ? ( +
+
+
+
+
+

+ Connected with {connectorName || 'Wallet'} +

+
+
+

+ {truncateAddress(address)} +

+
+
+ + +
-
- -
-
- ) : ( -
- -

No wallet connected

- -
- )} -
- + )} +
+ + + {/* Wallet Modal */} + + ); } -export default WalletCard; +export default WalletCard; \ No newline at end of file diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx new file mode 100644 index 0000000..525cfc0 --- /dev/null +++ b/src/context/WalletContext.tsx @@ -0,0 +1,161 @@ +// context/WalletContext.tsx +"use client"; + +import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; +import { useAccount, useDisconnect, useConnect } from '@starknet-react/core'; +import { argent, braavos } from '@starknet-react/core'; + +interface WalletContextType { + isConnected: boolean; + address: string | undefined; + connectorName: string | null; + setWalletConnected: (connected: boolean) => void; + persistConnection: () => void; + clearPersistedConnection: () => void; + restoreConnection: () => Promise; +} + +const WalletContext = createContext(undefined); + +export const WalletProvider = ({ children }: { children: ReactNode }) => { + const [isConnected, setIsConnected] = useState(false); + const [connectorName, setConnectorName] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); + + const { address, connector } = useAccount(); + const { disconnect } = useDisconnect(); + const { connect } = useConnect(); + + // Use localStorage instead of sessionStorage for persistent connection + const persistConnection = useCallback(() => { + if (address && connector && connector.id) { + try { + localStorage.setItem('wallet_connection', JSON.stringify({ + connected: true, + connectorId: connector.id, + address: address, + timestamp: Date.now() + })); + } catch (error) { + console.error('Failed to persist connection:', error); + } + } + }, [address, connector]); + + const clearPersistedConnection = useCallback(() => { + localStorage.removeItem('wallet_connection'); + }, []); + + const setWalletConnected = useCallback((connected: boolean) => { + setIsConnected(connected); + if (!connected) { + setConnectorName(null); + clearPersistedConnection(); + disconnect(); + } + }, [disconnect, clearPersistedConnection]); + + // Restore connection function + const restoreConnection = useCallback(async () => { + const savedConnection = localStorage.getItem('wallet_connection'); + if (savedConnection) { + try { + const { connected, connectorId, timestamp } = JSON.parse(savedConnection); + + // Check if the saved connection is not too old (optional: 24 hours) + const isConnectionValid = Date.now() - timestamp < 24 * 60 * 60 * 1000; + + if (connected && isConnectionValid && !address) { + // Validate connectorId before attempting connection + if (connectorId && (connectorId === 'argent' || connectorId === 'braavos')) { + try { + const connectorToUse = connectorId === 'argent' ? argent() : braavos(); + + // Check if connector is available before connecting + if (connectorToUse) { + await connect({ connector: connectorToUse }); + } else { + console.warn(`Connector ${connectorId} not available`); + clearPersistedConnection(); + } + } catch (error) { + console.error('Failed to restore connection:', error); + clearPersistedConnection(); + } + } else { + console.warn('Invalid connector ID in saved connection'); + clearPersistedConnection(); + } + } else if (!isConnectionValid) { + // Clear old connection data + clearPersistedConnection(); + } + } catch (error) { + console.error('Error parsing saved connection:', error); + localStorage.removeItem('wallet_connection'); + } + } + setIsInitialized(true); + }, [address, connect, clearPersistedConnection]); + + // Initialize and restore connection on mount + useEffect(() => { + if (!isInitialized) { + restoreConnection(); + } + }, [restoreConnection, isInitialized]); + + // Update connection state when account changes + useEffect(() => { + if (address && connector && connector.id) { + setIsConnected(true); + setConnectorName(connector.id); + persistConnection(); + } else if (!address && isConnected && isInitialized) { + // Only clear if we were previously connected and it's not during initialization + setIsConnected(false); + setConnectorName(null); + clearPersistedConnection(); + } + }, [address, connector?.id, isConnected, isInitialized, persistConnection, clearPersistedConnection]); + + // Handle page refresh or navigation - check for existing connection + useEffect(() => { + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible' && !address && isInitialized) { + // Page became visible and no wallet connected, try to restore + const savedConnection = localStorage.getItem('wallet_connection'); + if (savedConnection) { + restoreConnection(); + } + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); + }, [address, isInitialized, restoreConnection]); + + const value: WalletContextType = { + isConnected, + address, + connectorName, + setWalletConnected, + persistConnection, + clearPersistedConnection, + restoreConnection + }; + + return ( + + {children} + + ); +}; + +export const useWalletContext = () => { + const context = useContext(WalletContext); + if (context === undefined) { + throw new Error('useWalletContext must be used within a WalletProvider'); + } + return context; +}; \ No newline at end of file From 359a0b1141234fe3c0e59529935611c33d6f1961 Mon Sep 17 00:00:00 2001 From: Adefokun Adeoluwa Israel Date: Wed, 23 Jul 2025 11:20:07 +0100 Subject: [PATCH 2/2] Fix: Persist wallet connection across page navigation - Implement global wallet context for state management - Store connection state in sessionStorage for persistence - Remove duplicate wallet connection logic from individual pages - Handle wallet events (account changes, disconnections) - Ensure single wallet connection flow across the application Closes #[127] --- src/components/WalletLoader.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/WalletLoader.tsx b/src/components/WalletLoader.tsx index f31731f..49ea584 100644 --- a/src/components/WalletLoader.tsx +++ b/src/components/WalletLoader.tsx @@ -13,16 +13,20 @@ const WalletLoader = ({ children }: WalletLoaderProps) => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { restoreConnection } = useWalletContext(); - const { address } = useAccount(); + useAccount(); useEffect(() => { const initializeWallet = async () => { try { setError(null); await restoreConnection(); - } catch (error: any) { + } catch (error: unknown) { console.error('Failed to restore wallet connection:', error); - setError(error?.message || 'Failed to initialize wallet connection'); + if (error && typeof error === 'object' && 'message' in error && typeof (error as { message?: unknown }).message === 'string') { + setError((error as { message: string }).message); + } else { + setError('Failed to initialize wallet connection'); + } // Clear potentially corrupted data try {