From b6fd5f7d280dd00661213d4df65253fa48f30a4d Mon Sep 17 00:00:00 2001 From: Rambod Goshtasbi Date: Thu, 4 Dec 2025 18:56:01 -0500 Subject: [PATCH 1/5] Refactor UI, wallet integration, Tailwind config, and WillForm validation - Integrated Core Wallet + MetaMask support through updated ContractContext - Added network checks, auto-reconnect logic, and validation improvements - Installed and configured Tailwind CSS v4 + custom navy/gold theme - Redesigned Dashboard UI (premium legal-tech styling + animated cards) - Styled and animated Sidebar with gold accents and gradients - Upgraded WillsCard to a document-style legal layout with badges - Rebuilt WillForm to remove default empty rows and support full ENS-safe validation - Added auto-trim for inputs, address validation, amount validation, and timeout checks - Fixed TextInput component (removed forced "required" causing validation bugs) - Cleaned JSX errors and improved structure across components - General UI polish using Framer Motion animations and Tailwind v4 utilities --- contracts/Will.sol | 2 +- frontend/package-lock.json | 150 +++++-- frontend/package.json | 5 +- frontend/src/App.jsx | 4 +- .../src/components/Card/DashboardCard.jsx | 37 +- frontend/src/components/Card/WillsCard.jsx | 116 ++++- .../Core/Buttons/ConnectWalletButton.jsx | 67 +-- .../src/components/Core/Form/TextInput.jsx | 1 - frontend/src/components/Sidebar.jsx | 92 +++- frontend/src/components/WillForm.jsx | 409 ++++++++++++------ frontend/src/context/ContractContext.jsx | 142 +++--- frontend/src/index.css | 11 +- frontend/src/pages/DashBoard.jsx | 76 +++- frontend/src/utils/contractAddress.js | 2 +- frontend/tailwind.config.js | 17 + 15 files changed, 799 insertions(+), 332 deletions(-) create mode 100644 frontend/tailwind.config.js diff --git a/contracts/Will.sol b/contracts/Will.sol index 0bc553f..40e2904 100644 --- a/contracts/Will.sol +++ b/contracts/Will.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.28; contract CreateWill { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d030df..98c45ae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", "ethers": "^5.7.0", + "framer-motion": "^12.23.25", "lucide-react": "^0.526.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -21,13 +22,13 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.22", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.5.6", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.17", "vite": "^5.0.8" } }, @@ -1872,6 +1873,11 @@ "tailwindcss": "4.1.11" } }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + }, "node_modules/@tailwindcss/oxide": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", @@ -2119,6 +2125,11 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2454,9 +2465,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", "dev": true, "funding": [ { @@ -2472,11 +2483,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -2514,6 +2524,15 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.2.tgz", + "integrity": "sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -2544,9 +2563,9 @@ "license": "MIT" }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -2562,12 +2581,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2637,9 +2656,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -2654,8 +2673,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -2901,11 +2919,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", - "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", - "dev": true, - "license": "ISC" + "version": "1.5.265", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.265.tgz", + "integrity": "sha512-B7IkLR1/AE+9jR2LtVF/1/6PFhY5TlnEHnlrKmGk7PvkJibg5jr+mLXLLzq3QYl6PA1T/vLDthQPqIPAlS/PPA==", + "dev": true }, "node_modules/elliptic": { "version": "6.6.1", @@ -3161,7 +3178,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -3547,19 +3563,44 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.25", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz", + "integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4874,6 +4915,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4907,11 +4961,10 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true }, "node_modules/normalize-range": { "version": "0.1.2", @@ -5193,7 +5246,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5915,10 +5967,10 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT" + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true }, "node_modules/tapable": { "version": "2.2.2", @@ -5962,6 +6014,11 @@ "dev": true, "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6086,9 +6143,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { @@ -6104,7 +6161,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" diff --git a/frontend/package.json b/frontend/package.json index 7824c53..661644b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", "ethers": "^5.7.0", + "framer-motion": "^12.23.25", "lucide-react": "^0.526.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -23,13 +24,13 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.22", "eslint": "^8.55.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.5.6", - "tailwindcss": "^4.1.11", + "tailwindcss": "^4.1.17", "vite": "^5.0.8" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 387201b..c29f48b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { ContractProvider } from './context/ContractContext' -import CreateWillForm from './components/WillForm' +import WillForm from './components/WillForm' import ExecuteWill from './components/ExecuteWill' import { ToastContainer } from 'react-toastify' import { BrowserRouter, Route, Routes } from 'react-router-dom' @@ -15,7 +15,7 @@ function App() { } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Card/DashboardCard.jsx b/frontend/src/components/Card/DashboardCard.jsx index 4a9ced3..b48ebf5 100644 --- a/frontend/src/components/Card/DashboardCard.jsx +++ b/frontend/src/components/Card/DashboardCard.jsx @@ -1,11 +1,38 @@ +import { motion } from "framer-motion"; +import PropTypes from "prop-types"; + + const DashboardCard = ({ title, value }) => { - return ( -
-

{title}

-

{value}

-
+ +

+ {title} +

+ +

+ {value} +

+
); }; +DashboardCard.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, +}; + + export default DashboardCard; diff --git a/frontend/src/components/Card/WillsCard.jsx b/frontend/src/components/Card/WillsCard.jsx index ed400b2..5866145 100644 --- a/frontend/src/components/Card/WillsCard.jsx +++ b/frontend/src/components/Card/WillsCard.jsx @@ -1,21 +1,99 @@ -import React from 'react' -import TruncatedAddress from '../TruncatedAddress' -import CountdownTimer from '../../hooks/CountdownTimer' -import { ethers } from 'ethers' +import React from "react"; +import TruncatedAddress from "../TruncatedAddress"; +import CountdownTimer from "../../hooks/CountdownTimer"; +import { ethers } from "ethers"; +import { motion } from "framer-motion"; + +const statusColors = { + "Executed": "bg-green-600/20 text-green-300 border-green-600/40", + "Active": "bg-blue-600/20 text-blue-300 border-blue-600/40", + "Ready to Execute": "bg-yellow-600/20 text-yellow-300 border-yellow-600/40", + "Expired": "bg-red-600/20 text-red-300 border-red-600/40", + "Cancelled": "bg-gray-600/20 text-gray-300 border-gray-600/40" +}; + +const WillsCard = ({ deathTimeout, status, address, balance, lastPing, timeLeft, cancelled }) => { + + const formattedBalance = parseFloat( + ethers.utils.formatEther(balance.toString()) + ).toFixed(5); + + const resolvedStatus = + cancelled === "Yes" + ? "Cancelled" + : status; -const WillsCard = ({deathTimeout, status, address, balance, lastPing, timeLeft, cancelled}) => { return ( -
- -

{parseFloat(ethers.utils.formatEther(balance.toString())).toFixed(5)} ETH

-

{status}

- {timeLeft > 0 ? - : - 'Expired' - } -

{cancelled}

-
- ) -} - -export default WillsCard \ No newline at end of file + + {/* Header */} +
+

+ Will Document +

+ + + {resolvedStatus} + +
+ + {/* Content Grid */} +
+ +
+

Owner

+ +
+ +
+

Balance

+

{formattedBalance} ETH

+
+ +
+

Time Left

+ {timeLeft > 0 ? ( + + ) : ( +

Expired

+ )} +
+ +
+

Death Timeout

+

{deathTimeout} sec

+
+ +
+

Last Ping

+

{lastPing}

+
+ +
+

Cancelled

+

{cancelled}

+
+ +
+
+ ); +}; + +export default WillsCard; diff --git a/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx b/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx index ab2fccf..89ad4f9 100644 --- a/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx +++ b/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx @@ -1,32 +1,49 @@ -import { useEffect } from 'react' -import { useContract } from '../../../context/ContractContext' -import truncate from '../../../utils/truncate' -import Button from './Button' -import ButtonText from './ButtonText' -import { toast } from 'react-toastify' +import { motion } from "framer-motion"; +import { useContract } from "../../../context/ContractContext"; +import truncate from "../../../utils/truncate"; -const ConnectWalletButton = () => { - - const { connectWallet, walletAddress, isConnected, networkError } = useContract() - useEffect(() => { - toast.error(networkError) - }, [networkError]) +const ConnectWalletButton = () => { + const { connectWallet, walletAddress, isConnected } = useContract(); return ( <> - {!isConnected ? ( - - ): ( -

{truncate(walletAddress)}

- )} + {!isConnected ? ( + + Connect Wallet + + ) : ( + + {truncate(walletAddress)} + + )} - ) -} + ); +}; -export default ConnectWalletButton \ No newline at end of file +export default ConnectWalletButton; diff --git a/frontend/src/components/Core/Form/TextInput.jsx b/frontend/src/components/Core/Form/TextInput.jsx index 5591a0e..3afd0e4 100644 --- a/frontend/src/components/Core/Form/TextInput.jsx +++ b/frontend/src/components/Core/Form/TextInput.jsx @@ -9,7 +9,6 @@ const TextInput = ({placeholder, onChange, value, type, className, ...props}) => className={clsx('bg-[#0e0e0e] text-white placeholder:text-[#666666] px-3 py-2 outline-0 rounded', className)} value={value} onChange={onChange} - required {...props} /> ) diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 811ce88..045e55a 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,27 +1,75 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import { Home, FilePlus, Trash2, RefreshCcw, Menu } from 'lucide-react'; +import React from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Home, FilePlus, RefreshCcw, Menu } from "lucide-react"; +import { motion } from "framer-motion"; + +const links = [ + { to: "/", label: "Dashboard", icon: }, + { to: "/create", label: "Create Will", icon: }, + { to: "/execute", label: "Execute Will", icon: }, + { to: "/mywill", label: "My Will", icon: }, +]; const Sidebar = () => { + const { pathname } = useLocation(); + return ( -
-

Will DApp

-
- ) -} + ); + })} + + + + ); +}; -export default Sidebar \ No newline at end of file +export default Sidebar; diff --git a/frontend/src/components/WillForm.jsx b/frontend/src/components/WillForm.jsx index 0aab015..3f5831a 100644 --- a/frontend/src/components/WillForm.jsx +++ b/frontend/src/components/WillForm.jsx @@ -1,137 +1,302 @@ -import { useState } from 'react' -import { useContract } from '../context/ContractContext'; -import { ethers } from 'ethers'; -import { toast } from 'react-toastify'; -import DashboardLayout from './DashboardLayout'; -import { Trash2, Plus } from 'lucide-react'; -import Button from './Core/Buttons/Button'; -import ButtonText from './Core/Buttons/ButtonText'; -import TextInput from './Core/Form/TextInput'; - - -const CreateWillForm = ({onCreateWill}) => { - const [beneficiaries, setBeneficiaries] = useState(['']); - const [amounts, setAmounts] = useState(['']) - const [deathTimeout, setDeathTimeout] = useState(''); - const [etherValue, setEtherValue] = useState('') - const [loading, setLoading] = useState(false) +import { useState } from "react"; +import PropTypes from "prop-types"; +import { useContract } from "../context/ContractContext"; +import { ethers } from "ethers"; +import { toast } from "react-toastify"; +import { motion, AnimatePresence } from "framer-motion"; +import DashboardLayout from "./DashboardLayout"; +import { Trash2, Plus } from "lucide-react"; +import TextInput from "./Core/Form/TextInput"; +import Button from "./Core/Buttons/Button"; +import ButtonText from "./Core/Buttons/ButtonText"; + +const WillForm = ({ onCreateWill }) => { + const [step, setStep] = useState(1); + + const [beneficiaries, setBeneficiaries] = useState([]); + const [amounts, setAmounts] = useState([]); + + const [deathTimeout, setDeathTimeout] = useState(""); + const [etherValue, setEtherValue] = useState(""); + const [loading, setLoading] = useState(false); const { contract, walletAddress } = useContract(); - const handleChange = (setter, index, value) => { - setter(prev => { - const copy = [...prev]; - copy[index] = value; - return copy; - }) - } - - const removeField = (index) => { - const updated = beneficiaries.filter((_, i) => i !== index) - setBeneficiaries(updated) - } + // -------------------------- + // Input Helpers + // -------------------------- + const handleChange = (setter, i, value) => { + setter((prev) => { + const updated = [...prev]; + updated[i] = value.trim(); // ENS-safe + return updated; + }); + }; + const addField = () => { - if (beneficiaries.length < 10) { - setBeneficiaries([...beneficiaries, '']) - setAmounts([...amounts, '']) - } - } + if (beneficiaries.length < 10) { + setBeneficiaries((prev) => [...prev, ""]); + setAmounts((prev) => [...prev, ""]); + } + }; + + const removeField = (i) => { + setBeneficiaries((prev) => prev.filter((_, idx) => idx !== i)); + setAmounts((prev) => prev.filter((_, idx) => idx !== i)); + }; + + const nextStep = () => setStep((s) => s + 1); + const prevStep = () => setStep((s) => s - 1); - const handleSubmit = async (e) => { - e.preventDefault(); - if (!contract || !walletAddress) { - toast.warn('Wallet not connected') + // -------------------------- + // Submit Handler (ENS-safe) + // -------------------------- + const handleSubmit = async () => { + if (!contract || !walletAddress) { + toast.warn("Wallet not connected"); + return; + } + + const cleanedBeneficiaries = beneficiaries.filter( + (b) => b && b.trim() !== "" + ); + const cleanedAmounts = amounts.filter((a) => a && a.trim() !== ""); + + if (cleanedBeneficiaries.length === 0) { + toast.error("Please enter at least one beneficiary."); + return; + } + + if (cleanedBeneficiaries.length !== cleanedAmounts.length) { + toast.error("Each beneficiary must have an associated amount."); + return; + } + + // Validate addresses + for (let i = 0; i < cleanedBeneficiaries.length; i++) { + if (!ethers.utils.isAddress(cleanedBeneficiaries[i])) { + toast.error(`Invalid Ethereum address at row ${i + 1}`); return; } - setLoading(true) - try { - const tx = await contract.createWill( - beneficiaries, - amounts.map(a => ethers.utils.parseEther(a)), - parseInt(deathTimeout), - {value: ethers.utils.parseEther(etherValue)} - ) - await tx.wait() - - if(onCreateWill) onCreateWill() - - toast.success('Will created successfully.') - } catch (error) { - const message = error?.error?.message || error?.message || error; - console.error('Revert reason:', message); - toast.error(message) + } + + // Validate amounts + for (let i = 0; i < cleanedAmounts.length; i++) { + if (isNaN(cleanedAmounts[i]) || Number(cleanedAmounts[i]) <= 0) { + toast.error(`Invalid ETH amount at row ${i + 1}`); + return; } - setLoading(false) - } + } + + // Validate timeout + if (!deathTimeout || isNaN(deathTimeout) || Number(deathTimeout) <= 0) { + toast.error("Death timeout must be a valid number."); + return; + } + + // Validate funding + if (!etherValue || isNaN(etherValue) || Number(etherValue) <= 0) { + toast.error("Total ETH must be a valid positive number."); + return; + } + + setLoading(true); + + try { + const tx = await contract.createWill( + cleanedBeneficiaries, + cleanedAmounts.map((amt) => ethers.utils.parseEther(amt)), + Number(deathTimeout), + { value: ethers.utils.parseEther(etherValue) } + ); + + await tx.wait(); + toast.success("Will created successfully."); + + onCreateWill?.(); + + // reset + setBeneficiaries([]); + setAmounts([]); + setDeathTimeout(""); + setEtherValue(""); + + } catch (err) { + console.error(err); + toast.error(err?.error?.message || err?.message || "Transaction failed."); + } + + setLoading(false); + }; + + const stepMotion = { + initial: { opacity: 0, x: 40 }, + animate: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: -40 }, + transition: { duration: 0.35 }, + }; return ( -
-

Create your will

-
- {beneficiaries.map((b, i) => ( -
- handleChange(setBeneficiaries, i, e.target.value)} - /> - handleChange(setAmounts, i, e.target.value)} - min={'0.01'} - max={'99.99'} - step={'0.01'} - /> - {beneficiaries.length > 1 && ( - +
+ +

Create Your Will

+ + {/* Step Indicators */} +
+ {[1, 2, 3, 4].map((n) => ( +
= n ? "bg-gold" : "bg-slate/40" + }`} + >
+ ))} +
+ + + {/* ------------ STEP 1 ------------ */} + {step === 1 && ( + +

+ 1. Beneficiaries & Allocations +

+ + {beneficiaries.map((b, i) => ( +
+ + handleChange(setBeneficiaries, i, e.target.value) + } + /> + + + handleChange(setAmounts, i, e.target.value) + } + /> + + {beneficiaries.length > 1 && ( + + )} +
+ ))} + + {beneficiaries.length < 10 && ( + + )} + +
+ +
+
+ )} + + {/* ------------ STEP 2 ------------ */} + {step === 2 && ( + +

+ 2. Inactivity Timer +

+ + setDeathTimeout(e.target.value)} + className="w-full" + /> + +
+ + +
+
+ )} + + {/* ------------ STEP 3 ------------ */} + {step === 3 && ( + +

+ 3. Deposit ETH +

+ + setEtherValue(e.target.value)} + className="w-full" + /> + +
+ + +
+
+ )} + + {/* ------------ STEP 4 ------------ */} + {step === 4 && ( + +

+ 4. Review & Confirm +

+ +
+

Beneficiaries

+ + {beneficiaries.map((b, i) => + b?.trim() ? ( +

+ {b} β€” {amounts[i]} ETH +

+ ) : null )} + +

Timeout

+

{deathTimeout} seconds

+ +

Total Funding

+

{etherValue} ETH

- ))} - {beneficiaries.length < 10 && ( - - )} - - setDeathTimeout(e.target.value)} - /> - setEtherValue(e.target.value)} - /> - - -
+ +
+ + + +
+ + )} + +
- ) -} + ); +}; + +WillForm.propTypes = { + onCreateWill: PropTypes.func, +}; -export default CreateWillForm \ No newline at end of file +export default WillForm; diff --git a/frontend/src/context/ContractContext.jsx b/frontend/src/context/ContractContext.jsx index c252412..bd2508d 100644 --- a/frontend/src/context/ContractContext.jsx +++ b/frontend/src/context/ContractContext.jsx @@ -1,103 +1,117 @@ -import { Contract, ethers } from 'ethers' -import contractAbi from '../abi/CreateWill.json' -import { createContext, useContext, useEffect, useState } from 'react'; -import { contractAddress } from '../utils/contractAddress'; -import { useMemo } from 'react'; +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { Contract, ethers } from "ethers"; +import contractAbi from "../abi/CreateWill.json"; +import { contractAddress } from "../utils/contractAddress"; - -const SEPOLIA_CHAIN_ID = '11155111'; const ContractContext = createContext(); -const ABI = contractAbi.abi; +// Allowed networks +const ALLOWED_CHAINS = ["31337", "11155111"]; + +// Detect Core β†’ Avalanche β†’ MetaMask +function getWalletSource() { + if (typeof window === "undefined") return null; + return window.core || window.avalanche || window.ethereum || null; +} -export const ContractProvider = ({children}) => { +function ContractProvider({ children }) { + const [walletAddress, setWalletAddress] = useState(null); + const [provider, setProvider] = useState(null); + const [signer, setSigner] = useState(null); const [networkError, setNetworkError] = useState(null); - const [walletAddress, setWalletAddress] = useState(null) - const [provider, setProvider] = useState(null) - const [signer, setSigner] = useState(null) + // πŸ‘‡ THIS IS WHERE isConnecting MUST BE + const [isConnecting, setIsConnecting] = useState(false); + // CONNECT WALLET + async function connectWallet() { + if (isConnecting) return; + setIsConnecting(true); - const connectWallet = async () => { - if(!window.ethereum) return alert('Metamask not installed'); + const wallet = getWalletSource(); + + if (!wallet) { + alert("Please install Core Wallet or MetaMask."); + setIsConnecting(false); + return; + } try { - const ethProvider = new ethers.providers.Web3Provider(window.ethereum); - await ethProvider.send('eth_requestAccounts', []); - const signer = ethProvider.getSigner() + const ethProvider = new ethers.providers.Web3Provider(wallet); + await wallet.request({ method: "eth_requestAccounts" }); + + const signer = ethProvider.getSigner(); const address = await signer.getAddress(); - const network = await ethProvider.getNetwork() + const network = await ethProvider.getNetwork(); - if(network.chainId.toString() !== SEPOLIA_CHAIN_ID){ - setNetworkError('Please switch to Sepolia network') + if (!ALLOWED_CHAINS.includes(network.chainId.toString())) { + setNetworkError("Wrong network. Use Sepolia or Localhost (31337)."); + setIsConnecting(false); return; } - setWalletAddress(address) - setProvider(ethProvider) - setSigner(signer) - setNetworkError(null) - + setProvider(ethProvider); + setSigner(signer); + setWalletAddress(address); + setNetworkError(null); } catch (err) { - console.error('Connection Failed:', err); + console.error("Wallet connection error:", err); } + + setIsConnecting(false); } - const disconnectWallet = () => { - setWalletAddress(''); + // DISCONNECT + function disconnectWallet() { + setWalletAddress(null); setProvider(null); - setSigner(null) - setNetworkError(null) + setSigner(null); + setNetworkError(null); } + // CONTRACT INSTANCE const contract = useMemo(() => { - if(!signer) return null; - return new Contract(contractAddress, ABI, signer); - }, [signer]) + if (!signer) return null; + return new Contract(contractAddress, contractAbi.abi, signer); + }, [signer]); + // EVENT LISTENERS useEffect(() => { - const autoConnect = async () => { - if (window.ethereum) { - // const ethProvider = new ethers.providers.Web3Provider(window.ethereum); - // const accounts = await ethProvider.listAccounts() - - // if (accounts.length > 0) { - // const signer = ethProvider.getSigner() - // const address = await signer.getAddress() - // const network = await ethProvider.getNetwork() - - // if (network.chainId.toString() === SEPOLIA_CHAIN_ID) { - // setWalletAddress(address) - // setSigner(signer) - // setProvider(ethProvider) - // setNetworkError(null) - // } else { - // setNetworkError('Please switch to Sepolia network') - // } - // } - } - window.ethereum.on('accountsChanged', () => window.location.reload()) - window.ethereum.on('chainChanged', () => window.location.reload()) - } - autoConnect() - }, []) - + const wallet = getWalletSource(); + if (!wallet?.on) return; + + const handleAccountsChanged = () => disconnectWallet(); + const handleChainChanged = () => disconnectWallet(); + + wallet.on("accountsChanged", handleAccountsChanged); + wallet.on("chainChanged", handleChainChanged); + + return () => { + wallet.removeListener?.("accountsChanged", handleAccountsChanged); + wallet.removeListener?.("chainChanged", handleChainChanged); + }; + }, []); + return ( - {children} - ) + ); +} + +function useContract() { + return useContext(ContractContext); } -export const useContract = () => useContext(ContractContext); \ No newline at end of file +export { ContractProvider, useContract }; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index a461c50..81f2d71 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1 +1,10 @@ -@import "tailwindcss"; \ No newline at end of file +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;700&family=Inter:wght@400;600;700&display=swap'); +@import "tailwindcss"; + + + +body { + background-color: #0B1B34; /* navy */ + color: white; + font-family: 'Inter', sans-serif; +} \ No newline at end of file diff --git a/frontend/src/pages/DashBoard.jsx b/frontend/src/pages/DashBoard.jsx index 3012fe3..9a60984 100644 --- a/frontend/src/pages/DashBoard.jsx +++ b/frontend/src/pages/DashBoard.jsx @@ -3,32 +3,68 @@ import DashboardCard from "../components/Card/DashboardCard"; import useGetWills from "../hooks/useGetWills"; import WillCardHeader from "../components/Card/WillCardHeader"; import WillsCard from "../components/Card/WillsCard"; - +import { ethers } from "ethers"; const Dashboard = () => { + const { wills, willsCreated, totalBalance, hasWill, willInfo } = useGetWills(); - const {wills, willsCreated, totalBalance, hasWill, willInfo} = useGetWills() + const formattedBalance = totalBalance + ? `${ethers.utils.formatEther(totalBalance)} ETH` + : "0 ETH"; return ( -
- - - -
- - {wills?.map((will, idx) => ( - 0 ? will.timeLeft : 0} - status={will.executed ? 'Executed' : will.isDead ? 'Dead - Can execute' : 'Alive'} - balance={will.balance} - lastPing={will.lastPing} - deathTimeout={will.deathTimeout} - cancelled={will.cancelled? 'Yes' : 'No'} - /> - ))} - + + {/* Header */} +
+

+ Dashboard Overview +

+

+ Summary of your will, assets, and estate activity. +

+
+ + {/* Cards */} +
+ + + + + +
+ + {/* Wills Section */} + + {wills?.map((will, idx) => ( + + ))} + +
); }; diff --git a/frontend/src/utils/contractAddress.js b/frontend/src/utils/contractAddress.js index de5ad98..4c53494 100644 --- a/frontend/src/utils/contractAddress.js +++ b/frontend/src/utils/contractAddress.js @@ -1 +1 @@ -export const contractAddress = '0x2424819Cf2288e4D348BdbC4dDe2837d324B21fF' \ No newline at end of file +export const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..7ff510a --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,17 @@ +export default { + content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + colors: { + navy: "#0B1B34", + gold: "#D4AF37", + slate: "#A0A0A0", + }, + fontFamily: { + serif: ["Playfair Display", "serif"], + sans: ["Inter", "sans-serif"], + }, + }, + }, + plugins: [], +}; From 2b60e9578e801609eda3ec2f5f617ded1cac3f2b Mon Sep 17 00:00:00 2001 From: Rambod Goshtasbi Date: Fri, 26 Dec 2025 15:52:04 -0500 Subject: [PATCH 2/5] Install Buffer and fixing WillForm error - redeploying the contract --- contracts/Will.sol | 9 +- frontend/package-lock.json | 62 +++++++++++ frontend/package.json | 1 + frontend/src/components/WillForm.jsx | 147 +++++++++++++++++++-------- frontend/src/hooks/useGetWills.jsx | 52 +++++++--- frontend/src/main.jsx | 4 + frontend/vite.config.js | 16 +++ 7 files changed, 225 insertions(+), 66 deletions(-) diff --git a/contracts/Will.sol b/contracts/Will.sol index 40e2904..7211bdd 100644 --- a/contracts/Will.sol +++ b/contracts/Will.sol @@ -35,15 +35,14 @@ contract CreateWill { function createWill(address[] memory _beneficiaries, uint256[] memory _amounts, uint256 _deathTimeout) external payable { require(usersWill[msg.sender].beneficiaries.length == 0 || usersWill[msg.sender].cancelled || usersWill[msg.sender].executed, "Will already exists"); - require(msg.value >= 1 ether, "Minimum of 1 ether required to create will"); - require(_beneficiaries.length == _amounts.length, "Bebeficiaries and amount must be of the same length"); - require(_beneficiaries.length > 0 && _amounts.length <= 10, "1 to 10 beneficiaries allowed"); + require(msg.value >= 0.01 ether, "Minimum of 0.01 ether required to create will"); // Changed from 1 ether + require(_beneficiaries.length == _amounts.length, "Beneficiaries and amount must be of the same length"); + require(_beneficiaries.length > 0 && _beneficiaries.length <= 10, "1 to 10 beneficiaries allowed"); uint256 total = 0; for (uint i = 0; i < _beneficiaries.length; i++) { require(_beneficiaries[i] != address(0), "Beneficiary address must be a valid address"); - require(_amounts[i] > 0 && _amounts[i] < 100 ether, "Invalid mount"); - + require(_amounts[i] > 0, "Amount must be greater than 0"); // Removed max limit total += _amounts[i]; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 98c45ae..e0ee74e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.11", + "buffer": "^6.0.3", "clsx": "^2.1.1", "ethers": "^5.7.0", "framer-motion": "^12.23.25", @@ -2524,6 +2525,25 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/baseline-browser-mapping": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.2.tgz", @@ -2595,6 +2615,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3939,6 +3982,25 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 661644b..080ff95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.11", + "buffer": "^6.0.3", "clsx": "^2.1.1", "ethers": "^5.7.0", "framer-motion": "^12.23.25", diff --git a/frontend/src/components/WillForm.jsx b/frontend/src/components/WillForm.jsx index 3f5831a..785a2a5 100644 --- a/frontend/src/components/WillForm.jsx +++ b/frontend/src/components/WillForm.jsx @@ -13,8 +13,8 @@ import ButtonText from "./Core/Buttons/ButtonText"; const WillForm = ({ onCreateWill }) => { const [step, setStep] = useState(1); - const [beneficiaries, setBeneficiaries] = useState([]); - const [amounts, setAmounts] = useState([]); + const [beneficiaries, setBeneficiaries] = useState([""]); + const [amounts, setAmounts] = useState([""]); const [deathTimeout, setDeathTimeout] = useState(""); const [etherValue, setEtherValue] = useState(""); @@ -28,7 +28,7 @@ const WillForm = ({ onCreateWill }) => { const handleChange = (setter, i, value) => { setter((prev) => { const updated = [...prev]; - updated[i] = value.trim(); // ENS-safe + updated[i] = value.trim(); return updated; }); }; @@ -45,58 +45,91 @@ const WillForm = ({ onCreateWill }) => { setAmounts((prev) => prev.filter((_, idx) => idx !== i)); }; - const nextStep = () => setStep((s) => s + 1); - const prevStep = () => setStep((s) => s - 1); + const nextStep = () => { + // Validate before moving to next step + if (step === 1) { + const cleanedBeneficiaries = beneficiaries.filter((b) => b && b.trim() !== ""); + const cleanedAmounts = amounts.filter((a) => a && a.trim() !== ""); - // -------------------------- - // Submit Handler (ENS-safe) - // -------------------------- - const handleSubmit = async () => { - if (!contract || !walletAddress) { - toast.warn("Wallet not connected"); - return; - } + if (cleanedBeneficiaries.length === 0) { + toast.error("Please enter at least one beneficiary."); + return; + } - const cleanedBeneficiaries = beneficiaries.filter( - (b) => b && b.trim() !== "" - ); - const cleanedAmounts = amounts.filter((a) => a && a.trim() !== ""); + if (cleanedBeneficiaries.length !== cleanedAmounts.length) { + toast.error("Each beneficiary must have an associated amount."); + return; + } - if (cleanedBeneficiaries.length === 0) { - toast.error("Please enter at least one beneficiary."); - return; - } + // Validate addresses + for (let i = 0; i < cleanedBeneficiaries.length; i++) { + const addr = cleanedBeneficiaries[i]; + + // Check if it's a valid Ethereum address + if (!addr || addr === "" || !ethers.utils.isAddress(addr)) { + toast.error(`Invalid Ethereum address at row ${i + 1}. Please enter a valid address starting with 0x`); + return; + } + + // Additional check: make sure it's not the zero address + if (addr.toLowerCase() === "0x0000000000000000000000000000000000000000") { + toast.error(`Cannot use zero address (0x0...0) at row ${i + 1}`); + return; + } + } - if (cleanedBeneficiaries.length !== cleanedAmounts.length) { - toast.error("Each beneficiary must have an associated amount."); - return; + // Validate amounts + for (let i = 0; i < cleanedAmounts.length; i++) { + const amt = cleanedAmounts[i]; + if (!amt || amt === "" || isNaN(amt) || Number(amt) <= 0) { + toast.error(`Invalid ETH amount at row ${i + 1}. Must be greater than 0`); + return; + } + } } - // Validate addresses - for (let i = 0; i < cleanedBeneficiaries.length; i++) { - if (!ethers.utils.isAddress(cleanedBeneficiaries[i])) { - toast.error(`Invalid Ethereum address at row ${i + 1}`); + if (step === 2) { + if (!deathTimeout || deathTimeout === "" || isNaN(deathTimeout) || Number(deathTimeout) <= 0) { + toast.error("Death timeout must be a valid number greater than 0."); return; } } - // Validate amounts - for (let i = 0; i < cleanedAmounts.length; i++) { - if (isNaN(cleanedAmounts[i]) || Number(cleanedAmounts[i]) <= 0) { - toast.error(`Invalid ETH amount at row ${i + 1}`); + if (step === 3) { + if (!etherValue || etherValue === "" || isNaN(etherValue) || Number(etherValue) <= 0) { + toast.error("Total ETH must be a valid positive number."); + return; + } + + // Check if total amount matches sum of beneficiary amounts + const cleanedAmounts = amounts.filter((a) => a && a.trim() !== ""); + const sumOfAmounts = cleanedAmounts.reduce((sum, amt) => sum + Number(amt), 0); + + if (Number(etherValue) < sumOfAmounts) { + toast.error(`Total ETH (${etherValue}) must be at least the sum of all beneficiary amounts (${sumOfAmounts})`); return; } } - // Validate timeout - if (!deathTimeout || isNaN(deathTimeout) || Number(deathTimeout) <= 0) { - toast.error("Death timeout must be a valid number."); + setStep((s) => s + 1); + }; + + const prevStep = () => setStep((s) => s - 1); + + // -------------------------- + // Submit Handler + // -------------------------- + const handleSubmit = async () => { + if (!contract || !walletAddress) { + toast.warn("Wallet not connected"); return; } - // Validate funding - if (!etherValue || isNaN(etherValue) || Number(etherValue) <= 0) { - toast.error("Total ETH must be a valid positive number."); + const cleanedBeneficiaries = beneficiaries.filter((b) => b && b.trim() !== ""); + const cleanedAmounts = amounts.filter((a) => a && a.trim() !== ""); + + if (cleanedBeneficiaries.length === 0) { + toast.error("Please enter at least one beneficiary."); return; } @@ -116,14 +149,16 @@ const WillForm = ({ onCreateWill }) => { onCreateWill?.(); // reset - setBeneficiaries([]); - setAmounts([]); + setBeneficiaries([""]); + setAmounts([""]); setDeathTimeout(""); setEtherValue(""); + setStep(1); } catch (err) { console.error(err); - toast.error(err?.error?.message || err?.message || "Transaction failed."); + const errorMessage = err?.error?.data?.message || err?.error?.message || err?.message || "Transaction failed."; + toast.error(errorMessage); } setLoading(false); @@ -162,6 +197,10 @@ const WillForm = ({ onCreateWill }) => { 1. Beneficiaries & Allocations +

+ Enter valid Ethereum addresses (starting with 0x) and the amount each beneficiary should receive. +

+ {beneficiaries.map((b, i) => (
{ { 2. Inactivity Timer +

+ Set the inactivity period (in seconds). If you don't "ping" within this time, your will becomes executable. +

+ setDeathTimeout(e.target.value)} className="w-full" @@ -241,13 +287,24 @@ const WillForm = ({ onCreateWill }) => { 3. Deposit ETH +

+ Enter the total amount of ETH to deposit into your will. This should equal or exceed the sum of all beneficiary amounts. +

+ setEtherValue(e.target.value)} className="w-full" /> +
+ Sum of beneficiary amounts: {amounts.filter(a => a && a.trim()).reduce((sum, amt) => sum + Number(amt || 0), 0).toFixed(4)} ETH +
+
@@ -267,14 +324,14 @@ const WillForm = ({ onCreateWill }) => { {beneficiaries.map((b, i) => b?.trim() ? ( -

+

{b} β€” {amounts[i]} ETH

) : null )}

Timeout

-

{deathTimeout} seconds

+

{deathTimeout} seconds ({(Number(deathTimeout) / 86400).toFixed(1)} days)

Total Funding

{etherValue} ETH

@@ -299,4 +356,4 @@ WillForm.propTypes = { onCreateWill: PropTypes.func, }; -export default WillForm; +export default WillForm; \ No newline at end of file diff --git a/frontend/src/hooks/useGetWills.jsx b/frontend/src/hooks/useGetWills.jsx index 0b8325c..b7748e5 100644 --- a/frontend/src/hooks/useGetWills.jsx +++ b/frontend/src/hooks/useGetWills.jsx @@ -23,6 +23,14 @@ const useGetWills = () => { } try { + // DEBUG: Check network and contract + const network = await provider.getNetwork(); + console.log("Connected to network:", network.chainId); + + const code = await provider.getCode(contractAddress); + console.log("Contract exists:", code !== "0x"); + console.log("Contract address:", contractAddress); + const filterInstance = new Contract(contractAddress, ABI, provider) const filter = filterInstance.filters.WillCreated() const events = await filterInstance.queryFilter(filter, 0, 'latest'); @@ -33,11 +41,12 @@ const useGetWills = () => { amounts: event.args.amounts.map(a => ethers.utils.formatEther(a)), balance: ethers.utils.formatEther(event.args.balance), deathTimeout: event.args.deathTimeout.toString(), - blockNUmber: event.blockNumber + blockNumber: event.blockNumber })) - const totalEther = parsed.reduce((acc, cur) => parseFloat(acc) + parseFloat(cur.balance), ethers.BigNumber.from(0)) + const totalEther = parsed.reduce((acc, cur) => parseFloat(acc) + parseFloat(cur.balance), 0) + // Check user's will const will = await contract.usersWill(walletAddress) const isCreated = will?.balance.gt(0) @@ -47,22 +56,33 @@ const useGetWills = () => { const uniqueTestators = [...new Set(testators)] - for (const addr of uniqueTestators) { - const wills = await contract.usersWill(addr) - const timeLeft = parseInt(wills.lastPing) + parseInt(wills.deathTimeout) - now; - - willList.push({ - address: addr, - ...wills, - timeLeft, - isDead: timeLeft <= 0 && !wills.executed && !wills.cancelled - }) + try { + const wills = await contract.usersWill(addr) + + // Check if will exists (has balance) + if (wills.balance.gt(0)) { + const timeLeft = parseInt(wills.lastPing) + parseInt(wills.deathTimeout) - now; + + willList.push({ + address: addr, + balance: wills.balance, + beneficiaries: wills.beneficiaries, + amounts: wills.amounts, + deathTimeout: wills.deathTimeout, + lastPing: wills.lastPing, + executed: wills.executed, + cancelled: wills.cancelled, + timeLeft, + isDead: timeLeft <= 0 && !wills.executed && !wills.cancelled + }) + } + } catch (err) { + console.warn(`Failed to fetch will for ${addr}:`, err.message) + // Continue to next address + } } - // console.log(willList) - // console.log(will[0].lastPing) - setWills(willList) setHasWill(isCreated) setWillInfo(will) @@ -71,7 +91,7 @@ const useGetWills = () => { } catch (error) { const message = error?.error?.message || error?.message || error; - console.error(message); + console.error("Full error:", error); toast.error(message) } } diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 366a14f..cda1e14 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,9 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' +// biome-ignore lint/style/useNodejsImportProtocol: browser polyfill +import { Buffer} from 'buffer' import './index.css' import 'react-toastify/dist/ReactToastify.css'; +window.Buffer = Buffer + ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c909975..d81e1b8 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,4 +5,20 @@ import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + define: { + global: 'globalThis', + }, + resolve: { + alias: { + buffer: 'buffer', + } + }, + optimizeDeps: { + esbuildOptions: { + define: { + global: 'globalThis' + } + } + } }) + From 52c34908d259b480aa8462b821fae31b54f69fce Mon Sep 17 00:00:00 2001 From: Rambod Goshtasbi Date: Sat, 27 Dec 2025 20:34:55 -0500 Subject: [PATCH 3/5] Fix: Display all wills including executed ones as transaction history - Fixed Buffer polyfill issue for ethers.js browser compatibility - Connected to Hardhat local network (Chain ID 31337) - Fixed contract address validation in will creation form - Implemented proper BigNumber formatting throughout the app - Fixed wallet connection context to include isConnecting state - Optimized useGetWills hook with fetch cooldown and caching - Updated will display logic to show ALL wills (active, executed, cancelled) as complete history - Fixed WillsCard component to properly handle and display BigNumber values - Added ExecuteWill page with gold-themed UI matching app design - Improved error handling and user feedback throughout - Added loading states to dashboard for better UX --- frontend/src/components/Card/WillsCard.jsx | 43 ++-- frontend/src/components/ExecuteWill.jsx | 210 ++++++++++++++++--- frontend/src/components/MyWill.jsx | 163 +++++++++------ frontend/src/context/ContractContext.jsx | 76 ++++++- frontend/src/hooks/useGetWills.jsx | 225 ++++++++++++--------- frontend/src/pages/DashBoard.jsx | 53 ++--- 6 files changed, 547 insertions(+), 223 deletions(-) diff --git a/frontend/src/components/Card/WillsCard.jsx b/frontend/src/components/Card/WillsCard.jsx index 5866145..01299ab 100644 --- a/frontend/src/components/Card/WillsCard.jsx +++ b/frontend/src/components/Card/WillsCard.jsx @@ -14,14 +14,24 @@ const statusColors = { const WillsCard = ({ deathTimeout, status, address, balance, lastPing, timeLeft, cancelled }) => { - const formattedBalance = parseFloat( - ethers.utils.formatEther(balance.toString()) - ).toFixed(5); + // Convert BigNumber values to readable formats + const formattedBalance = ethers.utils.formatEther( + ethers.BigNumber.isBigNumber(balance) ? balance : ethers.BigNumber.from(balance || 0) + ); + + const deathTimeoutSeconds = ethers.BigNumber.isBigNumber(deathTimeout) + ? deathTimeout.toString() + : deathTimeout?.toString() || "0"; + + const lastPingTimestamp = ethers.BigNumber.isBigNumber(lastPing) + ? lastPing.toString() + : lastPing?.toString() || "0"; + + const isCancelled = typeof cancelled === 'boolean' + ? cancelled + : cancelled === true || cancelled === "Yes"; - const resolvedStatus = - cancelled === "Yes" - ? "Cancelled" - : status; + const resolvedStatus = isCancelled ? "Cancelled" : status; return (

Balance

-

{formattedBalance} ETH

+

{parseFloat(formattedBalance).toFixed(4)} ETH

Time Left

{timeLeft > 0 ? ( - + ) : (

Expired

)} @@ -78,17 +91,21 @@ const WillsCard = ({ deathTimeout, status, address, balance, lastPing, timeLeft,

Death Timeout

-

{deathTimeout} sec

+

+ {deathTimeoutSeconds} sec ({(parseInt(deathTimeoutSeconds) / 86400).toFixed(1)} days) +

Last Ping

-

{lastPing}

+

+ {new Date(parseInt(lastPingTimestamp) * 1000).toLocaleString()} +

Cancelled

-

{cancelled}

+

{isCancelled ? "Yes" : "No"}

@@ -96,4 +113,4 @@ const WillsCard = ({ deathTimeout, status, address, balance, lastPing, timeLeft, ); }; -export default WillsCard; +export default WillsCard; \ No newline at end of file diff --git a/frontend/src/components/ExecuteWill.jsx b/frontend/src/components/ExecuteWill.jsx index f826ee1..49793b9 100644 --- a/frontend/src/components/ExecuteWill.jsx +++ b/frontend/src/components/ExecuteWill.jsx @@ -6,59 +6,209 @@ import DashboardLayout from './DashboardLayout' import Button from './Core/Buttons/Button' import ButtonText from './Core/Buttons/ButtonText' import TextInput from './Core/Form/TextInput' -import Card from './Card/Card' -import CardHeader from './Card/CardHeader' +import { motion } from 'framer-motion' +import { Skull, AlertCircle, CheckCircle } from 'lucide-react' const ExecuteWill = () => { const [testatorAddress, setTestatorAddress] = useState('') const [loading, setLoading] = useState(false) + const [willStatus, setWillStatus] = useState(null) - const {contract} = useContract() + const { contract } = useContract() + + // Check if will is ready to execute + const checkWillStatus = async () => { + if (!contract || !testatorAddress) return; + + if (!ethers.utils.isAddress(testatorAddress)) { + toast.error('Invalid Ethereum address') + return; + } + + try { + const will = await contract.usersWill(testatorAddress) + const now = Math.floor(Date.now() / 1000) + const timeLeft = parseInt(will.lastPing) + parseInt(will.deathTimeout) - now + + setWillStatus({ + exists: will.balance.gt(0), + balance: ethers.utils.formatEther(will.balance), + executed: will.executed, + cancelled: will.cancelled, + timeLeft, + canExecute: timeLeft <= 0 && !will.executed && !will.cancelled && will.balance.gt(0) + }) + } catch (error) { + console.error(error) + toast.error('Failed to fetch will status') + } + } const handleExecute = async () => { if (!contract) { - toast.warn('Contract not connected.') + toast.warn('Wallet not connected.') return; } + if (!ethers.utils.isAddress(testatorAddress)) { - toast.warn('Invalid address') + toast.error('Invalid Ethereum address') return; } + setLoading(true) try { const tx = await contract.executeWill(testatorAddress) - await tx.wait(); - toast.success('Will executed successfully!') + await tx.wait() + toast.success('Will executed successfully! Funds distributed to beneficiaries.') + + // Clear form and status + setTestatorAddress('') + setWillStatus(null) } catch (error) { - const message = error?.error?.message || error?.message || error; - console.error(message); + const message = error?.error?.data?.message || error?.error?.message || error?.message || 'Execution failed'; + console.error(error) toast.error(message) } finally { setLoading(false) } } - return ( - - - Execute a Wil - setTestatorAddress(e.target.value)} - /> - - - - ) +
+ +

+ Testator Information +

+ + {/* Address Input */} +
+ + setTestatorAddress(e.target.value)} + /> +

+ Enter the address of the deceased testator whose will you want to execute. +

+
+ + {/* Check Status Button */} + + + {/* Will Status Display */} + {willStatus && ( + +
+ {willStatus.canExecute ? ( + + ) : ( + + )} + +
+

+ {willStatus.canExecute + ? 'βœ“ Will Ready for Execution' + : willStatus.exists && !willStatus.executed && !willStatus.cancelled + ? '⏳ Will Not Yet Executable' + : 'βœ— Will Cannot Be Executed'} +

+ +
+

Balance: {willStatus.balance} ETH

+ + {willStatus.executed && ( +

⚠️ This will has already been executed

+ )} + + {willStatus.cancelled && ( +

⚠️ This will has been cancelled

+ )} + + {!willStatus.exists && ( +

⚠️ No will exists for this address

+ )} + + {willStatus.timeLeft > 0 && !willStatus.executed && !willStatus.cancelled && ( +

+ ⏰ Time remaining: {Math.floor(willStatus.timeLeft / 86400)} days, {Math.floor((willStatus.timeLeft % 86400) / 3600)} hours +

+ )} + + {willStatus.canExecute && ( +

+ βœ“ The death timeout has expired. You can now execute this will. +

+ )} +
+
+
+
+ )} + + {/* Execute Button */} + + + {/* Warning Notice */} +
+

+ ⚠️ Important: Anyone can execute a will once the death timeout has expired. + This action is irreversible and will immediately distribute all assets to the designated beneficiaries. +

+
+
+ + + ) } export default ExecuteWill \ No newline at end of file diff --git a/frontend/src/components/MyWill.jsx b/frontend/src/components/MyWill.jsx index bbd787c..776860c 100644 --- a/frontend/src/components/MyWill.jsx +++ b/frontend/src/components/MyWill.jsx @@ -7,71 +7,118 @@ import CreateWillForm from './WillForm' import { useContract } from '../context/ContractContext' import DashboardLayout from './DashboardLayout' import useGetWills from '../hooks/useGetWills' +import { ethers } from 'ethers' const MyWill = () => { - const [loading, setLoading] = useState(true) - const [beneficiaries, setBeneficiaries] = useState(null) - const [amounts, setAmounts] = useState(null) - - const { walletAddress, contract} = useContract() - const {willInfo, hasWill, fetchAllWills} = useGetWills() - - const fetchWillInfo = async () => { - if (!contract || !walletAddress) return - - try { - const filter = await contract.filters.WillCreated(walletAddress) - const logs = await contract.queryFilter(filter) - - if (logs?.length > 0) { - const willEvent = logs[logs.length - 1] - const { beneficiaries, amounts, } = willEvent.args - - setBeneficiaries(beneficiaries) - setAmounts(amounts) - - if(fetchAllWills) fetchAllWills(); - - } - - } catch (error) { - const message = error?.error?.message || error?.message || error; - console.error(message); - toast.error(message) - } finally { - setLoading(false) - } + const [loading, setLoading] = useState(true) + const [history, setHistory] = useState([]) + + const { walletAddress, contract } = useContract() + const { willInfo, hasWill, fetchAllWills } = useGetWills() + + const fetchWillHistory = async () => { + if (!contract || !walletAddress) return + + try { + setLoading(true) + + // πŸ”₯ ALL wills ever created by THIS wallet + const filter = contract.filters.WillCreated(walletAddress) + const logs = await contract.queryFilter(filter, 0, 'latest') + + const formatted = logs.map((log, index) => ({ + id: `${log.transactionHash}-${index}`, + blockNumber: log.blockNumber, + beneficiaries: log.args.beneficiaries, + amounts: log.args.amounts.map(a => + ethers.utils.formatEther(a) + ), + balance: ethers.utils.formatEther(log.args.balance), + deathTimeout: log.args.deathTimeout.toString(), + txHash: log.transactionHash, + })) + + setHistory(formatted) + + // refresh global state if needed + if (fetchAllWills) fetchAllWills() + } catch (err) { + const message = err?.error?.message || err?.message || err + console.error(message) + toast.error(message) + } finally { + setLoading(false) } + } - useEffect(() => { - fetchWillInfo(); - }, [contract, walletAddress]) + useEffect(() => { + fetchWillHistory() + }, [contract, walletAddress]) return ( - <> -
- {hasWill ? ( - - {loading ? ( -
Loading Will data...
- ):( -
- -
- - -
+
+ {hasWill ? ( + + {loading ? ( +
+ Loading Will data... +
+ ) : ( + <> + {/* CURRENT WILL */} +
+ +
+ + +
+
+ + {/* HISTORY */} +
+

+ Will History +

+ + {history.length === 0 ? ( +

+ No previous wills found. +

+ ) : ( +
+ {history.map(will => ( +
+
+ Block #{will.blockNumber} +
+ +
+ Balance: {will.balance} ETH +
+ +
+ Beneficiaries: {will.beneficiaries.length} +
+ +
+ TX: {will.txHash}
- ) - } - - ) : ( - - )} -
- - +
+ ))} +
+ )} +
+ + )} + + ) : ( + + )} +
) } -export default MyWill \ No newline at end of file +export default MyWill diff --git a/frontend/src/context/ContractContext.jsx b/frontend/src/context/ContractContext.jsx index bd2508d..d3e7165 100644 --- a/frontend/src/context/ContractContext.jsx +++ b/frontend/src/context/ContractContext.jsx @@ -6,7 +6,7 @@ import { contractAddress } from "../utils/contractAddress"; const ContractContext = createContext(); // Allowed networks -const ALLOWED_CHAINS = ["31337", "11155111"]; +const ALLOWED_CHAINS = ["31337", "11155111"]; // Hardhat Local, Sepolia // Detect Core β†’ Avalanche β†’ MetaMask function getWalletSource() { @@ -19,8 +19,6 @@ function ContractProvider({ children }) { const [provider, setProvider] = useState(null); const [signer, setSigner] = useState(null); const [networkError, setNetworkError] = useState(null); - - // πŸ‘‡ THIS IS WHERE isConnecting MUST BE const [isConnecting, setIsConnecting] = useState(false); // CONNECT WALLET @@ -44,6 +42,9 @@ function ContractProvider({ children }) { const address = await signer.getAddress(); const network = await ethProvider.getNetwork(); + console.log("πŸ”— Connected to network:", network.chainId); + console.log("πŸ‘› Wallet address:", address); + if (!ALLOWED_CHAINS.includes(network.chainId.toString())) { setNetworkError("Wrong network. Use Sepolia or Localhost (31337)."); setIsConnecting(false); @@ -54,8 +55,11 @@ function ContractProvider({ children }) { setSigner(signer); setWalletAddress(address); setNetworkError(null); + + console.log("βœ… Wallet connected successfully"); } catch (err) { - console.error("Wallet connection error:", err); + console.error("❌ Wallet connection error:", err); + alert("Failed to connect wallet. Please try again."); } setIsConnecting(false); @@ -67,6 +71,7 @@ function ContractProvider({ children }) { setProvider(null); setSigner(null); setNetworkError(null); + console.log("πŸ”Œ Wallet disconnected"); } // CONTRACT INSTANCE @@ -75,13 +80,71 @@ function ContractProvider({ children }) { return new Contract(contractAddress, contractAbi.abi, signer); }, [signer]); + // AUTO-CONNECT IF PREVIOUSLY CONNECTED + useEffect(() => { + let mounted = true; + + const checkConnection = async () => { + if (!mounted) return; + + const wallet = getWalletSource(); + if (!wallet) return; + + try { + const accounts = await wallet.request({ method: "eth_accounts" }); + if (accounts.length > 0 && mounted) { + console.log("πŸ”„ Auto-connecting to previously connected wallet..."); + + // Don't call connectWallet() - just set up the connection directly + const ethProvider = new ethers.providers.Web3Provider(wallet); + const signer = ethProvider.getSigner(); + const address = await signer.getAddress(); + const network = await ethProvider.getNetwork(); + + console.log("πŸ”— Auto-connected to network:", network.chainId); + + if (!ALLOWED_CHAINS.includes(network.chainId.toString())) { + setNetworkError("Wrong network. Use Sepolia or Localhost (31337)."); + return; + } + + setProvider(ethProvider); + setSigner(signer); + setWalletAddress(address); + setNetworkError(null); + } + } catch (err) { + console.error("Auto-connect error:", err); + } + }; + + checkConnection(); + + return () => { + mounted = false; + }; + }, []); + // EVENT LISTENERS useEffect(() => { const wallet = getWalletSource(); if (!wallet?.on) return; - const handleAccountsChanged = () => disconnectWallet(); - const handleChainChanged = () => disconnectWallet(); + const handleAccountsChanged = (accounts) => { + console.log("πŸ‘€ Accounts changed:", accounts); + if (accounts.length === 0) { + disconnectWallet(); + } else { + // Reconnect with new account + connectWallet(); + } + }; + + const handleChainChanged = (chainId) => { + console.log("⛓️ Chain changed:", chainId); + // Reload the page to reset state + window.location.reload(); + }; wallet.on("accountsChanged", handleAccountsChanged); wallet.on("chainChanged", handleChainChanged); @@ -103,6 +166,7 @@ function ContractProvider({ children }) { disconnectWallet, networkError, isConnected: !!walletAddress, + isConnecting, // βœ… NOW INCLUDED! }} > {children} diff --git a/frontend/src/hooks/useGetWills.jsx b/frontend/src/hooks/useGetWills.jsx index b7748e5..da10bb3 100644 --- a/frontend/src/hooks/useGetWills.jsx +++ b/frontend/src/hooks/useGetWills.jsx @@ -1,106 +1,145 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useState, useCallback, useRef } from 'react' import { useContract } from '../context/ContractContext' import { toast } from 'react-toastify' import { Contract, ethers } from 'ethers' import { contractAddress } from '../utils/contractAddress' import contractAbi from '../abi/CreateWill.json' -const ABI = contractAbi.abi; +const ABI = contractAbi.abi +const ZERO = ethers.BigNumber.from(0) + +// πŸ”’ HARDENED NORMALIZER (FINAL FORM) +const normalizeWill = (w) => { + const beneficiaries = Array.isArray(w?.[0]) ? w[0] : [] + const rawAmounts = w?.[1] + + const amounts = Array.isArray(rawAmounts) + ? rawAmounts + : rawAmounts + ? [rawAmounts] + : [] + + return { + beneficiaries, + amounts, + executed: Boolean(w?.[2]), + lastPing: Number(w?.[3] ?? 0), + cancelled: Boolean(w?.[4]), + balance: ethers.BigNumber.isBigNumber(w?.[5]) ? w[5] : ZERO, + deathTimeout: Number(w?.[6] ?? 0), + } +} const useGetWills = () => { - const [wills, setWills] = useState([]) - const [willInfo, setWillInfo] = useState(null) - const [hasWill, setHasWill] = useState(false) - const [totalBalance, setTotalBalance] = useState(0) - const [willsCreated, setWillsCreated] = useState(0) - - const {contract, provider, walletAddress, isConnected} = useContract() - - const fetchAllWills = async () => { - if (!contract || !walletAddress) { - toast.warn("Connect Wallet") - return; - } - + const [wills, setWills] = useState([]) + const [willInfo, setWillInfo] = useState(null) + const [hasWill, setHasWill] = useState(false) + const [totalBalance, setTotalBalance] = useState(0) + const [willsCreated, setWillsCreated] = useState(0) + const [loading, setLoading] = useState(false) + + const { contract, provider, walletAddress, isConnected } = useContract() + const isFetching = useRef(false) + + const fetchAllWills = useCallback(async () => { + if (!contract || !provider || !walletAddress || isFetching.current) return + isFetching.current = true + setLoading(true) + + try { + /* ---------- EVENTS ---------- */ + const filterInstance = new Contract(contractAddress, ABI, provider) + const events = await filterInstance.queryFilter( + filterInstance.filters.WillCreated(), + 0, + 'latest' + ) + + setWillsCreated(events.length) + + setTotalBalance( + events.reduce( + (acc, e) => + acc + parseFloat(ethers.utils.formatEther(e.args.balance)), + 0 + ) + ) + + /* ---------- USER WILL ---------- */ + const userRaw = await contract.usersWill(walletAddress) + const userWill = normalizeWill(userRaw) + + setHasWill( + userWill.balance.gt(0) && + !userWill.executed && + !userWill.cancelled + ) + setWillInfo(userWill) + + /* ---------- ALL WILLS ---------- */ + const testators = await contract.getAllTestators() + const uniqueTestators = [...new Set(testators)] + const now = Math.floor(Date.now() / 1000) + + const willList = [] + + for (const addr of uniqueTestators) { try { - // DEBUG: Check network and contract - const network = await provider.getNetwork(); - console.log("Connected to network:", network.chainId); - - const code = await provider.getCode(contractAddress); - console.log("Contract exists:", code !== "0x"); - console.log("Contract address:", contractAddress); - - const filterInstance = new Contract(contractAddress, ABI, provider) - const filter = filterInstance.filters.WillCreated() - const events = await filterInstance.queryFilter(filter, 0, 'latest'); - - const parsed = events.map(event => ({ - testator: event.args.testator, - beneficiaries: event.args.beneficiaries, - amounts: event.args.amounts.map(a => ethers.utils.formatEther(a)), - balance: ethers.utils.formatEther(event.args.balance), - deathTimeout: event.args.deathTimeout.toString(), - blockNumber: event.blockNumber - })) - - const totalEther = parsed.reduce((acc, cur) => parseFloat(acc) + parseFloat(cur.balance), 0) - - // Check user's will - const will = await contract.usersWill(walletAddress) - const isCreated = will?.balance.gt(0) - - const testators = await contract.getAllTestators() - const now = Math.floor(Date.now() / 1000) - const willList = [] - - const uniqueTestators = [...new Set(testators)] - - for (const addr of uniqueTestators) { - try { - const wills = await contract.usersWill(addr) - - // Check if will exists (has balance) - if (wills.balance.gt(0)) { - const timeLeft = parseInt(wills.lastPing) + parseInt(wills.deathTimeout) - now; - - willList.push({ - address: addr, - balance: wills.balance, - beneficiaries: wills.beneficiaries, - amounts: wills.amounts, - deathTimeout: wills.deathTimeout, - lastPing: wills.lastPing, - executed: wills.executed, - cancelled: wills.cancelled, - timeLeft, - isDead: timeLeft <= 0 && !wills.executed && !wills.cancelled - }) - } - } catch (err) { - console.warn(`Failed to fetch will for ${addr}:`, err.message) - // Continue to next address - } - } - - setWills(willList) - setHasWill(isCreated) - setWillInfo(will) - setTotalBalance(totalEther) - setWillsCreated(parsed?.length) - - } catch (error) { - const message = error?.error?.message || error?.message || error; - console.error("Full error:", error); - toast.error(message) + const raw = await contract.usersWill(addr) + const will = normalizeWill(raw) + + const timeLeft = + will.lastPing + will.deathTimeout - now + + willList.push({ + address: addr, + balanceEth: ethers.utils.formatEther(will.balance), + beneficiaries: will.beneficiaries, + amounts: will.amounts.map((a) => + ethers.utils.formatEther(a) + ), + executed: will.executed, + cancelled: will.cancelled, + lastPing: will.lastPing, + deathTimeout: will.deathTimeout, + timeLeft, + status: will.executed + ? 'EXECUTED' + : will.cancelled + ? 'CANCELLED' + : will.balance.gt(0) + ? 'ACTIVE' + : 'EMPTY', + }) + } catch (e) { + console.error('Skipping broken will:', addr, e) } + } + + setWills(willList) + console.log('βœ… Wills loaded:', willList.length) + } catch (err) { + console.error('❌ Fetch error:', err) + toast.error('Failed to load wills') + } finally { + setLoading(false) + isFetching.current = false } - - useEffect(() => { - fetchAllWills() - }, [contract, isConnected, walletAddress]) - - return {fetchAllWills, wills, willsCreated, totalBalance, hasWill, willInfo} + }, [contract, provider, walletAddress]) + + useEffect(() => { + if (isConnected) fetchAllWills() + }, [isConnected, fetchAllWills]) + + return { + fetchAllWills, + wills, + willsCreated, + totalBalance, + hasWill, + willInfo, + loading, + } } -export default useGetWills \ No newline at end of file +export default useGetWills diff --git a/frontend/src/pages/DashBoard.jsx b/frontend/src/pages/DashBoard.jsx index 9a60984..7340cdf 100644 --- a/frontend/src/pages/DashBoard.jsx +++ b/frontend/src/pages/DashBoard.jsx @@ -3,13 +3,13 @@ import DashboardCard from "../components/Card/DashboardCard"; import useGetWills from "../hooks/useGetWills"; import WillCardHeader from "../components/Card/WillCardHeader"; import WillsCard from "../components/Card/WillsCard"; -import { ethers } from "ethers"; const Dashboard = () => { const { wills, willsCreated, totalBalance, hasWill, willInfo } = useGetWills(); + // totalBalance is already formatted as a number from useGetWills const formattedBalance = totalBalance - ? `${ethers.utils.formatEther(totalBalance)} ETH` + ? `${parseFloat(totalBalance).toFixed(4)} ETH` : "0 ETH"; return ( @@ -27,7 +27,7 @@ const Dashboard = () => { {/* Cards */}
- + @@ -35,7 +35,7 @@ const Dashboard = () => { title="My Will Status" value={ hasWill - ? willInfo.executed + ? willInfo?.executed ? "Executed" : "Active" : "No Will Created" @@ -45,28 +45,35 @@ const Dashboard = () => { {/* Wills Section */} - {wills?.map((will, idx) => ( - - ))} + {wills && wills.length > 0 ? ( + wills.map((will, idx) => ( + + )) + ) : ( +
+

No wills found

+

Create your first will to get started

+
+ )}
); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file From e84fdfa70b37e26b2ca23f8653eb1d0a8b998e65 Mon Sep 17 00:00:00 2001 From: Rambod Goshtasbi Date: Mon, 12 Jan 2026 18:14:09 -0500 Subject: [PATCH 4/5] Frontend refactor & UX stabilization for Digital Will dApp Refactored and stabilized the frontend of an Ethereum-based Digital Will dApp. Key improvements include: - UI redesign with a notary / law-firm inspired aesthetic - Animated and hardened wallet connection (MetaMask + Core Wallet) - Improved dashboard layout and data presentation - Defensive handling of smart contract edge cases - Reduced crashes from BigNumber, signer, and lifecycle inconsistencies - Improved Web3 UX without modifying the original smart contract Smart contract logic intentionally left unchanged. Project deployed and demo-ready for portfolio use. --- README.md | 126 ++++++++++--- contracts/Will.sol | 2 +- .../Core/Buttons/ConnectWalletButton.jsx | 62 +++--- frontend/src/components/MyWill.jsx | 177 +++++++++--------- frontend/src/context/ContractContext.jsx | 100 +++------- frontend/src/hooks/CountdownTimer.jsx | 80 ++++---- frontend/src/hooks/useGetWills.jsx | 158 ++++++++-------- frontend/vite.config.js | 16 -- 8 files changed, 380 insertions(+), 341 deletions(-) diff --git a/README.md b/README.md index 1cc026c..abf73b9 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,114 @@ -# Digital Last Will +# Digital Will dApp β€” Frontend Refactor & UX Stabilization -## Inspired by -- Alchemy University Certification Project +## Overview -**Digital Last Will** is a decentralized application that allows individuals to create a smart contract-powered digital will. The system ensures that assets (ETH or ERC20 tokens) are securely distributed to designated beneficiaries if the original owner becomes inactive for a specified period. +This project is a **frontend refactor and UX stabilization** of an existing Ethereum-based **Digital Will decentralized application**. -> Secure your assets. Automate your legacy. Trust code, not courts. +The original application enables users to create a digital will that distributes ETH to beneficiaries after a defined period of inactivity. +This fork focuses on improving **usability, visual clarity, wallet reliability, and frontend robustness** β€” without modifying the original smart contract logic. -## Features +The project is considered **feature-complete for frontend improvements** and is suitable for portfolio presentation. -### Core Features -- **Create a Digital Will** with multiple beneficiaries and custom distribution percentages -- **Deposit ETH** directly into your will -- **Ping Mechanism** to prove you’re still active ("alive") -- **Automated Will Execution** after a configurable period of inactivity -- **Manual Executor Access**: Any user can trigger the will once it's eligible -- **View, Update or Cancel** your will at any time (optional) +--- + +## What Was Done + +### Frontend & UX Improvements + +- Refactored dashboard layout and visual hierarchy +- Introduced a clean **law-firm / notary-inspired UI** +- Added an animated, accessible **Connect Wallet** button using Framer Motion +- Improved sidebar navigation and reusable card components +- Hardened UI rendering against invalid or partial contract state +- Defensive formatting for BigNumber values and timestamps +- Reduced UI crashes caused by undefined or cleared contract data + +### Wallet & Web3 Integration + +- Stabilized wallet connection logic +- Supported **MetaMask** and **Core Wallet** +- Prevented duplicate `eth_requestAccounts` calls +- Improved network validation and connection feedback +- Reduced common Web3 frontend issues (BigNumber overflow, stale signer, reload loops) + +### Code Quality & Reliability + +- Normalized smart contract return data +- Improved custom hooks (`useGetWills`) for safer reads +- Clear separation of concerns: + - Context (wallet & provider) + - Hooks (contract reads) + - UI components +- Added defensive guards to prevent runtime crashes + +--- + +## Known Limitation (Intentional) + +The **My Will** page may show inconsistent state in edge cases (e.g. cancelled or executed wills). + +This is due to **semantic ambiguity in the original smart contract**, where: + +- Cancelled or executed wills clear beneficiary data +- The same mapping slot is reused +- Frontend cannot reliably infer lifecycle history without additional contract state + +Fixing this correctly would require **smart contract redesign**, which was intentionally **out of scope** for this frontend-focused refactor. + +The **Dashboard** reflects aggregate state correctly. +Deeper lifecycle semantics are deferred by design. +--- ## Tech Stack -| Layer | Tech | -|-------------|--------------------------| -| Smart Contract | Solidity | -| Framework | Hardhat | -| Frontend | React.js, Tailwind CSS | -| Blockchain | Ethereum Sepolia Testnet | -| Wallet | MetaMask | +### Frontend + +- **React** (Vite) +- **Tailwind CSS** +- **Framer Motion** +- **React Router** +- **Lucide Icons** + +### Web3 + +- **ethers.js (v5)** +- **MetaMask** +- **Core Wallet** +- **Hardhat** (local development & testing) + +### Tooling + +- **Vite** +- **ESLint** +- **GitHub Desktop** + +--- + +## Project Status + +- βœ… Frontend refactor complete +- βœ… Wallet integration stabilized +- βœ… Deployed and demo-ready +- ❌ Smart contract redesign intentionally out of scope + +--- + +## Why This Project Matters + +This project demonstrates the ability to: + +- Refactor and stabilize an existing Web3 codebase +- Improve UX under smart contract constraints +- Debug real-world dApp integration issues +- Balance engineering quality with delivery +- Know when to ship instead of over-engineering --- -## Getting Started +## Notes -### 1. Clone the Repo +This repository represents a **frontend-focused contribution** to an existing dApp. +All smart contract logic remains unchanged from the original implementation. -```bash -git clone https://github.com/papilo-cloud/digital-will.git -cd digital-will -``` \ No newline at end of file +--- diff --git a/contracts/Will.sol b/contracts/Will.sol index 7211bdd..858ab5d 100644 --- a/contracts/Will.sol +++ b/contracts/Will.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; +pragma solidity ^0.8.20; contract CreateWill { diff --git a/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx b/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx index 89ad4f9..98a1ffb 100644 --- a/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx +++ b/frontend/src/components/Core/Buttons/ConnectWalletButton.jsx @@ -2,42 +2,62 @@ import { motion } from "framer-motion"; import { useContract } from "../../../context/ContractContext"; import truncate from "../../../utils/truncate"; - const ConnectWalletButton = () => { - const { connectWallet, walletAddress, isConnected } = useContract(); + const { connectWallet, walletAddress, isConnected, isConnecting } = useContract(); return ( <> {!isConnected ? ( - Connect Wallet + {isConnecting ? "Connecting…" : "Connect Wallet"} ) : ( {truncate(walletAddress)} diff --git a/frontend/src/components/MyWill.jsx b/frontend/src/components/MyWill.jsx index 776860c..0610808 100644 --- a/frontend/src/components/MyWill.jsx +++ b/frontend/src/components/MyWill.jsx @@ -1,124 +1,117 @@ -import { useEffect, useState } from 'react' -import { toast } from 'react-toastify' -import UserWill from './UserWill' -import PingWill from './PingWill' -import CancelWill from './CancelWill' -import CreateWillForm from './WillForm' -import { useContract } from '../context/ContractContext' -import DashboardLayout from './DashboardLayout' -import useGetWills from '../hooks/useGetWills' -import { ethers } from 'ethers' +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { Link } from "react-router-dom"; +import { ethers } from "ethers"; + +import UserWill from "./UserWill"; +import PingWill from "./PingWill"; +import CancelWill from "./CancelWill"; +import DashboardLayout from "./DashboardLayout"; + +import { useContract } from "../context/ContractContext"; +import useGetWills from "../hooks/useGetWills"; const MyWill = () => { - const [loading, setLoading] = useState(true) - const [history, setHistory] = useState([]) + const [loading, setLoading] = useState(true); + const [history, setHistory] = useState([]); - const { walletAddress, contract } = useContract() - const { willInfo, hasWill, fetchAllWills } = useGetWills() + const { walletAddress, contract } = useContract(); + const { willInfo, hasActiveWill, hasEverCreatedWill, fetchAllWills } = useGetWills(); const fetchWillHistory = async () => { - if (!contract || !walletAddress) return + if (!contract || !walletAddress) return; try { - setLoading(true) + setLoading(true); - // πŸ”₯ ALL wills ever created by THIS wallet - const filter = contract.filters.WillCreated(walletAddress) - const logs = await contract.queryFilter(filter, 0, 'latest') + // All wills created by THIS wallet (events history) + const filter = contract.filters.WillCreated(walletAddress); + const logs = await contract.queryFilter(filter, 0, "latest"); const formatted = logs.map((log, index) => ({ id: `${log.transactionHash}-${index}`, blockNumber: log.blockNumber, beneficiaries: log.args.beneficiaries, - amounts: log.args.amounts.map(a => - ethers.utils.formatEther(a) - ), + amounts: log.args.amounts.map((a) => ethers.utils.formatEther(a)), balance: ethers.utils.formatEther(log.args.balance), deathTimeout: log.args.deathTimeout.toString(), txHash: log.transactionHash, - })) + })); - setHistory(formatted) + setHistory(formatted); - // refresh global state if needed - if (fetchAllWills) fetchAllWills() + // refresh global state + if (fetchAllWills) fetchAllWills(); } catch (err) { - const message = err?.error?.message || err?.message || err - console.error(message) - toast.error(message) + const message = err?.error?.message || err?.message || err; + console.error(message); + toast.error(message); } finally { - setLoading(false) + setLoading(false); } - } + }; useEffect(() => { - fetchWillHistory() - }, [contract, walletAddress]) + fetchWillHistory(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contract, walletAddress]); return ( -
- {hasWill ? ( - - {loading ? ( -
- Loading Will data... + + {loading ? ( +
Loading Will data...
+ ) : ( + <> + {/* TOP SECTION: Active vs Not Active */} + {hasActiveWill ? ( +
+ +
+ + +
) : ( - <> - {/* CURRENT WILL */} -
- -
- - -
-
+
+

No Active Will

+

+ {hasEverCreatedWill + ? "Your previous will was cancelled or executed. You can create a new one anytime." + : "You haven’t created a digital will yet."} +

+ + + Create Your Will + +
+ )} - {/* HISTORY */} -
-

- Will History -

- - {history.length === 0 ? ( -

- No previous wills found. -

- ) : ( -
- {history.map(will => ( -
-
- Block #{will.blockNumber} -
- -
- Balance: {will.balance} ETH -
- -
- Beneficiaries: {will.beneficiaries.length} -
- -
- TX: {will.txHash} -
-
- ))} + {/* HISTORY SECTION (show if any) */} +
+

Will History

+ + {history.length === 0 ? ( +

No previous wills found.

+ ) : ( +
+ {history.map((w) => ( +
+
Block #{w.blockNumber}
+
Balance: {w.balance} ETH
+
Beneficiaries: {w.beneficiaries.length}
+
TX: {w.txHash}
- )} + ))}
- - )} - - ) : ( - + )} +
+ )} -
- ) -} + + ); +}; -export default MyWill +export default MyWill; diff --git a/frontend/src/context/ContractContext.jsx b/frontend/src/context/ContractContext.jsx index d3e7165..86b5306 100644 --- a/frontend/src/context/ContractContext.jsx +++ b/frontend/src/context/ContractContext.jsx @@ -82,78 +82,40 @@ function ContractProvider({ children }) { // AUTO-CONNECT IF PREVIOUSLY CONNECTED useEffect(() => { - let mounted = true; - - const checkConnection = async () => { - if (!mounted) return; - - const wallet = getWalletSource(); - if (!wallet) return; - - try { - const accounts = await wallet.request({ method: "eth_accounts" }); - if (accounts.length > 0 && mounted) { - console.log("πŸ”„ Auto-connecting to previously connected wallet..."); - - // Don't call connectWallet() - just set up the connection directly - const ethProvider = new ethers.providers.Web3Provider(wallet); - const signer = ethProvider.getSigner(); - const address = await signer.getAddress(); - const network = await ethProvider.getNetwork(); - - console.log("πŸ”— Auto-connected to network:", network.chainId); - - if (!ALLOWED_CHAINS.includes(network.chainId.toString())) { - setNetworkError("Wrong network. Use Sepolia or Localhost (31337)."); - return; - } - - setProvider(ethProvider); - setSigner(signer); - setWalletAddress(address); - setNetworkError(null); - } - } catch (err) { - console.error("Auto-connect error:", err); - } - }; + const wallet = getWalletSource(); + if (!wallet?.on) return; - checkConnection(); + const handleAccountsChanged = async (accounts) => { + console.log("πŸ‘€ Accounts changed:", accounts); - return () => { - mounted = false; - }; - }, []); + if (!accounts || accounts.length === 0) { + return; + } - // EVENT LISTENERS - useEffect(() => { - const wallet = getWalletSource(); - if (!wallet?.on) return; - - const handleAccountsChanged = (accounts) => { - console.log("πŸ‘€ Accounts changed:", accounts); - if (accounts.length === 0) { - disconnectWallet(); - } else { - // Reconnect with new account - connectWallet(); - } - }; - - const handleChainChanged = (chainId) => { - console.log("⛓️ Chain changed:", chainId); - // Reload the page to reset state - window.location.reload(); - }; - - wallet.on("accountsChanged", handleAccountsChanged); - wallet.on("chainChanged", handleChainChanged); - - return () => { - wallet.removeListener?.("accountsChanged", handleAccountsChanged); - wallet.removeListener?.("chainChanged", handleChainChanged); - }; - }, []); + const ethProvider = new ethers.providers.Web3Provider(wallet); + const signer = ethProvider.getSigner(); + + setProvider(ethProvider); + setSigner(signer); + setWalletAddress(accounts[0]); + setNetworkError(null); + + console.log("βœ… Rebound signer & provider after account change"); + }; + + const handleChainChanged = () => { + console.log("⛓️ Chain changed"); + window.location.reload(); + }; + + wallet.on("accountsChanged", handleAccountsChanged); + wallet.on("chainChanged", handleChainChanged); + + return () => { + wallet.removeListener("accountsChanged", handleAccountsChanged); + wallet.removeListener("chainChanged", handleChainChanged); + }; +}, []); return ( { - const [timeleft, setTimeLeft] = useState(0) - - useEffect(() => { - const lastPingTime = BigNumber.from(lastPing).toNumber() - const timeOut = BigNumber.from(deathTimeout).toNumber() - const deathTimeStamp = lastPingTime + timeOut - - const updateCountdown = () => { - const now = Math.floor(Date.now() / 1000) - const remaining = deathTimeStamp - now - setTimeLeft(remaining > 0 ? remaining : 0) - } - - updateCountdown() - const interval = setInterval(updateCountdown, 1000) - - return () => clearInterval(interval) - }, [lastPing, deathTimeout]) - - return ( -
- {children} {formatTime(timeleft)} -
- ) -} - -export default CountdownTimer \ No newline at end of file +import { useEffect, useState } from "react"; +import { ethers } from "ethers"; + +const CountdownTimer = ({ lastPing, deathTimeout }) => { + const [timeLeft, setTimeLeft] = useState(0); + + useEffect(() => { + if (!lastPing || !deathTimeout) return; + + // βœ… ENSURE BigNumber math, not JS math + const lastPingBN = ethers.BigNumber.from(lastPing); + const timeoutBN = ethers.BigNumber.from(deathTimeout); + + const endTime = lastPingBN.add(timeoutBN); + + const tick = () => { + const now = Math.floor(Date.now() / 1000); + const nowBN = ethers.BigNumber.from(now); + + if (nowBN.gte(endTime)) { + setTimeLeft(0); + return; + } + + const diff = endTime.sub(nowBN).toNumber(); // SAFE: seconds, small number + setTimeLeft(diff); + }; + + tick(); + const interval = setInterval(tick, 1000); + return () => clearInterval(interval); + }, [lastPing, deathTimeout]); + + if (timeLeft <= 0) return Expired; + + const hours = Math.floor(timeLeft / 3600); + const minutes = Math.floor((timeLeft % 3600) / 60); + const seconds = timeLeft % 60; + + return ( + + {hours}h {minutes}m {seconds}s + + ); +}; + +export default CountdownTimer; diff --git a/frontend/src/hooks/useGetWills.jsx b/frontend/src/hooks/useGetWills.jsx index da10bb3..3a036eb 100644 --- a/frontend/src/hooks/useGetWills.jsx +++ b/frontend/src/hooks/useGetWills.jsx @@ -1,23 +1,18 @@ -import { useEffect, useState, useCallback, useRef } from 'react' -import { useContract } from '../context/ContractContext' -import { toast } from 'react-toastify' -import { Contract, ethers } from 'ethers' -import { contractAddress } from '../utils/contractAddress' -import contractAbi from '../abi/CreateWill.json' +import { useEffect, useState, useCallback, useRef } from "react"; +import { useContract } from "../context/ContractContext"; +import { toast } from "react-toastify"; +import { Contract, ethers } from "ethers"; +import { contractAddress } from "../utils/contractAddress"; +import contractAbi from "../abi/CreateWill.json"; -const ABI = contractAbi.abi -const ZERO = ethers.BigNumber.from(0) +const ABI = contractAbi.abi; +const ZERO = ethers.BigNumber.from(0); -// πŸ”’ HARDENED NORMALIZER (FINAL FORM) const normalizeWill = (w) => { - const beneficiaries = Array.isArray(w?.[0]) ? w[0] : [] - const rawAmounts = w?.[1] + const beneficiaries = Array.isArray(w?.[0]) ? w[0] : []; + const rawAmounts = w?.[1]; - const amounts = Array.isArray(rawAmounts) - ? rawAmounts - : rawAmounts - ? [rawAmounts] - : [] + const amounts = Array.isArray(rawAmounts) ? rawAmounts : rawAmounts ? [rawAmounts] : []; return { beneficiaries, @@ -27,119 +22,116 @@ const normalizeWill = (w) => { cancelled: Boolean(w?.[4]), balance: ethers.BigNumber.isBigNumber(w?.[5]) ? w[5] : ZERO, deathTimeout: Number(w?.[6] ?? 0), - } -} + }; +}; const useGetWills = () => { - const [wills, setWills] = useState([]) - const [willInfo, setWillInfo] = useState(null) - const [hasWill, setHasWill] = useState(false) - const [totalBalance, setTotalBalance] = useState(0) - const [willsCreated, setWillsCreated] = useState(0) - const [loading, setLoading] = useState(false) + const [wills, setWills] = useState([]); + const [willInfo, setWillInfo] = useState(null); - const { contract, provider, walletAddress, isConnected } = useContract() - const isFetching = useRef(false) + // βœ… TWO FLAGS (DO NOT REUSE ONE BOOLEAN) + const [hasActiveWill, setHasActiveWill] = useState(false); + const [hasEverCreatedWill, setHasEverCreatedWill] = useState(false); + + const [totalBalance, setTotalBalance] = useState(0); + const [willsCreated, setWillsCreated] = useState(0); + const [loading, setLoading] = useState(false); + + const { contract, provider, walletAddress, isConnected } = useContract(); + const isFetching = useRef(false); const fetchAllWills = useCallback(async () => { - if (!contract || !provider || !walletAddress || isFetching.current) return - isFetching.current = true - setLoading(true) + if (!contract || !provider || !walletAddress || isFetching.current) return; + + isFetching.current = true; + setLoading(true); try { - /* ---------- EVENTS ---------- */ - const filterInstance = new Contract(contractAddress, ABI, provider) - const events = await filterInstance.queryFilter( - filterInstance.filters.WillCreated(), - 0, - 'latest' - ) + // ---------- EVENTS ---------- + const filterInstance = new Contract(contractAddress, ABI, provider); + const events = await filterInstance.queryFilter(filterInstance.filters.WillCreated(), 0, "latest"); - setWillsCreated(events.length) + setWillsCreated(events.length); + setHasEverCreatedWill(events.length > 0); setTotalBalance( - events.reduce( - (acc, e) => - acc + parseFloat(ethers.utils.formatEther(e.args.balance)), - 0 - ) - ) - - /* ---------- USER WILL ---------- */ - const userRaw = await contract.usersWill(walletAddress) - const userWill = normalizeWill(userRaw) - - setHasWill( + events.reduce((acc, e) => acc + parseFloat(ethers.utils.formatEther(e.args.balance)), 0) + ); + + // ---------- USER WILL (ACTIVE STATE) ---------- + const userRaw = await contract.usersWill(walletAddress); + const userWill = normalizeWill(userRaw); + + const active = userWill.balance.gt(0) && !userWill.executed && - !userWill.cancelled - ) - setWillInfo(userWill) + !userWill.cancelled; + + setHasActiveWill(active); + setWillInfo(userWill); - /* ---------- ALL WILLS ---------- */ - const testators = await contract.getAllTestators() - const uniqueTestators = [...new Set(testators)] - const now = Math.floor(Date.now() / 1000) + // ---------- ALL WILLS ---------- + const testators = await contract.getAllTestators(); + const uniqueTestators = [...new Set(testators)]; + const now = Math.floor(Date.now() / 1000); - const willList = [] + const willList = []; for (const addr of uniqueTestators) { try { - const raw = await contract.usersWill(addr) - const will = normalizeWill(raw) + const raw = await contract.usersWill(addr); + const will = normalizeWill(raw); - const timeLeft = - will.lastPing + will.deathTimeout - now + const timeLeft = will.lastPing + will.deathTimeout - now; willList.push({ address: addr, balanceEth: ethers.utils.formatEther(will.balance), beneficiaries: will.beneficiaries, - amounts: will.amounts.map((a) => - ethers.utils.formatEther(a) - ), + amounts: will.amounts.map((a) => ethers.utils.formatEther(a)), executed: will.executed, cancelled: will.cancelled, lastPing: will.lastPing, deathTimeout: will.deathTimeout, timeLeft, status: will.executed - ? 'EXECUTED' + ? "EXECUTED" : will.cancelled - ? 'CANCELLED' + ? "CANCELLED" : will.balance.gt(0) - ? 'ACTIVE' - : 'EMPTY', - }) + ? "ACTIVE" + : "EMPTY", + }); } catch (e) { - console.error('Skipping broken will:', addr, e) + console.error("Skipping broken will:", addr, e); } } - setWills(willList) - console.log('βœ… Wills loaded:', willList.length) + setWills(willList); + console.log("βœ… Wills loaded:", willList.length); } catch (err) { - console.error('❌ Fetch error:', err) - toast.error('Failed to load wills') + console.error("❌ Fetch error:", err); + toast.error("Failed to load wills"); } finally { - setLoading(false) - isFetching.current = false + setLoading(false); + isFetching.current = false; } - }, [contract, provider, walletAddress]) + }, [contract, provider, walletAddress]); useEffect(() => { - if (isConnected) fetchAllWills() - }, [isConnected, fetchAllWills]) + if (isConnected) fetchAllWills(); + }, [isConnected, fetchAllWills]); return { fetchAllWills, wills, willsCreated, totalBalance, - hasWill, + hasActiveWill, // βœ… + hasEverCreatedWill, // βœ… willInfo, loading, - } -} + }; +}; -export default useGetWills +export default useGetWills; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d81e1b8..c909975 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,20 +5,4 @@ import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], - define: { - global: 'globalThis', - }, - resolve: { - alias: { - buffer: 'buffer', - } - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis' - } - } - } }) - From c8a314fb6df0b3ac7eaf28fbbf8d04802f2f6f9a Mon Sep 17 00:00:00 2001 From: Rambod Goshtasbi Date: Mon, 12 Jan 2026 19:00:37 -0500 Subject: [PATCH 5/5] Fix build and preview, front end stable. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 044791f..04d6fe4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "test": "test" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" }, "keywords": [], "author": "",