import { type ComponentProps, type ReactNode, type FormEvent, useCallback, useMemo, useState, } from "react"; import { DialogTrigger, TextField, Label, Input, Form, Group, } from "react-aria-components"; import { StateType, useAsync } from "../../hooks/use-async"; import { useToast } from "../../hooks/use-toast"; import { stringToTokens, tokensToString } from "../../tokens"; import { Button } from "../Button"; import { ErrorMessage } from "../ErrorMessage"; import { ModalDialog } from "../ModalDialog"; import { Tokens } from "../Tokens"; import PythTokensIcon from "../Tokens/pyth.svg"; type Props = Omit, "children"> & { enableWithZeroMax?: boolean | undefined; actionName: ReactNode; actionDescription: ReactNode; title?: ReactNode | undefined; submitButtonText?: ReactNode | undefined; max: bigint; children?: | ((amount: Amount) => ReactNode | ReactNode[]) | ReactNode | ReactNode[] | undefined; transfer?: ((amount: bigint) => Promise) | undefined; successMessage: ReactNode; }; export const TransferButton = ({ enableWithZeroMax, actionName, submitButtonText, actionDescription, title, max, transfer, children, isDisabled, successMessage, ...props }: Props) => { return transfer === undefined || isDisabled === true || (max === 0n && !enableWithZeroMax) ? ( ) : ( {children} ); }; type TransferDialogProps = Omit< ComponentProps, "children" > & { max: bigint; transfer: (amount: bigint) => Promise; submitButtonText: ReactNode; children?: | ((amount: Amount) => ReactNode | ReactNode[]) | ReactNode | ReactNode[] | undefined; successMessage: ReactNode; }; const TransferDialog = ({ max, transfer, submitButtonText, children, successMessage, ...props }: TransferDialogProps) => { const [closeDisabled, setCloseDisabled] = useState(false); return ( {({ close }) => ( {children} )} ); }; type DialogContentsProps = { max: bigint; children: Props["children"]; transfer: (amount: bigint) => Promise; setCloseDisabled: (value: boolean) => void; submitButtonText: ReactNode; close: () => void; successMessage: ReactNode; }; const DialogContents = ({ max, transfer, children, submitButtonText, setCloseDisabled, close, successMessage, }: DialogContentsProps) => { const { amount, setAmount, setMax, stringValue } = useAmountInput(max); const toast = useToast(); const validationError = useMemo(() => { switch (amount.type) { case AmountType.Empty: { return "Enter an amount"; } case AmountType.AboveMax: { return "Amount exceeds maximum"; } case AmountType.NotPositive: { return "Amount must be greater than zero"; } case AmountType.Invalid: { return "Enter a valid amount"; } case AmountType.Valid: { return; } } }, [amount]); const doTransfer = useCallback( () => amount.type === AmountType.Valid ? transfer(amount.amount) : Promise.reject(new InvalidAmountError()), [amount, transfer], ); const { execute, state } = useAsync(doTransfer); const handleSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); setCloseDisabled(true); execute() .then(() => { close(); toast.success(successMessage); }) .catch(() => { /* no-op since this is already handled in the UI using `state` and is logged in useAsync */ }) .finally(() => { setCloseDisabled(false); }); }, [execute, close, setCloseDisabled, toast, successMessage], ); return (
{max} Max
{state.type === StateType.Error && (
)}
{children && ( <>{typeof children === "function" ? children(amount) : children} )}
); }; const useAmountInput = (max: bigint) => { const [stringValue, setAmount] = useState(""); return { stringValue, setAmount, setMax: useCallback(() => { setAmount(tokensToString(max)); }, [setAmount, max]), amount: useMemo((): Amount => { if (stringValue === "") { return Amount.Empty(); } else { const amountAsTokens = stringToTokens(stringValue); if (amountAsTokens === undefined) { return Amount.Invalid(); } else if (amountAsTokens > max) { return Amount.AboveMax(amountAsTokens); } else if (amountAsTokens <= 0) { return Amount.NotPositive(amountAsTokens); } else { return Amount.Valid(amountAsTokens); } } }, [stringValue, max]), }; }; export enum AmountType { Empty, NotPositive, Valid, Invalid, AboveMax, } const Amount = { Empty: () => ({ type: AmountType.Empty as const }), NotPositive: (amount: bigint) => ({ type: AmountType.NotPositive as const, amount, }), Valid: (amount: bigint) => ({ type: AmountType.Valid as const, amount }), Invalid: () => ({ type: AmountType.Invalid as const }), AboveMax: (amount: bigint) => ({ type: AmountType.AboveMax as const, amount, }), }; type Amount = ReturnType<(typeof Amount)[keyof typeof Amount]>; class InvalidAmountError extends Error { constructor() { super("Invalid amount"); } }