diff --git a/src/App.tsx b/src/App.tsx index b74b61c..824f216 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,64 @@ import { Route, Routes } from 'react-router'; import LoginPage from './pages/LoginPage'; +import React from 'react'; +import AuthContext from './context/AuthContext'; +import connectToBlockchain from './config'; function App() { + const auth = React.useContext(AuthContext); + + const { dispatch } = auth; + + React.useEffect(() => { + let isMounted = true; + + const init = async () => { + if (isMounted) { + const blockchainConnection = await connectToBlockchain(); + if (!blockchainConnection) return; + + const { contractInstance, signer } = blockchainConnection; + const account = await signer.getAddress(); + + let is_admin = false; + let is_voter = true; + + try { + is_admin = await contractInstance.isAdmin(account); + } catch (error) { + console.error('Error while fetching user data:', error); + } + + if (!is_admin) { + try { + const voter = await contractInstance.getVoterdetails(account); + if (voter?.votername) { + is_voter = false; + } + } catch (error) { + console.error('Error while fetching user data:', error); + } + } + + dispatch({ + type: 'LOGIN', + payload: { account, instance: contractInstance, is_admin, flag: is_voter }, + }); + } + }; + + init(); + + return () => { + isMounted = false; + }; + }, [dispatch]); + return ( - } /> + } /> + home} /> + not found return 404} /> ); } diff --git a/src/components/shared/Form-error.tsx b/src/components/shared/Form-error.tsx index e69de29..f4358b2 100644 --- a/src/components/shared/Form-error.tsx +++ b/src/components/shared/Form-error.tsx @@ -0,0 +1,16 @@ +import { AlertTriangle } from 'lucide-react'; + +type FormErrorProps = { + message?: string; +}; + +export const FormError = ({ message }: FormErrorProps) => { + if (!message) return null; + + return ( +
+ +

{message}

+
+ ); +}; diff --git a/src/components/shared/auth/Login-form.tsx b/src/components/shared/auth/Login-form.tsx index 414fc39..aa9d6cf 100644 --- a/src/components/shared/auth/Login-form.tsx +++ b/src/components/shared/auth/Login-form.tsx @@ -1,119 +1,89 @@ -import { Vote } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import connectToBlockchain from '@/config'; -import { useContext, useState } from 'react'; +import React from 'react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { LoginSchema } from '@/schemas'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { FormError } from '../Form-error'; import AuthContext from '@/context/AuthContext'; -import { toast } from 'sonner'; -export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { - const auth = useContext(AuthContext); - const [name, setName] = useState(''); - const [loading, setLoading] = useState(false); - - if (!auth) { - return

Loading...

; - } +export function LoginForm() { + const [error, setError] = React.useState(''); + const [success, setSuccess] = React.useState(''); + const [isPending, startTransition] = React.useTransition(); + const auth = React.useContext(AuthContext); const { dispatch } = auth; - const connectWallet = async (e: React.FormEvent) => { - e.preventDefault(); // Prevent form submission - setLoading(true); - - try { - const blockchainConnection = await connectToBlockchain(); - console.log('blockchainConnection', blockchainConnection); - if (!blockchainConnection) { - setLoading(false); - return; - } - - const { contractInstance, signer } = blockchainConnection; - console.log('contractInstacne', contractInstance); - console.log('signer', signer); - const account = await signer.getAddress(); - console.log('account', signer); - console.log('account1', account); - - console.log('contractInstance', contractInstance); - - toast.success(`Connected to ${account}`); - - let isAdmin = false; - let isVoter = true; + const form = useForm>({ + resolver: zodResolver(LoginSchema), + defaultValues: { + name: '', + }, + }); - try { - isAdmin = await contractInstance.isAdmin(account); - console.log('isAdmin', isAdmin); - } catch (err) { - console.error('Error checking admin status:', err); - } - - if (!isAdmin) { - try { - const voter = await contractInstance.getVoterdetails(account); - if (voter?.votername) { - isVoter = false; - } - } catch (error) { - console.error('Error fetching voter details:', error); - } - } - - dispatch({ - type: 'login', - payload: { account, instance: contractInstance, is_admin: isAdmin, flag: isVoter }, - }); - - toast.success('Wallet connected successfully!'); - } catch (error) { - console.error('MetaMask connection error:', error); - toast.error('Failed to connect wallet.'); - } finally { - setLoading(false); - } + const onSubmit = (values: z.infer) => { + setError(''); + setSuccess(''); + startTransition(async () => { + console.log('values', values); + console.log('dispatch', dispatch); + }); }; return ( -
-
-
-
- -
- -
- VoteChain -
-

Welcome to VoteChain.

-
- Don't have an account?{' '} - - Sign up - -
-
-
-
- - setName(e.target.value)} - required - /> -
- + + +
+

Login to your account

+

+ Enter your Name below to login to your account +

+
+
+
+ ( + + Name + + + + + + )} + />
+ + +
+
+ Don't have an account?{' '} + + Sign up +
-
+ ); } diff --git a/src/config/index.ts b/src/config/index.ts index b5946e4..3840fd5 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,28 +2,108 @@ import { ethers } from 'ethers'; import { ContractABI } from '@/utils/components'; import { toast } from 'sonner'; -const contractAddress = '0x75d4f9C120f8B41EA49244e9a543fCA830e0eb22'; +const contractAddress = '0x75d4f9C120f8B41EA49244e9a543fCA830e0eb22' as const; + +interface BlockchainConnection { + provider: ethers.providers.Web3Provider; + contractInstance: ethers.Contract; + signer: ethers.Signer; + account: string; +} + +let cachedConnection: BlockchainConnection | null = null; + +const loadCachedConnection = async (): Promise => { + const storedData = sessionStorage.getItem('cachedBlockchainConnection'); + if (!storedData) return null; -const connectToBlockchain = async () => { try { - const ethereum = window.ethereum; + const { account } = JSON.parse(storedData); + + if (!window.ethereum) return null; + + const provider = new ethers.providers.Web3Provider(window.ethereum); + const signer = provider.getSigner(); + const contractInstance = new ethers.Contract(contractAddress, ContractABI, signer); - if (!ethereum) { + cachedConnection = { provider, contractInstance, signer, account }; + return cachedConnection; + } catch (error) { + console.error('Failed to restore blockchain connection:', error); + sessionStorage.removeItem('cachedBlockchainConnection'); + return null; + } +}; + +const connectToBlockchain = async (): Promise => { + try { + if (cachedConnection) { + console.log('Using cachedConnection:', cachedConnection); + return cachedConnection; + } + + const existingConnection = await loadCachedConnection(); + if (existingConnection) { + console.log('Restored connection from sessionStorage:', existingConnection); + return existingConnection; + } + + if (!window.ethereum) { toast.error('Ethereum not found, please install MetaMask'); return null; } - const provider = new ethers.providers.Web3Provider(ethereum); - await provider.send('eth_requestAccounts', []); + const provider = new ethers.providers.Web3Provider(window.ethereum); + const accounts = await provider.listAccounts(); + if (accounts.length === 0) { + await provider.send('eth_requestAccounts', []); + } + const signer = provider.getSigner(); + const account = await signer.getAddress(); const contractInstance = new ethers.Contract(contractAddress, ContractABI, signer); - return { provider, contractInstance, signer }; + cachedConnection = { provider, contractInstance, signer, account }; + sessionStorage.setItem('cachedBlockchainConnection', JSON.stringify({ account })); + + window.ethereum.on('accountsChanged', (accounts: string[]) => { + if (accounts.length === 0) { + cachedConnection = null; + sessionStorage.removeItem('cachedBlockchainConnection'); + toast.info('Disconnected from MetaMask'); + } else { + window.location.reload(); + } + }); + + window.ethereum.on('chainChanged', () => { + cachedConnection = null; + sessionStorage.removeItem('cachedBlockchainConnection'); + window.location.reload(); + }); + + toast.success('Connected to blockchain successfully'); + return cachedConnection; } catch (error) { + console.error('Connection error:', error); + const ethersError = error as { code?: number; message?: string }; + if (ethersError.code === 4001) { + toast.error('User rejected the connection request'); + } else { + console.error('Failed to connect to blockchain'); + } toast.error('Failed to connect to blockchain'); - console.error(error); return null; } }; +export const getCurrentConnection = (): BlockchainConnection | null => { + return cachedConnection; +}; + +export const disconnectFromBlockchain = (): void => { + cachedConnection = null; + sessionStorage.removeItem('cachedBlockchainConnection'); +}; + export default connectToBlockchain; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 671e867..e6e52aa 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,6 +1,19 @@ import React from 'react'; import { AuthContextProps } from './types'; -const AuthContext = React.createContext(undefined); +const defaultAuthState = { + is_connected: false, + account: '', + instance: null, + is_admin: false, + flag: false, +}; + +const noop = () => {}; + +const AuthContext = React.createContext({ + state: defaultAuthState, + dispatch: noop, +}); export default AuthContext; diff --git a/src/context/authReducer.tsx b/src/context/authReducer.tsx index 9aec257..55b91a4 100644 --- a/src/context/authReducer.tsx +++ b/src/context/authReducer.tsx @@ -10,7 +10,7 @@ export const initialState: AuthState = { export const authReducer = (state: AuthState, action: AuthAction): AuthState => { switch (action.type) { - case 'login': + case 'LOGIN': return { ...state, is_connected: true, @@ -20,7 +20,7 @@ export const authReducer = (state: AuthState, action: AuthAction): AuthState => flag: action.payload?.flag || false, }; - case 'register': + case 'REGISTER': return { ...state, flag: false }; default: diff --git a/src/context/types.ts b/src/context/types.ts index efa2295..cff81c7 100644 --- a/src/context/types.ts +++ b/src/context/types.ts @@ -9,7 +9,7 @@ export interface AuthState { } export interface AuthAction { - type: 'login' | 'register'; + type: 'LOGIN' | 'REGISTER'; payload?: Partial; } diff --git a/src/main.tsx b/src/main.tsx index 95e1ead..41c8125 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -10,7 +10,7 @@ createRoot(document.getElementById('root')!).render( - +