From a19a0a02a54579b5c23ee2bc5f9f5f6731e4d25b Mon Sep 17 00:00:00 2001 From: scottrepreneur Date: Thu, 18 Jan 2024 20:46:59 -0600 Subject: [PATCH] drop in forms --- .eslintrc.json | 3 +- jest.config.ts | 51 +- package.json | 8 + packages/constants/package.json | 1 + packages/constants/tsconfig.json | 6 +- packages/dapp/.eslintrc.json | 15 +- packages/dapp/package.json | 3 +- packages/dapp/pages/contracts.tsx | 8 +- packages/dapp/pages/create/escrow.tsx | 77 +-- packages/dapp/pages/create/index.tsx | 6 +- packages/dapp/pages/create/instant.tsx | 73 +-- packages/dapp/pages/index.tsx | 2 +- .../invoice/[chainId]/[invoiceId]/index.tsx | 80 +-- .../invoice/[chainId]/[invoiceId]/instant.tsx | 66 +- .../invoice/[chainId]/[invoiceId]/locked.tsx | 15 +- packages/dapp/tsconfig.json | 18 +- packages/forms/package.json | 13 +- packages/forms/src/AddMilestones.tsx | 19 +- packages/forms/src/DepositFunds.tsx | 480 +++++++-------- packages/forms/src/EscrowDetailsForm.tsx | 213 +++++++ packages/forms/src/InvoicePaymentDetails.tsx | 330 ++++++++++ packages/forms/src/LockFunds.tsx | 273 ++++----- packages/forms/src/PaymentChunksForm.tsx | 17 +- packages/forms/src/PaymentDetailsForm.tsx | 101 ++-- packages/forms/src/PaymentsForm.tsx | 214 +++++++ packages/forms/src/ProjectDetailsForm.tsx | 240 ++++---- packages/forms/src/ReleaseFunds.tsx | 250 ++++---- packages/forms/src/ResolveFunds.tsx | 501 +++++++--------- packages/forms/src/WithdrawFunds.tsx | 132 ++-- packages/forms/src/index.ts | 12 +- .../src}/instant/DepositFunds.tsx | 72 +-- .../src}/instant/FormConfirmation.tsx | 65 +- .../src}/instant/PaymentDetailsForm.tsx | 45 +- .../src}/instant/ProjectDetailsForm.tsx | 67 +-- .../src}/instant/WithdrawFunds.tsx | 16 +- packages/forms/tsconfig.json | 6 +- packages/graphql/package.json | 9 +- packages/graphql/src/invoice.ts | 2 +- packages/graphql/src/invoices.ts | 2 +- packages/graphql/tsconfig.json | 19 + packages/hooks/package.json | 9 +- packages/hooks/src/index.ts | 10 + packages/hooks/src/useDeposit.ts | 89 +++ packages/hooks/src/useEscrowZap.ts | 243 ++++++++ packages/hooks/src/useFetchTokensViaIPFS.ts | 3 +- packages/hooks/src/useInvoiceDetails.ts | 22 + packages/hooks/src/useInvoiceStatus.tsx | 90 +-- packages/hooks/src/useLock.ts | 80 +++ packages/hooks/src/usePollSubgraph.ts | 45 ++ packages/hooks/src/useRegister.ts | 163 +++++ packages/hooks/src/useRelease.ts | 60 ++ packages/hooks/src/useResolve.ts | 75 +++ packages/hooks/src/useTokenBalance.ts | 5 + packages/hooks/src/useWithdraw.ts | 45 ++ packages/hooks/tsconfig.json | 6 +- packages/types/package.json | 6 +- packages/types/src/index.ts | 1 + packages/types/src/invoice.ts | 28 + packages/types/tsconfig.json | 6 +- packages/ui/package.json | 18 +- packages/ui/src/components/ErrorBoundary.tsx | 2 +- .../ui/src/components/FormConfirmation.tsx | 66 +- .../ui/src/components/GenerateInvoicePDF.tsx | 2 +- .../src/components/InvoiceDashboardTable.tsx | 11 +- packages/ui/src/components/InvoicePDF.tsx | 28 +- .../ui/src/components/InvoiceStatusLabel.tsx | 5 +- .../components/NetworkChangeAlertModal.tsx | 6 +- .../ui/src/components/RegisterSuccess.tsx | 19 +- packages/ui/src/components/VerifyInvoice.tsx | 11 +- .../ui/src/components/WhatIsThisModal.tsx | 7 +- packages/ui/src/shared/AccountLink.tsx | 9 +- packages/ui/src/shared/ConnectWeb3.tsx | 7 +- packages/ui/src/shared/Container.tsx | 5 +- packages/ui/src/shared/InvoiceNotFound.tsx | 2 +- packages/ui/src/shared/Layout.tsx | 2 +- packages/ui/src/shared/OrderedInput.tsx | 2 +- packages/ui/src/shared/StepInfo.tsx | 2 +- packages/ui/src/shared/index.ts | 18 +- packages/ui/tsconfig.json | 6 +- packages/utils/package.json | 3 +- ...mpatibilityHelpers.ts => compatibility.ts} | 0 packages/utils/src/helpers.ts | 7 +- packages/utils/src/index.ts | 3 +- packages/utils/src/invoice.ts | 4 + .../utils/src/tokenSupport/uploadTokenInfo.ts | 2 +- packages/utils/tsconfig.json | 7 +- pnpm-lock.yaml | 567 ++++++------------ tsconfig.base.json | 105 +--- 88 files changed, 3272 insertions(+), 2170 deletions(-) create mode 100644 packages/forms/src/EscrowDetailsForm.tsx create mode 100644 packages/forms/src/InvoicePaymentDetails.tsx create mode 100644 packages/forms/src/PaymentsForm.tsx rename packages/{ui/src/components => forms/src}/instant/DepositFunds.tsx (92%) rename packages/{ui/src/components => forms/src}/instant/FormConfirmation.tsx (72%) rename packages/{ui/src/components => forms/src}/instant/PaymentDetailsForm.tsx (86%) rename packages/{ui/src/components => forms/src}/instant/ProjectDetailsForm.tsx (73%) rename packages/{ui/src/components => forms/src}/instant/WithdrawFunds.tsx (90%) create mode 100644 packages/graphql/tsconfig.json create mode 100644 packages/hooks/src/useDeposit.ts create mode 100644 packages/hooks/src/useEscrowZap.ts create mode 100644 packages/hooks/src/useInvoiceDetails.ts create mode 100644 packages/hooks/src/useLock.ts create mode 100644 packages/hooks/src/usePollSubgraph.ts create mode 100644 packages/hooks/src/useRegister.ts create mode 100644 packages/hooks/src/useRelease.ts create mode 100644 packages/hooks/src/useResolve.ts create mode 100644 packages/hooks/src/useTokenBalance.ts create mode 100644 packages/hooks/src/useWithdraw.ts create mode 100644 packages/types/src/invoice.ts rename packages/utils/src/{compatibilityHelpers.ts => compatibility.ts} (100%) diff --git a/.eslintrc.json b/.eslintrc.json index 6d968ab0..eef1009f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,9 +24,10 @@ // Allow props spreading "react/jsx-props-no-spreading": "off", - // disable prop-types "react/prop-types": "off", + // typescript + "@typescript-eslint/no-explicit-any": "off", // disable form label "jsx-a11y/label-has-associated-control": "off" diff --git a/jest.config.ts b/jest.config.ts index 4ae27160..d8132e39 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,32 +1,33 @@ -import type {Config} from 'jest'; -import nextJest from 'next/jest.js' - +import type { Config } from 'jest'; +import nextJest from 'next/jest.js'; + const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment dir: './packages/dapp', -}) +}); const config: Config = { - coverageProvider: 'v8', - projects: [ - { - preset: "ts-jest", - rootDir: "./packages/dapp", - setupFilesAfterEnv: ["/test/setupTests.ts"], - testEnvironment: 'jsdom', - testMatch: ["/test/**/*.test.(ts|tsx|js|jsx)"], - transform: { - "^.+\\.(js|jsx|ts|tsx)$": [ - "ts-jest", { - tsconfig: "/tsconfig.test.json" - } - ], - }, - transformIgnorePatterns: ["node_modules/(?!(viem|isows)/)"], - } - ], - verbose: true, -} + coverageProvider: 'v8', + projects: [ + { + preset: 'ts-jest', + rootDir: './packages/dapp', + setupFilesAfterEnv: ['/test/setupTests.ts'], + testEnvironment: 'jsdom', + testMatch: ['/test/**/*.test.(ts|tsx|js|jsx)'], + transform: { + '^.+\\.(js|jsx|ts|tsx)$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.test.json', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!(viem|isows)/)'], + }, + ], + verbose: true, +}; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -export default createJestConfig(config); \ No newline at end of file +export default createJestConfig(config); diff --git a/package.json b/package.json index 419ef6be..734eb246 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "license": "MIT", "devDependencies": { "@babel/eslint-parser": "^7.23.3", + "@testing-library/jest-dom": "^6.2.0", + "@testing-library/react": "^14.1.2", "@typescript-eslint/parser": "^6.17.0", "async-prompt": "^1.0.1", "dotenv": "^16.3.1", @@ -62,6 +64,8 @@ "ethers": "^5.1.0", "husky": "^6.0.0", "it-all": "^1.0.2", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "lint-staged": "^14.0.1", "prettier": "^3.1.1", "prettier-plugin-solidity": "^1.3.1", @@ -75,5 +79,9 @@ "*.{json,md,sol}": [ "prettier --write" ] + }, + "dependencies": { + "lodash": "^4.17.21", + "next": "^14.0.4" } } diff --git a/packages/constants/package.json b/packages/constants/package.json index 520dda5a..da734514 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/packages/constants/tsconfig.json b/packages/constants/tsconfig.json index 0a56d933..e2573e48 100644 --- a/packages/constants/tsconfig.json +++ b/packages/constants/tsconfig.json @@ -13,11 +13,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./*"] - } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../../jest.config.ts"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], } diff --git a/packages/dapp/.eslintrc.json b/packages/dapp/.eslintrc.json index b1d53497..01845a63 100644 --- a/packages/dapp/.eslintrc.json +++ b/packages/dapp/.eslintrc.json @@ -13,6 +13,7 @@ "sourceType": "module" }, "rules": { + "no-unused-vars": "off", "react/function-component-definition": "off", "react/no-unescaped-entities": "off", "react/prop-types": "off", @@ -20,7 +21,10 @@ "import/no-default-export": "off", "import/prefer-default-export": "off", "@next/next/no-page-custom-font": "off", - "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], + "react/jsx-filename-extension": [ + 2, + { "extensions": [".js", ".jsx", ".ts", ".tsx"] } + ], "import/extensions": [ "error", "ignorePackages", @@ -29,7 +33,7 @@ "jsx": "never", "ts": "never", "tsx": "never" - } + } ], // exclude test files "import/no-extraneous-dependencies": [ @@ -45,10 +49,7 @@ "optionalDependencies": false } ], - "mocha/no-mocha-arrows": "off", + "mocha/no-mocha-arrows": "off" }, - "extends": [ - "next", - "prettier" - ] + "extends": ["next", "prettier"] } diff --git a/packages/dapp/package.json b/packages/dapp/package.json index b8d2e51e..83652c7b 100644 --- a/packages/dapp/package.json +++ b/packages/dapp/package.json @@ -14,13 +14,14 @@ "@rainbow-me/rainbowkit": "^1.3.3", "@react-pdf/renderer": "^3.1.14", "@smart-invoice/constants": "*", + "@smart-invoice/forms": "*", + "@smart-invoice/graphql": "*", "@smart-invoice/hooks": "*", "@smart-invoice/ui": "*", "@smart-invoice/utils": "*", "@tanstack/react-query": "^5.17.9", "@tanstack/react-table": "^8.11.3", "@vercel/analytics": "^1.1.1", - "@wagmi/core": "^1.4.13", "abitype": "^0.10.3", "base-58": "^0.0.1", "dotenv": "^16.0.1", diff --git a/packages/dapp/pages/contracts.tsx b/packages/dapp/pages/contracts.tsx index b25c5b31..e73a1d24 100644 --- a/packages/dapp/pages/contracts.tsx +++ b/packages/dapp/pages/contracts.tsx @@ -8,9 +8,9 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { CONFIG } from '../constants'; -import { useFetchTokensViaIPFS } from '../hooks/useFetchTokensViaIPFS'; -import { Container } from '../shared/Container'; +import { CONFIG } from '@smart-invoice/constants'; +import { useFetchTokensViaIPFS } from '@smart-invoice/hooks'; +import { Container } from '@smart-invoice/ui'; import { getKeys, getAccountString, @@ -18,7 +18,7 @@ import { getInvoiceFactoryAddress, getTokenInfo, getTokens, -} from '../utils'; +} from '@smart-invoice/utils'; const { NETWORK_CONFIG } = CONFIG; const chainIds = getKeys(NETWORK_CONFIG); diff --git a/packages/dapp/pages/create/escrow.tsx b/packages/dapp/pages/create/escrow.tsx index d6e0565c..eb9c2521 100644 --- a/packages/dapp/pages/create/escrow.tsx +++ b/packages/dapp/pages/create/escrow.tsx @@ -1,7 +1,6 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { useWalletClient } from 'wagmi'; - /* eslint-disable no-nested-ternary */ +import React, { useEffect, useRef, useState } from 'react'; +import { useWalletClient } from 'wagmi'; import { Button, Flex, @@ -13,34 +12,34 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { FormConfirmation } from '../../components/FormConfirmation'; -import { NetworkChangeAlertModal } from '../../components/NetworkChangeAlertModal'; -import { PaymentChunksForm } from '../../components/PaymentChunksForm'; -import { PaymentDetailsForm } from '../../components/PaymentDetailsForm'; -import { ProjectDetailsForm } from '../../components/ProjectDetailsForm'; -import { RegisterSuccess } from '../../components/RegisterSuccess'; -import { ESCROW_STEPS, INVOICE_TYPES } from '../../constants'; import { - CreateContext, - CreateContextProvider, -} from '../../context/CreateContext'; -import { useFetchTokensViaIPFS } from '../../hooks/useFetchTokensViaIPFS'; -import { Container } from '../../shared/Container'; -import { StepInfo } from '../../shared/StepInfo'; + FormConfirmation, + NetworkChangeAlertModal, + RegisterSuccess, + Container, + StepInfo, +} from '@smart-invoice/ui'; +// import { +// PaymentChunksForm, +// PaymentDetailsForm, +// ProjectDetailsForm, +// } from '@smart-invoice/forms'; +import { ESCROW_STEPS, INVOICE_TYPES } from '@smart-invoice/constants'; +import { useFetchTokensViaIPFS } from '@smart-invoice/hooks'; type EscrowStepNumber = keyof typeof ESCROW_STEPS; -export function CreateInvoiceEscrowInner() { - const { - txHash, - loading, - currentStep, - nextStepEnabled, - goBackHandler, - nextStepHandler, - invoiceType, - setInvoiceType, - } = useContext(CreateContext); +export function CreateInvoiceEscrow() { + // const { + // txHash, + // loading, + // currentStep, + // nextStepEnabled, + // goBackHandler, + // nextStepHandler, + // invoiceType, + // setInvoiceType, + // } = useContext(CreateContext); const { data: walletClient } = useWalletClient(); const chainId = walletClient?.chain?.id; const [{ tokenData, allTokens }] = useFetchTokensViaIPFS(); @@ -49,9 +48,9 @@ export function CreateInvoiceEscrowInner() { const [showChainChangeAlert, setShowChainChangeAlert] = useState(false); const { Escrow } = INVOICE_TYPES; - useEffect(() => { - setInvoiceType(Escrow); - }, [invoiceType, setInvoiceType, Escrow]); + // useEffect(() => { + // setInvoiceType(Escrow); + // }, [invoiceType, setInvoiceType, Escrow]); useEffect(() => { if (chainId === undefined) return; @@ -82,7 +81,7 @@ export function CreateInvoiceEscrowInner() { return ( - {txHash ? ( + {true ? ( // txHash ? ( ) : tokenData ? ( - - + - + */} @@ -191,12 +188,4 @@ export function CreateInvoiceEscrowInner() { ); } -function CreateInvoiceEscrow() { - return ( - - - - ); -} - export default CreateInvoiceEscrow; diff --git a/packages/dapp/pages/create/index.tsx b/packages/dapp/pages/create/index.tsx index 61ee0bd7..5932fc12 100644 --- a/packages/dapp/pages/create/index.tsx +++ b/packages/dapp/pages/create/index.tsx @@ -11,8 +11,8 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { INVOICE_TYPES } from '../../constants'; -import { logError } from '../../utils/helpers'; +import { INVOICE_TYPES } from '@smart-invoice/constants'; +import { logError } from '@smart-invoice/utils'; function SelectInvoiceType() { const { Instant, Escrow } = INVOICE_TYPES; @@ -33,7 +33,7 @@ function SelectInvoiceType() { } }; - const createType = async invoiceType => { + const createType = async (invoiceType: any) => { try { router.push(`/create/${invoiceType}`); } catch (error) { diff --git a/packages/dapp/pages/create/instant.tsx b/packages/dapp/pages/create/instant.tsx index 9d1c978f..852b73af 100644 --- a/packages/dapp/pages/create/instant.tsx +++ b/packages/dapp/pages/create/instant.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; -import { useWalletClient } from 'wagmi'; +import { useChainId, useWalletClient } from 'wagmi'; /* eslint-disable no-nested-ternary */ import { @@ -13,40 +13,39 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { NetworkChangeAlertModal } from '../../components/NetworkChangeAlertModal'; -import { RegisterSuccess } from '../../components/RegisterSuccess'; -import { FormConfirmation } from '../../components/instant/FormConfirmation'; -import { InstantPaymentDetailsForm } from '../../components/instant/PaymentDetailsForm'; -import { ProjectDetailsForm } from '../../components/instant/ProjectDetailsForm'; -import { INSTANT_STEPS, INVOICE_TYPES } from '../../constants'; import { - CreateContext, - CreateContextProvider, -} from '../../context/CreateContext'; -import { useFetchTokensViaIPFS } from '../../hooks/useFetchTokensViaIPFS'; -import { Container } from '../../shared/Container'; -import { StepInfo } from '../../shared/StepInfo'; + NetworkChangeAlertModal, + RegisterSuccess, + FormConfirmation, + Container, + StepInfo, +} from '@smart-invoice/ui'; +// import { +// InstantPaymentDetailsForm, +// ProjectDetailsForm, +// } from '@smart-invoice/forms'; +import { INSTANT_STEPS, INVOICE_TYPES } from '@smart-invoice/constants'; +import { useFetchTokensViaIPFS } from '@smart-invoice/hooks'; type InstantStepNumber = keyof typeof INSTANT_STEPS; -export function CreateInvoiceInstantInner() { - const { - txHash, - loading, - currentStep, - nextStepEnabled, - goBackHandler, - nextStepHandler, - invoiceType, - setInvoiceType, - } = useContext(CreateContext); +export function CreateInvoiceInstant() { + // const { + // txHash, + // loading, + // currentStep, + // nextStepEnabled, + // goBackHandler, + // nextStepHandler, + // invoiceType, + // setInvoiceType, + // } = useContext(CreateContext); const { Instant } = INVOICE_TYPES; - useEffect(() => { - setInvoiceType(Instant); - }, [invoiceType, setInvoiceType, Instant]); - const { data: walletClient } = useWalletClient(); - const chainId = walletClient?.chain?.id; + // useEffect(() => { + // setInvoiceType(Instant); + // }, [invoiceType, setInvoiceType, Instant]); + const chainId = useChainId(); const [{ tokenData, allTokens }] = useFetchTokensViaIPFS(); const prevChainIdRef = useRef(); const [showChainChangeAlert, setShowChainChangeAlert] = useState(false); @@ -76,7 +75,7 @@ export function CreateInvoiceInstantInner() { return ( - {txHash ? ( + {true ? ( // txHash ? ( ) : tokenData ? ( - - + */} @@ -183,12 +182,4 @@ export function CreateInvoiceInstantInner() { ); } -function CreateInvoiceInstant() { - return ( - - - - ); -} - export default CreateInvoiceInstant; diff --git a/packages/dapp/pages/index.tsx b/packages/dapp/pages/index.tsx index 8c485609..36385571 100644 --- a/packages/dapp/pages/index.tsx +++ b/packages/dapp/pages/index.tsx @@ -9,7 +9,7 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { logError } from '../utils/helpers'; +import { logError } from '@smart-invoice/utils'; import { useAccount } from 'wagmi'; function Home() { diff --git a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/index.tsx b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/index.tsx index 5f558f3f..b865238a 100644 --- a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/index.tsx +++ b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/index.tsx @@ -25,24 +25,28 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { AddMilestones } from '../../../../components/AddMilestones'; -import { DepositFunds } from '../../../../components/DepositFunds'; -import { GenerateInvoicePDF } from '../../../../components/GenerateInvoicePDF'; -import { Loader } from '../../../../components/Loader'; -import { LockFunds } from '../../../../components/LockFunds'; -import { ReleaseFunds } from '../../../../components/ReleaseFunds'; -import { ResolveFunds } from '../../../../components/ResolveFunds'; -import { VerifyInvoice } from '../../../../components/VerifyInvoice'; -import { WithdrawFunds } from '../../../../components/WithdrawFunds'; -import { Invoice, fetchInvoice } from '../../../../graphql/fetchInvoice'; -import { useFetchTokensViaIPFS } from '../../../../hooks/useFetchTokensViaIPFS'; -import { CopyIcon } from '../../../../icons/CopyIcon'; -import { QuestionIcon } from '../../../../icons/QuestionIcon'; -import { AccountLink } from '../../../../shared/AccountLink'; -import { Container } from '../../../../shared/Container'; -import { InvoiceNotFound } from '../../../../shared/InvoiceNotFound'; -import { balanceOf } from '../../../../utils/erc20'; import { + AddMilestones, + DepositFunds, + LockFunds, + ReleaseFunds, + ResolveFunds, + WithdrawFunds, +} from '@smart-invoice/forms'; +import { + GenerateInvoicePDF, + Loader, + VerifyInvoice, + CopyIcon, + QuestionIcon, + AccountLink, + Container, + InvoiceNotFound, +} from '@smart-invoice/ui'; +import { Invoice, fetchInvoice } from '@smart-invoice/graphql'; +import { useFetchTokensViaIPFS } from '@smart-invoice/hooks'; +import { + balanceOf, copyToClipboard, getAccountString, getAddressLink, @@ -53,7 +57,7 @@ import { getTxLink, isAddress, logError, -} from '../../../../utils/helpers'; +} from '@smart-invoice/utils'; function ViewInvoice() { const { data: walletClient } = useWalletClient(); @@ -85,15 +89,15 @@ function ViewInvoice() { walletClient?.chain?.id === invoiceChainId ) { setBalanceLoading(true); - balanceOf(walletClient.chain, validToken, validAddress) - .then(b => { - setBalance(b); - setBalanceLoading(false); - }) - .catch(balanceError => { - logError({ balanceError }); - setBalanceLoading(false); - }); + // balanceOf(walletClient.chain, validToken, validAddress) + // .then(b => { + // setBalance(b); + // setBalanceLoading(false); + // }) + // .catch(balanceError => { + // logError({ balanceError }); + // setBalanceLoading(false); + // }); } }, [invoice, walletClient?.chain, invoiceChainId, validToken, validAddress]); @@ -105,15 +109,15 @@ function ViewInvoice() { walletClient?.chain?.id === invoiceChainId ) { setBalanceLoading(true); - balanceOf(walletClient.chain, validToken, validAddress) - .then(b => { - setBalance(b); - setBalanceLoading(false); - }) - .catch(balanceError => { - logError({ balanceError }); - setBalanceLoading(false); - }); + // balanceOf(walletClient.chain, validToken, validAddress) + // .then(b => { + // setBalance(b); + // setBalanceLoading(false); + // }) + // .catch(balanceError => { + // logError({ balanceError }); + // setBalanceLoading(false); + // }); } }, [invoice, walletClient?.chain, invoiceChainId, validToken, validAddress]); @@ -947,7 +951,7 @@ function ViewInvoice() { right="0.5rem" color="gray" /> - {modal && selected === 0 && ( + {/* {modal && selected === 0 && ( setModal(false)} /> - )} + )} */} diff --git a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/instant.tsx b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/instant.tsx index 2ea88d51..af70036f 100644 --- a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/instant.tsx +++ b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/instant.tsx @@ -22,20 +22,21 @@ import { useBreakpointValue, } from '@chakra-ui/react'; -import { GenerateInvoicePDF } from '../../../../components/GenerateInvoicePDF'; -import { Loader } from '../../../../components/Loader'; -import { DepositFunds } from '../../../../components/instant/DepositFunds'; -import { WithdrawFunds } from '../../../../components/instant/WithdrawFunds'; -import { ChainId } from '../../../../constants/config'; -import { Invoice, fetchInvoice } from '../../../../graphql/fetchInvoice'; -import { useFetchTokensViaIPFS } from '../../../../hooks/useFetchTokensViaIPFS'; -import { CopyIcon } from '../../../../icons/CopyIcon'; -import { QuestionIcon } from '../../../../icons/QuestionIcon'; -import { AccountLink } from '../../../../shared/AccountLink'; -import { Container } from '../../../../shared/Container'; -import { InvoiceNotFound } from '../../../../shared/InvoiceNotFound'; -import { balanceOf } from '../../../../utils/erc20'; import { + GenerateInvoicePDF, + Loader, + CopyIcon, + QuestionIcon, + AccountLink, + Container, + InvoiceNotFound, +} from '@smart-invoice/ui'; +import { DepositFunds, WithdrawFunds } from '@smart-invoice/forms'; +import { ChainId } from '@smart-invoice/constants'; +import { Invoice, fetchInvoice } from '@smart-invoice/graphql'; +import { useFetchTokensViaIPFS } from '@smart-invoice/hooks'; +import { + balanceOf, copyToClipboard, getAccountString, getAddressLink, @@ -44,17 +45,18 @@ import { getTokenInfo, isAddress, logError, -} from '../../../../utils/helpers'; -import { getDeadline, getLateFee, getTotalDue, getTotalFulfilled, -} from '../../../../utils/invoice'; +} from '@smart-invoice/utils'; import { useParams } from 'next/navigation'; function ViewInstantInvoice() { - const {hexChainId, invoiceId} = useParams<{ hexChainId: string; invoiceId: Address; }>(); + const { hexChainId, invoiceId } = useParams<{ + hexChainId: string; + invoiceId: Address; + }>(); const invoiceChainId = parseInt(hexChainId, 16) as ChainId; const { data: walletClient } = useWalletClient(); const account = walletClient?.account?.address; @@ -101,8 +103,8 @@ function ViewInstantInvoice() { // Get Balance try { setBalanceLoading(true); - const b = await balanceOf(chain, validToken, validAddress); - setBalance(b); + // const b = await balanceOf(chain, validToken, validAddress); + // setBalance(b); setBalanceLoading(false); } catch (balanceError) { logError({ balanceError }); @@ -110,8 +112,8 @@ function ViewInstantInvoice() { // Get Total Due try { - const t = await getTotalDue(chain, validAddress); - setTotalDue(t); + // const t = await getTotalDue(chain, validAddress); + // setTotalDue(t); } catch (totalDueError) { logError({ totalDueError }); setTotalDue(total); @@ -119,20 +121,20 @@ function ViewInstantInvoice() { // Get Deadline, Late Fee and its time interval try { - const d = await getDeadline(chain, validAddress); - setDeadline(Number(d)); - const { amount, timeInterval } = await getLateFee(chain, validAddress); - setLateFeeAmount(amount); - setLateFeeTimeInterval(Number(timeInterval)); + // const d = await getDeadline(chain, validAddress); + setDeadline(0); // Number(d)); + // const { amount, timeInterval } = await getLateFee(chain, validAddress); + // setLateFeeAmount(amount); + // setLateFeeTimeInterval(Number(timeInterval)); } catch (lateFeeError) { logError({ lateFeeError }); } // Get Total Fulfilled try { - const tf = await getTotalFulfilled(chain, validAddress); - setTotalFulfilled(tf.amount); - setFulfilled(tf.isFulfilled); + // const tf = await getTotalFulfilled(chain, validAddress); + // setTotalFulfilled(tf.amount); + // setFulfilled(tf.isFulfilled); } catch (totalFulfilledError) { logError({ totalFulfilledError }); } @@ -342,7 +344,7 @@ function ViewInstantInvoice() { invoice={invoice} symbol={symbol} buttonText="Preview & Download Invoice PDF" - buttonProps={{textColor:"blue.dark"}} + buttonProps={{ textColor: 'blue.dark' }} /> @@ -523,7 +525,7 @@ function ViewInstantInvoice() { right="0.5rem" color="gray" /> - {modal && selected === 1 && ( + {/* {modal && selected === 1 && ( setModal(false)} /> - )} + )} */} diff --git a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/locked.tsx b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/locked.tsx index 5bbeb1fa..8db37fb2 100644 --- a/packages/dapp/pages/invoice/[chainId]/[invoiceId]/locked.tsx +++ b/packages/dapp/pages/invoice/[chainId]/[invoiceId]/locked.tsx @@ -5,16 +5,17 @@ import { Address, isAddress } from 'viem'; import { Button, Heading, Link, Text, VStack } from '@chakra-ui/react'; -import { Loader } from '../../../../components/Loader'; -import { ChainId } from '../../../../constants/config'; -import { Invoice, fetchInvoice } from '../../../../graphql/fetchInvoice'; -import { Container } from '../../../../shared/Container'; -import { InvoiceNotFound } from '../../../../shared/InvoiceNotFound'; -import { getIpfsLink, getTxLink } from '../../../../utils/helpers'; +import { Loader, Container, InvoiceNotFound } from '@smart-invoice/ui'; +import { ChainId } from '@smart-invoice/constants'; +import { Invoice, fetchInvoice } from '@smart-invoice/graphql'; +import { getIpfsLink, getTxLink } from '@smart-invoice/utils'; import { useParams } from 'next/navigation'; function LockedInvoice() { - const {hexChainId, invoiceId} = useParams<{ hexChainId: string; invoiceId: Address; }>(); + const { hexChainId, invoiceId } = useParams<{ + hexChainId: string; + invoiceId: Address; + }>(); const [invoice, setInvoice] = useState(); const router = useRouter(); const invoiceChainId = parseInt(hexChainId, 16) as ChainId; diff --git a/packages/dapp/tsconfig.json b/packages/dapp/tsconfig.json index 4a666666..36b29c78 100644 --- a/packages/dapp/tsconfig.json +++ b/packages/dapp/tsconfig.json @@ -16,13 +16,17 @@ "incremental": true, "plugins": [ { - "name": "next" - } + "name": "next", + }, ], - "paths": { - "@/*": ["./src/*"] - }, }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.js", "../../jest.config.ts"], - "exclude": ["node_modules", "dist"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "next.config.js", + "../../jest.config.ts", + ], + "exclude": ["node_modules", "dist"], } diff --git a/packages/forms/package.json b/packages/forms/package.json index 8fcf5015..c3edd859 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -12,18 +12,23 @@ "author": "", "license": "ISC", "dependencies": { + "@chakra-ui/icon": "^3.2.0", + "@chakra-ui/react": "^2.8.2", + "@hookform/resolvers": "^3.3.4", "@smart-invoice/constants": "*", "@smart-invoice/graphql": "*", "@smart-invoice/types": "*", "@smart-invoice/ui": "*", "@smart-invoice/utils": "*", - "@chakra-ui/icon": "^3.2.0", - "@chakra-ui/react": "^2.8.2", + "lodash": "^4.17.21", "react": "^18.2.0", - "viem": "^2.3.1", - "wagmi": "^1.4.13" + "react-hook-form": "^7.49.3", + "viem": "^1.21.4", + "wagmi": "^1.4.13", + "yup": "^1.3.3" }, "devDependencies": { + "@types/lodash": "^4.14.202", "@types/react": "^18.2.47" } } diff --git a/packages/forms/src/AddMilestones.tsx b/packages/forms/src/AddMilestones.tsx index 109c7b3c..8997125a 100644 --- a/packages/forms/src/AddMilestones.tsx +++ b/packages/forms/src/AddMilestones.tsx @@ -1,14 +1,13 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable guard-for-in */ -import React, { useEffect, useMemo, useState } from 'react'; -import { Hash, formatUnits, parseUnits } from 'viem'; -import { useWalletClient } from 'wagmi'; - /* eslint-disable react/no-array-index-key */ /* eslint-disable no-restricted-globals */ /* eslint-disable no-shadow */ /* eslint-disable no-plusplus */ /* eslint-disable radix */ +import React, { useEffect, useMemo, useState } from 'react'; +import { Hash, formatUnits, parseUnits } from 'viem'; +import { useWalletClient } from 'wagmi'; import { Button, Flex, @@ -23,7 +22,6 @@ import { VStack, useBreakpointValue, } from '@chakra-ui/react'; - import { ChainId } from '@smart-invoice/constants'; import { OrderedInput } from '@smart-invoice/ui'; import { TokenData } from '@smart-invoice/types'; @@ -200,6 +198,7 @@ export function AddMilestones({ invoice, due, tokenData }: AddMilestonesProps) { {revisedProjectAgreementType === 'ipfs' ? (
+ ) : ( // - ) : ( '' )} @@ -224,7 +222,7 @@ export function AddMilestones({ invoice, due, tokenData }: AddMilestonesProps) { color="black" value={addedTotalInput} isInvalid={addedTotalInvalid} - setValue={(v) => { + setValue={v => { if (v && !isNaN(Number(v))) { setAddedTotalInput(Number(v)); const p = parseUnits(v, decimals); @@ -245,7 +243,7 @@ export function AddMilestones({ invoice, due, tokenData }: AddMilestonesProps) { type="number" value={addedMilestones} isInvalid={addedMilestonesInvalid} - setValue={(v) => { + setValue={v => { const numMilestones = v ? Number(v) : 1; setAddedMilestones(numMilestones); setMilestoneAmounts( @@ -288,7 +286,7 @@ export function AddMilestones({ invoice, due, tokenData }: AddMilestonesProps) { borderColor="lightgrey" _hover={{ borderColor: 'lightgrey' }} pr="3.5rem" - onChange={(e) => { + onChange={e => { if (!e.target.value || isNaN(Number(e.target.value))) return; const amount = parseUnits(e.target.value, decimals); const newAmounts = milestoneAmounts.slice(); @@ -366,8 +364,7 @@ export function AddMilestones({ invoice, due, tokenData }: AddMilestonesProps) { color="white" backgroundColor="blue.1" isDisabled={ - milestoneAmountsInput.reduce((t, v) => t + v, 0) !== - addedTotalInput + milestoneAmountsInput.reduce((t, v) => t + v, 0) !== addedTotalInput } textTransform="uppercase" size={buttonSize} diff --git a/packages/forms/src/DepositFunds.tsx b/packages/forms/src/DepositFunds.tsx index 30b8e1fe..d922b7de 100644 --- a/packages/forms/src/DepositFunds.tsx +++ b/packages/forms/src/DepositFunds.tsx @@ -1,7 +1,3 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Hash, formatUnits, parseUnits } from 'viem'; -import { useWalletClient } from 'wagmi'; - /* eslint-disable react/no-array-index-key */ import { Alert, @@ -9,333 +5,297 @@ import { AlertTitle, Button, Checkbox, + // ControlledSelect, Flex, Heading, - Input, - InputGroup, - InputRightElement, + HStack, Link, - Select, + NumberInput, + Stack, Text, Tooltip, VStack, - useBreakpointValue, } from '@chakra-ui/react'; - -import { ChainId } from '@smart-invoice/constants'; -import { QuestionIcon } from '@smart-invoice/ui'; -import { TokenData } from '@smart-invoice/types'; -import { balanceOf, transfer } from '@smart-invoice/utils'; import { - calculateResolutionFeePercentage, - getNativeTokenSymbol, - getTokenInfo, + commify, getTxLink, + getNativeTokenSymbol, getWrappedNativeToken, - isAddress, - logError, - // waitForTransaction } from '@smart-invoice/utils'; -import { Invoice } from '@smart-invoice/graphql'; - -const getCheckedStatus = (deposited: bigint, validAmounts: bigint[]) => { - let sum = BigInt(0); - return validAmounts.map(a => { - sum += a; - return deposited > sum; - }); -}; - -const checkedAtIndex = (index: number, checked: boolean[]) => - checked.map((_c, i) => i <= index); +import { useDeposit } from '@smart-invoice/hooks'; +import { + // checkedAtIndex, + // depositedMilestones, + Invoice, + // parseTokenAddress, + // PAYMENT_TYPES, +} from '@smart-invoice/types'; +import _ from 'lodash'; +import { useEffect, useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { formatUnits, Hex, parseUnits } from 'viem'; +import { useAccount, useBalance, useChainId } from 'wagmi'; -export type DepositFundsProps = { - invoice: Invoice; - deposited: bigint; - due: bigint; - // total: bigint; - tokenData: Record>; - // fulfilled: boolean; - // close?: React.EventHandler; // () => void; -}; +import { QuestionIcon } from '@smart-invoice/ui'; -export const DepositFunds: React.FC = ({ +const DepositFunds = ({ invoice, deposited, due, - tokenData, +}: { + invoice: Invoice; + deposited: bigint; + due: bigint; }) => { - const { data: walletClient } = useWalletClient(); - const chainId = walletClient?.chain?.id; - const NATIVE_TOKEN_SYMBOL = getNativeTokenSymbol(chainId); - const WRAPPED_NATIVE_TOKEN = getWrappedNativeToken(chainId); - const { address, token, amounts, currentMilestone, resolutionRate } = - invoice ?? {}; - const validAmounts = useMemo(() => amounts?.map(BigInt) ?? [], [amounts]); - const validAddress = useMemo(() => isAddress(address), [address]); - const validToken = useMemo(() => isAddress(token), [token]); - const [paymentType, setPaymentType] = useState(0); - const [amount, setAmount] = useState(BigInt(0)); - const [amountInput, setAmountInput] = useState(''); - const { decimals, symbol } = getTokenInfo(chainId, token, tokenData); - const [loading, setLoading] = useState(false); - const [txHash, setTxHash] = useState(); - const buttonSize = useBreakpointValue({ base: 'md', md: 'lg' }); - const [depositError, setDepositError] = useState(false); - const isWRAPPED = token?.toLowerCase() === WRAPPED_NATIVE_TOKEN; - const initialStatus = getCheckedStatus(deposited, validAmounts); - const [checked, setChecked] = useState(initialStatus); + const { token, amounts, currentMilestone } = invoice; + const chainId = useChainId(); + const { address } = useAccount(); + + const TOKEN_DATA = useMemo( + () => ({ + nativeSymbol: getNativeTokenSymbol(chainId), + wrappedToken: getWrappedNativeToken(chainId), + isWrapped: _.eq(_.toLower(token), getWrappedNativeToken(chainId)), + }), + [chainId, token], + ); + + const [transaction, setTransaction] = useState(); + + const localForm = useForm(); + const { watch, setValue } = localForm; + + const paymentType = watch('paymentType'); + const amount = watch('amount', '0'); + const checked = watch('checked'); - const [balance, setBalance] = useState(); + const amountsSum = _.sumBy(amounts); // number, not parsed + // const paidMilestones = depositedMilestones(BigInt(deposited), amounts); - const deposit = async () => { - if (!amount || !balance || !validAddress || !validToken || !walletClient) return; - if (formatUnits(amount, decimals) > formatUnits(balance, decimals)) { - setDepositError(true); - return; - } + // const { data: nativeBalance } = useBalance({ address }); + // const { data: tokenBalance } = useBalance({ address, token }); + // const balance = + // paymentType?.value === PAYMENT_TYPES.NATIVE + // ? nativeBalance?.value + // : tokenBalance?.value; + // const displayBalance = + // paymentType?.value === PAYMENT_TYPES.NATIVE + // ? nativeBalance?.formatted + // : tokenBalance?.formatted; + // const decimals = + // paymentType?.value === PAYMENT_TYPES.NATIVE ? 18 : tokenBalance?.decimals; + const hasAmount = true; + // balance > BigInt(amount) * BigInt(10) ** BigInt(decimals || 0); - try { - setLoading(true); - let hash; - if (paymentType === 1) { - hash = await walletClient.sendTransaction({ - to: validAddress, - value: amount, - }); - } else { - hash = await transfer(walletClient, validToken, validAddress, amount); - } - setTxHash(hash); - const { chain } = walletClient; - // await waitForTransaction(chain, hash); - window.location.href = `/invoice/${chain.id.toString(16)}/${address}`; - } catch (e) { - setLoading(false); - logError({ depositError: e }); - } + const { handleDeposit, isLoading, isReady } = useDeposit({ + invoice, + amount, + hasAmount, // (+ gas) + paymentType: paymentType?.value, + }); + + const depositHandler = async () => { + const result = await handleDeposit(); + if (!result) return; + setTransaction(result.hash); }; + // const paymentTypeOptions = [ + // { value: PAYMENT_TYPES.TOKEN, label: parseTokenAddress(chainId, token) }, + // { value: PAYMENT_TYPES.NATIVE, label: TOKEN_DATA.nativeSymbol }, + // ]; useEffect(() => { - try { - if (!walletClient || !validToken) return; - const { chain, account } = walletClient; - if (paymentType === 0) { - balanceOf(chain, validToken, account.address).then(setBalance); - } else { - // TODO: get balance of native token - // provider.getBalance(account).then(setBalance); - } - } catch (balanceError) { - logError({ balanceError }); - } - }, [paymentType, validToken, walletClient]); + // setValue('paymentType', paymentTypeOptions[0]); + setValue('amount', '0'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { - if ( - depositError && - balance && - formatUnits(balance, decimals) > formatUnits(amount, decimals) - ) { - setDepositError(false); - } - }, [decimals, depositError, amount, balance]); + if (!amount) return; + + // setValue( + // 'checked', + // depositedMilestones(BigInt(deposited) + parseUnits(amount, 18), amounts), + // ); + }, [amount, deposited, amounts, setValue]); return ( Pay Invoice - - + At a minimum, you’ll need to deposit enough to cover the{' '} - {currentMilestone && Number(currentMilestone) === 0 ? 'first' : 'next'}{' '} - project payment. + {currentMilestone === 0 ? 'first' : 'next'} project payment. - {depositError ? ( - - - - - - Not enough available {symbol} for this deposit - - - - ) : null} - - + How much will you be depositing today? + + {_.map(amounts, (a: number, i: number) => ( + + {/* { + const newChecked = e.target.checked + ? checkedAtIndex(i, checked) + : checkedAtIndex(i - 1, checked); + const totAmount = amounts.reduce( + (tot: any, cur: any, ind: any) => + newChecked[ind] ? tot + BigInt(cur) : tot, + BigInt(0), + ); + const newAmount = + totAmount > BigInt(deposited) + ? totAmount - BigInt(deposited) + : BigInt(0); - - {validAmounts.map((a: any, i: any) => ( - { - const newChecked = e.target.checked - ? checkedAtIndex(i, checked) - : checkedAtIndex(i - 1, checked); - const totAmount = validAmounts.reduce( - (tot: any, cur: any, ind: any) => (newChecked[ind] ? tot + cur : tot), - BigInt(0), - ); - const newAmount = - totAmount && totAmount >= deposited - ? totAmount - deposited - : BigInt(0); - - setChecked(newChecked); - setAmount(newAmount); - setAmountInput(formatUnits(newAmount, decimals)); - }} - colorScheme="blue" - borderColor="lightgrey" - size="lg" - fontSize="1rem" - color="#323C47" - > - Payment #{i + 1}     - {formatUnits(a, decimals)} {symbol} - + setValue('amount', formatUnits(newAmount, 18)); + }} + color="yellow.300" + border="none" + size="lg" + fontSize="1rem" + fontFamily="texturina" + > + + Payment #{i + 1} -{' '} + {commify(formatUnits(BigInt(a), 18))}{' '} + {parseTokenAddress(chainId, token)} + + */} + ))} - - - Amount + OR - - {paymentType === 1 && ( - - - - )} - + + + + Enter a Manual Deposit Amount + + {/* {paymentType === PAYMENT_TYPES.NATIVE && ( + + + + )} */} - - + {/* { - const newAmountInput = e.target.value; - setAmountInput(newAmountInput); - if (newAmountInput) { - const newAmount = parseUnits(newAmountInput, decimals); - setAmount(newAmount); - setChecked( - getCheckedStatus(BigInt(deposited) + newAmount, validAmounts), - ); - } else { - setAmount(BigInt(0)); - setChecked(initialStatus); - } - }} - placeholder="Amount to Deposit" - pr={isWRAPPED ? '6rem' : '3.5rem'} - /> + variant="outline" + placeholder="0" + color="yellow.500" + defaultValue="0" + min={0} + max={amountsSum} + /> */} - - {isWRAPPED ? ( - + + {TOKEN_DATA.isWrapped ? ( + // test ) : ( - symbol + // options={paymentTypeOptions} + // value={paymentType} + // onChange={e => { + // setValue('paymentType', e); + // }} + // // width='100%' + // /> +
test
+ // parseTokenAddress(chainId, token) )} -
-
- {amount > due ? ( - - - +
+ + {/* {BigInt(amount) * BigInt(10) ** BigInt(decimals || 0) > due && ( + + - Your deposit is greater than the due amount! + Your deposit is greater than the total amount due! - ) : null} -
- - - {deposited !== undefined ? ( + )} */} + + + {/* {deposited && ( Total Deposited - {`${formatUnits(deposited, decimals)} ${symbol}`} - - ) : null} - {deposited !== undefined ? ( - - Potential Dispute Fee - {`${formatUnits( - (amount - deposited) * - BigInt(calculateResolutionFeePercentage(resolutionRate)), - decimals, - )} ${symbol}`} + + {`${commify( + formatUnits(BigInt(deposited), 18), + )} ${parseTokenAddress(chainId, token)}`} + - ) : null} - {due !== undefined ? ( + )} + {due && ( Total Due - {`${formatUnits(due, decimals)} ${symbol}`} + + {`${formatUnits(BigInt(due), 18)} ${parseTokenAddress( + chainId, + token, + )}`} + - ) : null} - {balance !== undefined ? ( + )} */} + {/* {displayBalance && ( Your Balance - {`${formatUnits(balance, decimals)} ${ - paymentType === 0 ? symbol : NATIVE_TOKEN_SYMBOL + {`${_.toNumber(displayBalance).toFixed(2)} ${ + paymentType === 0 + ? parseTokenAddress(chainId, token) + : TOKEN_DATA.nativeSymbol }`} - ) : null} + )} */} - {chainId && txHash && ( - + {transaction && ( + Follow your transaction{' '} here @@ -345,3 +305,5 @@ export const DepositFunds: React.FC = ({
); }; + +export default DepositFunds; diff --git a/packages/forms/src/EscrowDetailsForm.tsx b/packages/forms/src/EscrowDetailsForm.tsx new file mode 100644 index 00000000..fe96f12b --- /dev/null +++ b/packages/forms/src/EscrowDetailsForm.tsx @@ -0,0 +1,213 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { + Box, + Button, + Card, + // Checkbox, + // DatePicker, + Flex, + HStack, + // Input, + Stack, +} from '@chakra-ui/react'; +import { Invoice } from '@smart-invoice/types'; +import _ from 'lodash'; +import { useEffect } from 'react'; +import { useForm, UseFormReturn } from 'react-hook-form'; +import { useChainId } from 'wagmi'; +import * as Yup from 'yup'; +import { SUPPORTED_NETWORKS } from '@smart-invoice/constants'; + +const unsupportedNetwork = (chainId: number) => + !_.includes(SUPPORTED_NETWORKS, chainId); + +export const sevenDaysFromNow = () => { + const localDate = new Date(); + localDate.setDate(localDate.getDate() + 7); + return localDate; +}; + +const schema = Yup.object().shape({ + client: Yup.string().required('Client address is required'), + // TODO handle nested when for provider !== client + provider: Yup.string().when( + 'raidPartySplit', + (raidPartySplit: any, localSchema: any) => { + if (_.first(raidPartySplit)) return localSchema; + return localSchema.required('Raid party address is required'); + }, + ), + safetyValveDate: Yup.date() + .required('Safety valve date is required') + .min( + sevenDaysFromNow(), + 'Safety valve date must be at least a week in the future', + ), + daoSplit: Yup.boolean().required('DAO split is required'), + spoilsPercent: Yup.string(), + raidPartySplit: Yup.boolean().required('Raid party split is required'), +}); + +const EscrowDetailsForm = ({ + escrowForm, + raid, + updateStep, + backStep, +}: { + escrowForm: UseFormReturn; + raid: any; // IRaid; + updateStep: (i?: number) => void; + backStep: () => void; +}) => { + const chainId = useChainId(); + const { watch, setValue } = escrowForm; + const { provider, client, safetyValveDate, raidPartySplit } = watch(); + const localForm = useForm({ + resolver: yupResolver(schema), + }); + const { + handleSubmit, + setValue: localSetValue, + watch: localWatch, + } = localForm; + const { + // safetyValveDate: localSafetyValveDate, + daoSplit: localDaoSplit, + spoilsPercent: localSpoilsPercent, + raidPartySplit: localRaidPartySplit, + } = localWatch(); + + const saveEscrowValues = (values: Partial) => { + // update values in escrow form + setValue('client', values.client); + setValue('provider', values.provider); + setValue('safetyValveDate', values.safetyValveDate); + setValue('raidPartySplit', values.raidPartySplit); + setValue('daoSplit', values.daoSplit); + }; + + // values: Partial + const onSubmit = (values: any) => { + saveEscrowValues(values); + + // move form + if (localRaidPartySplit) updateStep(1); + else updateStep(2); + }; + + const onBack = () => { + const values = watch(); + saveEscrowValues(values); + + backStep(); + }; + + useEffect(() => { + // set initial local values + localSetValue('client', client || ''); + if (provider) localSetValue('provider', provider); + localSetValue('safetyValveDate', safetyValveDate || sevenDaysFromNow()); + if (_.isUndefined(raidPartySplit)) localSetValue('raidPartySplit', true); + else localSetValue('raidPartySplit', raidPartySplit); + // set daoSplit for zap, not used in form explicitly + localSetValue('daoSplit', !_.isUndefined(raid)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chainId, raid]); + + useEffect(() => { + localSetValue('spoilsPercent', localDaoSplit ? '10' : '0'); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [localDaoSplit]); + + return ( + + + + {/* */} + + + + + {/* */} + + + + {/* */} + + + + {!localRaidPartySplit && ( + + {/* */} + + )} + + + {/* + + */} + + + + + {!raid && ( + + )} + + + + + + ); +}; + +export default EscrowDetailsForm; diff --git a/packages/forms/src/InvoicePaymentDetails.tsx b/packages/forms/src/InvoicePaymentDetails.tsx new file mode 100644 index 00000000..47cf7913 --- /dev/null +++ b/packages/forms/src/InvoicePaymentDetails.tsx @@ -0,0 +1,330 @@ +import { + Card, + Divider, + Flex, + HStack, + Link, + Stack, + Text, + VStack, +} from '@chakra-ui/react'; +import { + commify, + getTxLink, + // ipfsUrl, + // parseTokenAddress, +} from '@smart-invoice/utils'; +import { Invoice } from '@smart-invoice/types'; +// import _ from 'lodash'; +import { formatUnits } from 'viem'; +import { useBalance, useChainId } from 'wagmi'; + +import { AccountLink } from '@smart-invoice/ui'; + +const InvoicePaymentDetails = ({ invoice }: { invoice: Invoice }) => { + const chainId = useChainId(); + + const { + client, + released, + total, + token, + address: invoiceAddress, + isLocked, + disputes, + resolutions, + terminationTime, + currentMilestone, + amounts, + // deposits, + releases, + resolver, + } = invoice; + + // console.log(invoiceAddress, token, chainId); + const { data } = useBalance({ + address: invoiceAddress, + token, + }); + const balance = data?.value || BigInt(0); + // console.log('balance', balance, isLoading, error, status); + + const deposited = BigInt(released) + balance; + const due = deposited > total ? BigInt(0) : BigInt(total) - deposited; + const dispute = + isLocked && disputes.length > 0 ? disputes[disputes.length - 1] : undefined; + const resolution = + !isLocked && resolutions.length > 0 + ? resolutions[resolutions.length - 1] + : undefined; + const isExpired = terminationTime <= new Date().getTime() / 1000; + const amount = BigInt( + currentMilestone < amounts.length ? amounts[currentMilestone] : 0, + ); + const isReleasable = !isLocked && balance >= amount && balance > 0; + + // const sum = BigInt(0); + + // console.log(amounts); + + return ( + + + + Total Project Amount + + {commify(formatUnits(BigInt(total), 18))}{' '} + {/* {parseTokenAddress(chainId, token)} */} + + + + {amounts.map((amt, index) => ( + // let tot = BigInt(0); + // let ind = -1; + // let full = false; + // if (deposits.length > 0) { + // for (let i = 0; i < deposits.length; i += 1) { + // tot += deposits[i].amount; + // console.log(tot); + // if (tot > sum) { + // ind = i; + // console.log(tot, sum, amt, full); + // if (tot - sum >= BigInt(amt)) { + // full = true; + // break; + // } + // } + // } + // } + // sum += BigInt(amt); + + // const totalPayments = _.sum(amounts); + // const paidPayments = _.difference( + // amounts, + // _.map(deposits, 'amount') + // ); + // const totalDeposits = _.sumBy(deposits, 'amount'); + // console.log(totalPayments, paidPayments, totalDeposits); + + + + Milestone #{index + 1} + {index < currentMilestone && releases.length > index && ( + + Released{' '} + {new Date( + releases[index].timestamp * 1000, + ).toLocaleDateString()} + + )} + + + + {/* {!(index < currentMilestone && releases.length > index) && + ind !== -1 && ( + + {full ? '' : 'Partially '}Deposited{' '} + {new Date( + deposits[ind].timestamp * 1000 + ).toLocaleDateString()} + + )} */} + {/* {`${commify(formatUnits(BigInt(amt), 18))} ${parseTokenAddress( + chainId, + invoice.token, + )}`} */} + + + ))} + + + {/* TODO use array */} + + Total Deposited + + {commify(formatUnits(deposited, 18))}{' '} + {/* {parseTokenAddress(chainId, invoice.token)} */} + + + + Total Released + + {commify(formatUnits(BigInt(released), 18))}{' '} + {/* {parseTokenAddress(chainId, invoice.token)} */} + + + + Remaining Amount Due + + {commify(formatUnits(due, 18))}{' '} + {/* {parseTokenAddress(chainId, invoice.token)} */} + + + + + {!dispute && !resolution && ( + + {isExpired || (due === BigInt(0) && !isReleasable) ? ( + <> + Remaining Balance + + {`${formatUnits(balance, 18)}`} + {/* )} ${parseTokenAddress(chainId, invoice.token)}`}{' '} */} + + + ) : ( + <> + + {isReleasable && 'Next Amount to Release'} + {!isReleasable && 'Total Due Today'} + + + {`${commify( + formatUnits(isReleasable ? amount : amount - balance, 18), + )}`} + {/* ${parseTokenAddress(chainId, invoice.token)}`} */} + + + )} + + )} + + {dispute && ( + + + Amount Locked + {`${formatUnits(balance, 18)}`} + {/* ${parseTokenAddress(chainId, invoice.token)}`} */} + + + {`A dispute is in progress with `} + +
+ {/* + View details on IPFS + */} +
+ + View transaction + +
+
+ )} + + {resolution && ( + + + Amount Dispersed + {`${formatUnits( + BigInt(resolution.clientAward) + + resolution.providerAward + + resolution.resolutionFee + ? resolution.resolutionFee + : 0, + 18, + )}`} + {/* ${parseTokenAddress(chainId, invoice.token)}`} */} + + + + + + {' has resolved the dispute and dispersed remaining funds'} +
+
+ {/* + View details on IPFS + */} +
+ + View transaction + +
+
+ + {/* {resolution.resolutionFee && ( + + {`${formatUnits( + BigInt(resolution.resolutionFee), + 18, + )} ${parseTokenAddress(chainId, invoice.token)} to `} + + + )} + + {`${formatUnits( + BigInt(resolution.clientAward), + 18, + )} ${parseTokenAddress(chainId, invoice.token)} to `} + + + + {`${formatUnits( + BigInt(resolution.providerAward), + 18, + )} ${parseTokenAddress(chainId, invoice.token)} to `} + + */} + +
+
+ )} +
+
+ ); +}; + +export default InvoicePaymentDetails; diff --git a/packages/forms/src/LockFunds.tsx b/packages/forms/src/LockFunds.tsx index baca689f..d525a9d8 100644 --- a/packages/forms/src/LockFunds.tsx +++ b/packages/forms/src/LockFunds.tsx @@ -1,134 +1,110 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Address, Hash, formatUnits } from 'viem'; -import { useWalletClient } from 'wagmi'; - import { Button, Flex, Heading, Image, Link, + Spinner, Text, + Textarea, VStack, - useBreakpointValue, - useToast, } from '@chakra-ui/react'; -import { waitForTransaction } from '@wagmi/core'; - -import { AccountLink } from '../shared/AccountLink'; -import { OrderedTextarea } from '../shared/OrderedInput'; +import { useLock } from '@smart-invoice/hooks'; import { - getHexChainId, getResolverInfo, getResolverString, - getTokenInfo, getTxLink, - isAddress, isKnownResolver, - logError, -} from '../utils/helpers'; -import { lock } from '../utils/invoice'; -import { uploadDisputeDetails } from '../utils/ipfs'; -import { Loader } from './Loader'; -import { Invoice } from '../graphql/fetchInvoice'; -import { ChainId } from '../constants/config'; -import { Network, TokenData } from '../types'; - -type LockFundsProps = { - invoice: Invoice, - balance: bigint, - tokenData: Record>; -} - -export function LockFunds({ invoice, balance, tokenData }: LockFundsProps) { - const { data: walletClient } = useWalletClient(); - const chainId = walletClient?.chain?.id; - const { network, address, resolver, token, resolutionRate } = invoice || {}; - const { decimals, symbol } = getTokenInfo(chainId, token, tokenData); - const [disputeReason, setDisputeReason] = useState(''); - const toast = useToast(); - - const fee = useMemo(() => resolutionRate ? `${formatUnits( - balance / resolutionRate, - decimals, - )} ${symbol}` : undefined, [balance, decimals, resolutionRate, symbol]); - - const validAddress = isAddress(address); - const validResolver = isAddress(resolver); - - const [locking, setLocking] = useState(false); - const [txHash, setTxHash] = useState(); - const buttonSize = useBreakpointValue({ base: 'md', md: 'lg' }); - - const lockFunds = useCallback(async () => { - if (walletClient && !locking && balance > 0 && disputeReason && validAddress) { - try { - setLocking(true); - const detailsHash = await uploadDisputeDetails({ - reason: disputeReason, - invoice: address, - amount: balance.toString(), - }); - const hash = await lock(walletClient, validAddress, detailsHash); - setTxHash(hash); - const txReceipt = await waitForTransaction({ chainId, hash }); - setLocking(false); - if (txReceipt.status === 'success') { - setTimeout(() => { - window.location.href = `/invoice/${getHexChainId( - network as Network, - )}/${address}`; - }, 2000); - } else { - toast({ - status: 'error', - title: 'Transaction failed', - description: ( - - Transaction failed - - Transaction {txReceipt.transactionHash} status is ' - {txReceipt.status}'. - - - ), - isClosable: true, - duration: 5000, - }); - } - } catch (lockError) { - setLocking(false); - logError({ lockError }); - } - } - }, [walletClient, locking, balance, disputeReason, validAddress, address, chainId, network, toast]); - - if (locking) { + // NETWORK_CONFIG, + // uploadDisputeDetails, +} from '@smart-invoice/utils'; +import { Invoice } from '@smart-invoice/types'; +import _ from 'lodash'; +import { useForm } from 'react-hook-form'; +import { formatUnits, Hex } from 'viem'; +import { useChainId } from 'wagmi'; + +// import LockImage from '../../assets/lock.svg'; +import { AccountLink } from '@smart-invoice/ui'; + +const parseTokenAddress = (chainId: number, address: Hex) => { + // eslint-disable-next-line no-restricted-syntax + // for (const [key, value] of Object.entries(NETWORK_CONFIG[chainId].TOKENS)) { + // // eslint-disable-next-line @typescript-eslint/no-explicit-any + // if ((value as any).address === _.toLower(address)) { + // return key; + // } + // } + return address; +}; + +const LockFunds = ({ + invoice, + balance, +}: { + invoice: Invoice; + balance: bigint; +}) => { + const chainId = useChainId(); + const { resolver, token, resolutionRate } = invoice; + + const localForm = useForm(); + const { watch, handleSubmit } = localForm; + + const fee = formatUnits( + resolutionRate === 0 ? BigInt(0) : BigInt(balance) / BigInt(resolutionRate), + 18, + ); + const feeDisplay = `${fee} ${parseTokenAddress(chainId, token)}`; + + const disputeReason = watch('disputeReason'); + const amount = formatUnits(BigInt(balance), 18); + + // const onSuccess = () => { + // // handle tx success + // // mark locked + // }; + + const { + writeAsync: lockFunds, + writeLoading, + txHash, + } = useLock({ + invoice, + disputeReason, + amount, + }); + + const resolverInfo = getResolverInfo(resolver, chainId); + const resolverDisplayName = isKnownResolver(resolver, chainId) + ? resolverInfo.name + : resolver; + + if (writeLoading) { return ( Locking Funds - {chainId && txHash && ( - + {txHash && ( + Follow your transaction{' '} here )} - - - - - lock - + ); } return ( - + Lock Funds - - + Locking freezes all remaining funds in the contract and initiates a dispute. - - + {'Once a dispute has been initiated, '} - - { validResolver ? : resolver } + {/* */} { ' will review your case, the project agreement and dispute reasoning before making a decision on how to fairly distribute remaining funds.' } - - - - {`Upon resolution, a fee of ${fee} will be deducted from the locked fund amount and sent to `} - - { validResolver ? : resolver } + placeholder="Dispute Reason" + localForm={localForm} + /> */} + + {`Upon resolution, a fee of ${feeDisplay} will be deducted from the locked fund amount and sent to `} + {/* */} {` for helping resolve this dispute.`} - - {validResolver && isKnownResolver(validResolver, chainId) && ( + {/* {isKnownResolver(chainId, resolver) && ( - Learn about {getResolverString(validResolver, chainId)} dispute process & + Learn about {getResolverString(chainId, resolver)} dispute process & terms - )} + )} */} ); -} +}; + +export default LockFunds; diff --git a/packages/forms/src/PaymentChunksForm.tsx b/packages/forms/src/PaymentChunksForm.tsx index 1c3e3d6a..44fbecec 100644 --- a/packages/forms/src/PaymentChunksForm.tsx +++ b/packages/forms/src/PaymentChunksForm.tsx @@ -12,11 +12,10 @@ import { VStack, } from '@chakra-ui/react'; -import { CreateContext } from '../context/CreateContext'; -import { QuestionIcon } from '../icons/QuestionIcon'; -import { getTokenInfo } from '../utils/helpers'; -import { ChainId } from '../constants/config'; -import { TokenData } from '../types'; +import { QuestionIcon } from '@smart-invoice/ui'; +import { getTokenInfo } from '@smart-invoice/utils'; +import { ChainId } from '@smart-invoice/constants'; +import { TokenData } from '@smart-invoice/types'; type PaymentChunksFormProps = { display: boolean; @@ -29,12 +28,10 @@ export function PaymentChunksForm({ }: PaymentChunksFormProps) { const { data: walletClient } = useWalletClient(); const chainId = walletClient?.chain?.id; - const { paymentToken, milestones, payments, setPayments, paymentDue } = - useContext(CreateContext); - const { decimals, symbol } = getTokenInfo(chainId, paymentToken, tokenData); + // const { decimals, symbol } = getTokenInfo(chainId, paymentToken, tokenData); return ( - {Array.from(Array(Number(milestones))).map(_val => ( + {/* {Array.from(Array(Number(milestones))).map(_val => ( Payment #{_val} @@ -84,7 +81,7 @@ export function PaymentChunksForm({ Total Amount Must Add Up to {formatUnits(paymentDue, decimals)}{' '} {symbol} - ) : null} + ) : null} */} ); } diff --git a/packages/forms/src/PaymentDetailsForm.tsx b/packages/forms/src/PaymentDetailsForm.tsx index 1b4b1d7d..bdf1d25c 100644 --- a/packages/forms/src/PaymentDetailsForm.tsx +++ b/packages/forms/src/PaymentDetailsForm.tsx @@ -4,10 +4,9 @@ import { useWalletClient } from 'wagmi'; import { Checkbox, Link, SimpleGrid, Text, VStack } from '@chakra-ui/react'; -import { ChainId } from '../constants/config'; -import { CreateContext } from '../context/CreateContext'; -import { OrderedInput, OrderedSelect } from '../shared/OrderedInput'; -import { TokenData } from '../types'; +import { ChainId } from '@smart-invoice/constants'; +import { OrderedInput, OrderedSelect } from '@smart-invoice/ui'; +import { TokenData } from '@smart-invoice/types'; import { getResolverInfo, getResolverString, @@ -16,8 +15,8 @@ import { getTokens, isAddress, isKnownResolver, -} from '../utils/helpers'; -import { getResolutionRateFromFactory } from '../utils/invoice'; + getResolutionRateFromFactory, +} from '@smart-invoice/utils'; export type PaymentDetailsFormProps = { display: boolean; @@ -35,33 +34,33 @@ export function PaymentDetailsForm({ const { id: chainId } = chain || {}; const RESOLVERS = useMemo(() => getResolvers(chainId), [chainId]); - const { - clientAddress, - setClientAddress, - paymentAddress, - setPaymentAddress, - paymentToken, - setPaymentToken, - paymentDue, - setPaymentDue, - milestones, - setMilestones, - arbitrationProvider, - setArbitrationProvider, - setPayments, - termsAccepted, - setTermsAccepted, - } = useContext(CreateContext); + // const { + // clientAddress, + // setClientAddress, + // paymentAddress, + // setPaymentAddress, + // paymentToken, + // setPaymentToken, + // paymentDue, + // setPaymentDue, + // milestones, + // setMilestones, + // arbitrationProvider, + // setArbitrationProvider, + // setPayments, + // termsAccepted, + // setTermsAccepted, + // } = useContext(CreateContext); const TOKENS = useMemo( () => getTokens(allTokens, chainId), [chainId, allTokens], ); - const { decimals, symbol } = useMemo( - () => getTokenInfo(chainId, paymentToken, tokenData), - [chainId, paymentToken, tokenData], - ); + // const { decimals, symbol } = useMemo( + // () => getTokenInfo(chainId, paymentToken, tokenData), + // [chainId, paymentToken, tokenData], + // ); const [arbitrationProviderType, setArbitrationProviderType] = useState('0'); const [paymentDueInput, setPaymentDueInput] = useState(''); @@ -72,27 +71,27 @@ export function PaymentDetailsForm({ const [milestonesInvalid, setMilestonesInvalid] = useState(false); const [resolutionRate, setResolutionRate] = useState(20); - useEffect(() => { - if (!chain || !arbitrationProvider) return; - getResolutionRateFromFactory(chain, arbitrationProvider).then( - setResolutionRate, - ); - }, [arbitrationProvider, chain]); + // useEffect(() => { + // if (!chain || !arbitrationProvider) return; + // getResolutionRateFromFactory(chain, arbitrationProvider).then( + // setResolutionRate, + // ); + // }, [arbitrationProvider, chain]); - useEffect(() => { - if (paymentDueInput && !Number.isNaN(Number(paymentDueInput))) { - const p = parseUnits(paymentDueInput, decimals); - setPaymentDue(p); - setPaymentInvalid(p <= 0); - } else { - setPaymentDue(BigInt(0)); - setPaymentInvalid(true); - } - }, [paymentToken, paymentDueInput, setPaymentDue, decimals]); + // useEffect(() => { + // if (paymentDueInput && !Number.isNaN(Number(paymentDueInput))) { + // const p = parseUnits(paymentDueInput, decimals); + // setPaymentDue(p); + // setPaymentInvalid(p <= 0); + // } else { + // setPaymentDue(BigInt(0)); + // setPaymentInvalid(true); + // } + // }, [paymentToken, paymentDueInput, setPaymentDue, decimals]); return ( - + /> */} - + /> */} {(paymentInvalid || milestonesInvalid) && ( - { @@ -230,9 +229,9 @@ export function PaymentDetailsForm({ }% of that milestone’s escrowed funds are automatically deducted as an arbitration fee to resolve the dispute.`} isDisabled /> - ) : null} + ) : null} */} - {!arbitrationProvider || + {/* {!arbitrationProvider || !isKnownResolver(arbitrationProvider, chainId) ? ( - )} + )} */} ); } diff --git a/packages/forms/src/PaymentsForm.tsx b/packages/forms/src/PaymentsForm.tsx new file mode 100644 index 00000000..65a8120f --- /dev/null +++ b/packages/forms/src/PaymentsForm.tsx @@ -0,0 +1,214 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { + Button, + Card, + Flex, + FormControl, + Heading, + HStack, + Icon, + IconButton, + NumberInput, + // RadioBox, + Stack, + Text, + Tooltip, +} from '@chakra-ui/react'; +import { commify } from '@smart-invoice/utils'; +import { Invoice } from '@smart-invoice/types'; +import _ from 'lodash'; +import { useEffect } from 'react'; +import { useFieldArray, useForm, UseFormReturn } from 'react-hook-form'; +// import { FaInfoCircle, FaPlusCircle, FaRegTrashAlt } from 'react-icons/fa'; +import { useChainId } from 'wagmi'; +import * as Yup from 'yup'; + +// TODO migrate to design system + +const tokens = (chainId: number) => { + if (chainId === 100) { + return ['WETH', 'WXDAI']; + } + if (chainId === 1) { + return ['WETH', 'DAI']; + } + return ['WETH', 'DAI', 'TEST']; +}; + +const validationSchema = Yup.object().shape({ + milestones: Yup.array().of( + Yup.object().shape({ + value: Yup.string().required('Milestone Amount is required'), + }), + ), + token: Yup.string().required('Token is required'), + // token: Yup.object().shape({ + // label: Yup.string(), + // value: Yup.string().required(), + // }), +}); + +const PaymentsForm = ({ + escrowForm, + updateStep, + backStep, +}: { + escrowForm: UseFormReturn; + updateStep: () => void; + backStep: (i?: number) => void; +}) => { + const chainId = useChainId(); + const { watch, setValue } = escrowForm; + const { milestones, token, raidPartySplit } = watch(); + const localForm = useForm({ + defaultValues: { + milestones: [{ value: '1000' }], + }, + resolver: yupResolver(validationSchema), + }); + const { + setValue: localSetValue, + handleSubmit, + watch: localWatch, + control, + getValues, + } = localForm; + const { milestones: localMilestones, token: localToken } = localWatch(); + + const setEscrowValues = (values: Partial) => { + // set values in escrow form + setValue('milestones', values.milestones); + setValue('token', values.token); + }; + + const onSubmit = (values: any) => { + setEscrowValues(values); + // navigate form + updateStep(); + }; + + const onBack = () => { + const values = getValues(); + setEscrowValues(values as Partial); + + if (raidPartySplit) backStep(); + else backStep(2); + }; + + const { + fields: milestonesFields, + append: appendMilestone, + remove: removeMilestone, + } = useFieldArray({ + name: 'milestones', + control, + }); + + useEffect(() => { + if (milestones) localSetValue('milestones', milestones); + localSetValue('token', token || tokens(chainId)[0]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const total = _.sumBy( + localMilestones, + (milestone: any) => _.toNumber(milestone.value) || 0, + ); + + return ( + + + + {/* */} + + + + + Milestone Amounts + +

Test

+ {/* */} +
+
+ {_.map(milestonesFields, (field, index) => { + const handleRemoveMilestone = () => { + removeMilestone(index); + }; + + return ( + + + {/* */} + + } + aria-label="remove milestone" + variant="outline" + onClick={handleRemoveMilestone} + /> + + ); + })} + + + {total && ( + + Total: {commify(total)} {localToken} + + )} + +
+ + + + + +
+ ); +}; + +export default PaymentsForm; diff --git a/packages/forms/src/ProjectDetailsForm.tsx b/packages/forms/src/ProjectDetailsForm.tsx index 636356df..1be460bb 100644 --- a/packages/forms/src/ProjectDetailsForm.tsx +++ b/packages/forms/src/ProjectDetailsForm.tsx @@ -1,135 +1,133 @@ -import React, { useContext, useState } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { + Box, + Button, + Card, + // DatePicker, + Flex, + HStack, + Input, + Stack, + Textarea, +} from '@chakra-ui/react'; +// import { ProjectDetails } from '@smart-invoice/types'; +import { useEffect } from 'react'; +import { useForm, UseFormReturn } from 'react-hook-form'; +import * as Yup from 'yup'; -import { SimpleGrid, Text, VStack } from '@chakra-ui/react'; +import { sevenDaysFromNow } from './EscrowDetailsForm'; -import { CreateContext } from '../context/CreateContext'; -import { - OrderedInput, - OrderedLinkInput, - OrderedTextarea, -} from '../shared/OrderedInput'; -import { formatDate } from '../utils/helpers'; +const validationSchema = Yup.object().shape({ + projectName: Yup.string().required('Project Name is required'), + projectDescription: Yup.string().required('Project Description is required'), + agreement: Yup.string().url('Agreement must be a valid URL'), + startDate: Yup.date().required('Start Date is required'), + endDate: Yup.date().required('End Date is required'), +}); -type ProjectDetailsFormProps = { - display: boolean; -}; +// interface ProjectDetailsForm extends ProjectDetails { +// agreement?: string; +// } -export function ProjectDetailsForm({ display }: ProjectDetailsFormProps) { - const { - startDate, - setStartDate, - endDate, - setEndDate, - safetyValveDate, - setSafetyValveDate, - projectName, - setProjectName, - projectDescription, - setProjectDescription, - projectAgreementSource, - setProjectAgreementSource, - projectAgreementLinkType, - setProjectAgreementLinkType, - } = useContext(CreateContext); +// : { +// escrowForm: UseFormReturn; +// updateStep: () => void; +// } - const startDateString = startDate ? formatDate(startDate) : ''; - const endDateString = endDate ? formatDate(endDate) : ''; - const safetyValveDateString = safetyValveDate - ? formatDate(safetyValveDate) - : ''; +const ProjectDetailsForm = ({ escrowForm, updateStep }: any) => { + const { setValue, watch } = escrowForm; + const { projectName, projectDescription, startDate, endDate } = watch(); + const localForm = useForm({ + resolver: yupResolver(validationSchema), + }); + const { + handleSubmit, + setValue: localSetValue, + watch: localWatch, + } = localForm; + const { startDate: localStartDate, endDate: localEndDate } = localWatch(); - const [nameInvalid, setNameInvalid] = useState(false); - const [dateInvalid, setDateInvalid] = useState(false); + const onSubmit = async (values: any) => { + const projectAgreement = []; + if (values.agreement) { + // TODO handle ipfs agreement link + projectAgreement.push({ + type: 'https', + src: values.agreement, + createdAt: Math.floor(Date.now() / 1000), + }); + } - return ( - - { - setProjectName(v); - setNameInvalid(v === ''); - }} - isInvalid={nameInvalid} - error={nameInvalid ? 'Cannot be empty' : ''} - tooltip="Choose something easily identifiable by you & your client. This is how the invoice will appear on your sortable invoices list later." - required="required" - /> + setValue('projectName', values.projectName); + setValue('projectDescription', values.projectDescription); + setValue('projectAgreement', projectAgreement); + setValue('startDate', values.startDate); + setValue('endDate', values.endDate); - + // move form + updateStep(); + }; - + useEffect(() => { + localSetValue('projectName', projectName || ''); + localSetValue('projectDescription', projectDescription || ''); + localSetValue('startDate', startDate || new Date()); + localSetValue('endDate', endDate || sevenDaysFromNow()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - - setStartDate(Date.parse(v))} - required="optional" - tooltip="This is the date you expect to begin work on this project." + return ( + + + {/* - - setEndDate(Date.parse(v))} - required="optional" - tooltip="This is the date you expect to complete work on this project." +