Skip to content

Commit

Permalink
Merge pull request #38 from bleu/jean/cow-410-refactor-create-vesting-ui
Browse files Browse the repository at this point in the history
Refactor create vesting UI
  • Loading branch information
JeanNeiverth authored Oct 18, 2024
2 parents c65876c + 3838d45 commit 9e76b65
Show file tree
Hide file tree
Showing 40 changed files with 1,132 additions and 333 deletions.
2 changes: 1 addition & 1 deletion apps/claim-vesting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@bleu/tsconfig": "workspace:*",
"@bleu/ui": "0.1.131",
"@cowprotocol/cow-sdk": "^5.5.1",
"@cowprotocol/hook-dapp-lib": "1.0.0-RC1",
"@cowprotocol/hook-dapp-lib": "1.1.0-RC0",
"@ethersproject/providers": "5.7.2",
"babel-plugin-react-compiler": "0.0.0-experimental-6067d4e-20240923",
"lodash.debounce": "4.0.8",
Expand Down
6 changes: 5 additions & 1 deletion apps/create-vesting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"@bleu/ui": "0.1.131",
"@bleu/utils": "workspace:*",
"@cowprotocol/cow-sdk": "^5.5.1",
"@cowprotocol/hook-dapp-lib": "1.0.0-RC1",
"@cowprotocol/hook-dapp-lib": "1.1.0-RC0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "3.9.0",
"@radix-ui/react-icons": "1.3.0",
"@uniswap/sdk-core": "5.4.0",
Expand Down
18 changes: 13 additions & 5 deletions apps/create-vesting/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import "@bleu/cow-hooks-ui/global.css";
import { IFrameContextProvider } from "@bleu/cow-hooks-ui";
import Head from "next/head";
import type * as React from "react";
import { TokenAmountTypeProvider } from "#/context/TokenAmountType";
import { FormContextProvider } from "#/context/form";
import { TokenContextProvider } from "#/context/token";
import "@fortawesome/fontawesome-svg-core/styles.css";
import { config } from "@fortawesome/fontawesome-svg-core";
config.autoAddCss = false;

export default function Layout({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -13,11 +17,15 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<link rel="manifest" href="/manifest.json" />
</Head>
<IFrameContextProvider>
<TokenAmountTypeProvider>
<body className="flex flex-col h-full font-sans font-normal">
{children}
<TokenContextProvider>
<body className="bg-transparent">
<FormContextProvider>
<div className="font-sans font-normal scrollbar-w-1 scrollbar scrollbar-thumb-color-paper-darkest scrollbar-track-color-paper-darker h-screen overflow-y-scroll">
{children}
</div>
</FormContextProvider>
</body>
</TokenAmountTypeProvider>
</TokenContextProvider>
</IFrameContextProvider>
</html>
);
Expand Down
285 changes: 146 additions & 139 deletions apps/create-vesting/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,169 +2,176 @@

import {
ButtonPrimary,
ContentWrapper,
ClipBoardButton,
type HookDappContextAdjusted,
Input,
PeriodWithScaleInput,
TokenAmountInput,
Info,
Spinner,
Wrapper,
useIFrameContext,
useReadTokenContract,
} from "@bleu/cow-hooks-ui";
import { Form } from "@bleu/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { Token } from "@uniswap/sdk-core";
import { useCallback, useMemo } from "react";
import { useForm } from "react-hook-form";

import {
type CreateVestingFormData,
createVestingSchema,
periodScaleOptions,
} from "#/utils/schema";

import { useRouter } from "next/navigation";
import {
VestAllFromAccountCheckbox,
VestAllFromSwapCheckbox,
} from "#/components/Checkboxes";
import { useTokenAmountTypeContext } from "#/context/TokenAmountType";
import { useGetHooksTransactions } from "#/hooks/useGetHooksTransactions";
import { vestingFactoriesMapping } from "#/utils/vestingFactoriesMapping";
import { useCallback, useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";

import { ALL_SUPPORTED_CHAIN_IDS } from "@cowprotocol/cow-sdk";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { AmountInput } from "#/components/AmountInput";
import { PeriodInput } from "#/components/PeriodInput";
import { RecipientInput } from "#/components/RecipientInput";
import { VestAllFromAccountCheckbox } from "#/components/VestAllFromAccountCheckbox";
import { VestAllFromSwapCheckbox } from "#/components/VestAllFromSwapCheckbox";
import { VestUserInputCheckbox } from "#/components/VestUserInputCheckbox";
import { useTokenContext } from "#/context/token";
import { useFormatVariables } from "#/hooks/useFormatVariables";
import { decodeCalldata } from "#/utils/decodeCalldata";

export default function Page() {
const { vestAllFromSwap, vestAllFromAccount } = useTokenAmountTypeContext();
const { context, setHookInfo } = useIFrameContext();
const router = useRouter();

const form = useForm<CreateVestingFormData>({
resolver: zodResolver(createVestingSchema),
defaultValues: {
period: 1,
periodScale: "Day",
vestAllFromSwap: false,
},
const { context, publicClient } = useIFrameContext();
const [isEditHookLoading, setIsEditHookLoading] = useState(true);

const { token } = useTokenContext();

const { control, setValue } = useFormContext();

const vestUserInput = useWatch({ control, name: "vestUserInput" });
const vestAllFromSwap = useWatch({ control, name: "vestAllFromSwap" });
const vestAllFromAccount = useWatch({ control, name: "vestAllFromAccount" });
const amount = useWatch({ control, name: "amount" });

const {
userBalanceFloat,
swapAmountFloat,
allAfterSwapFloat,
formattedUserBalance,
formattedSwapAmount,
formattedAllAfterSwap,
} = useFormatVariables({
userBalance: token?.userBalance,
tokenDecimals: token?.decimals,
});

const getHooksTransactions = useGetHooksTransactions();
const tokenAddress = context?.orderParams?.buyTokenAddress as
| `0x${string}`
| undefined;

const { tokenSymbol, tokenDecimals } = useReadTokenContract({ tokenAddress });

const token = useMemo(
() =>
context?.chainId && tokenAddress && tokenDecimals
? new Token(context.chainId, tokenAddress, tokenDecimals, tokenSymbol)
: undefined,
[context?.chainId, tokenAddress, tokenDecimals, tokenSymbol],
);

const vestingEscrowFactoryAddress = useMemo(() => {
return context?.chainId
? vestingFactoriesMapping[context.chainId]
: undefined;
}, [context?.chainId]);

const onSubmitCallback = useCallback(
async (data: CreateVestingFormData) => {
if (!context?.account || !token || !vestingEscrowFactoryAddress) return;
const hookInfo = await getHooksTransactions({
token,
vestingEscrowFactoryAddress,
formData: data,
});
if (!hookInfo) return;
setHookInfo(hookInfo);
router.push("/signing");
},
[
context?.account,
token,
vestingEscrowFactoryAddress,
router.push,
setHookInfo,
getHooksTransactions,
],
);

const onSubmit = useMemo(
() => form.handleSubmit(onSubmitCallback),
[form, onSubmitCallback],
);
const loadHookInfo = useCallback(async () => {
if (
!context?.hookToEdit ||
!context.account ||
!publicClient ||
!token?.decimals ||
!isEditHookLoading
)
return;
try {
const data = await decodeCalldata(
context?.hookToEdit?.hook.callData as `0x${string}`,
token.decimals,
);
if (data) {
setValue("vestUserInput", data.vestUserInput);
setValue("vestAllFromSwap", data.vestAllFromSwap);
setValue("vestAllFromAccount", data.vestAllFromAccount);
setValue("recipient", data.recipient);
setValue("period", data.period);
setValue("amount", data.amount);
setIsEditHookLoading(false);
}
} catch {}
}, [
context?.hookToEdit,
context?.account,
publicClient,
token?.decimals,
setValue,
isEditHookLoading,
]);

if (context?.hookToEdit && isEditHookLoading) {
loadHookInfo();
}

if (!context)
// For some reason we had a bug with spinner being rendered without styling. Please use null here
return null;
return (
<div className="flex items-center justify-center w-full h-full bg-transparent text-color-text-paper">
<Spinner size="lg" style={{ color: "gray" }} />
</div>
);

if (!context.account)
return <span className="mt-10 text-center">Connect your wallet</span>;
return <span className="mt-10 text-center">Connect your wallet first</span>;

if (!context?.orderParams?.buyTokenAddress)
return (
<span className="mt-10 text-center">Provide a buy token in swap</span>
);

if (!ALL_SUPPORTED_CHAIN_IDS.includes(context.chainId)) {
return <span className="mt-10 text-center">Unsupported chain</span>;
}

const amountPreview = vestAllFromSwap
? formattedSwapAmount
: formattedAllAfterSwap;
const amountPreviewFullDecimals = vestAllFromSwap
? String(swapAmountFloat)
: String(allAfterSwapFloat);

const isOutOfFunds =
!!vestUserInput &&
!!amount &&
!!allAfterSwapFloat &&
amount > allAfterSwapFloat;

return (
<Form {...form} onSubmit={onSubmit} className="contents">
<Wrapper>
<ContentWrapper>
<Input
name="recipient"
label="Recipient"
placeholder="0xabc..."
autoComplete="off"
className="h-12 p-2.5 rounded-xl bg-color-paper-darker border-none placeholder:opacity-100"
/>
<br />
<div className="flex flex-col w-full xsm:gap-4 xsm:flex-row">
<PeriodWithScaleInput
periodScaleOptions={periodScaleOptions}
namePeriodValue="period"
namePeriodScale="periodScale"
type="number"
step="0.0000001"
label="Period"
validation={{ valueAsNumber: true, required: true }}
onKeyDown={(e) =>
["e", "E", "+", "-"].includes(e.key) && e.preventDefault()
}
/>
<br className="xsm:h-0 xsm:w-0" />
<TokenAmountInput
name="amount"
type="number"
step={`0.${"0".repeat(tokenDecimals ? tokenDecimals - 1 : 8)}1`}
token={token}
label="Amount"
placeholder="0.0"
autoComplete="off"
disabled={vestAllFromSwap || vestAllFromAccount}
validation={{
setValueAs: (v) =>
v === "" ? undefined : Number.parseInt(v, 10),
required: !(vestAllFromAccount || vestAllFromSwap),
}}
onKeyDown={(e) =>
["e", "E", "+", "-"].includes(e.key) && e.preventDefault()
}
/>
</div>
<br />
<Wrapper>
<div className="flex flex-col flex-grow py-4 gap-4 items-start justify-start text-center">
<RecipientInput />
<PeriodInput />
<AmountInput
token={token}
vestAllFromSwap={vestAllFromSwap}
vestAllFromAccount={vestAllFromAccount}
amountPreview={amountPreview}
amountPreviewFullDecimals={amountPreviewFullDecimals}
formattedUserBalance={formattedUserBalance}
userBalanceFloat={userBalanceFloat}
/>
<div className="flex flex-col gap-y-2">
<VestUserInputCheckbox />
<VestAllFromSwapCheckbox />
<VestAllFromAccountCheckbox />
<br />
</ContentWrapper>
<ButtonPrimary type="submit">
<ButtonText context={context} />
</ButtonPrimary>
</Wrapper>
</Form>
</div>
</div>
<Info content={<InfoContent />} />
<ButtonPrimary type="submit" disabled={isOutOfFunds}>
<ButtonText context={context} isOutOfFunds={isOutOfFunds} />
</ButtonPrimary>
</Wrapper>
);
}

const ButtonText = ({ context }: { context: HookDappContextAdjusted }) => {
const InfoContent = () => {
return (
<span className="cursor-default">
To access Vesting Post-hook contract after swap, connect with the
recipient wallet at{" "}
<ClipBoardButton
buttonText="llamapay.io/vesting"
contentToCopy="https://llamapay.io/vesting"
className="flex items-center justify-center gap-1 cursor-pointer"
/>
</span>
);
};

const ButtonText = ({
context,
isOutOfFunds,
}: { context: HookDappContextAdjusted; isOutOfFunds: boolean }) => {
if (isOutOfFunds)
return (
<span className="flex items-center justify-center gap-2">
<ExclamationTriangleIcon className="w-6 h-6" />
You won't have enough funds
</span>
);

if (context?.hookToEdit && context?.isPreHook)
return <span>Update Pre-hook</span>;
if (context?.hookToEdit && !context?.isPreHook)
Expand Down
5 changes: 1 addition & 4 deletions apps/create-vesting/src/app/signing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
import { BigNumber, type BigNumberish } from "ethers";
import { useCallback, useMemo, useState } from "react";
import type { Address } from "viem";
import { useTokenAmountTypeContext } from "#/context/TokenAmountType";

export default function Page() {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
Expand All @@ -27,13 +26,11 @@ export default function Page() {
cowShedProxy,
} = useIFrameContext();

const { vestAllFromSwap } = useTokenAmountTypeContext();

const submitHook = useSubmitHook({
actions,
context,
publicClient,
recipientOverride: vestAllFromSwap ? cowShedProxy : undefined,
recipientOverride: hookInfo?.recipientOverride,
});
const cowShedSignature = useCowShedSignature({
cowShed,
Expand Down
Loading

0 comments on commit 9e76b65

Please sign in to comment.