diff --git a/apps/web/src/components/SendFlow/Token/SignPage.test.tsx b/apps/web/src/components/SendFlow/LocalSignPage.test.tsx similarity index 57% rename from apps/web/src/components/SendFlow/Token/SignPage.test.tsx rename to apps/web/src/components/SendFlow/LocalSignPage.test.tsx index cf7ff64b66..305dc1ba4b 100644 --- a/apps/web/src/components/SendFlow/Token/SignPage.test.tsx +++ b/apps/web/src/components/SendFlow/LocalSignPage.test.tsx @@ -1,7 +1,5 @@ import { Modal } from "@chakra-ui/react"; import { - type FA12TokenBalance, - type FA2TokenBalance, makeAccountOperations, mockFA2Token, mockImplicitAccount, @@ -9,20 +7,20 @@ import { } from "@umami/core"; import { type UmamiStore, addTestAccount, makeStore } from "@umami/state"; import { executeParams } from "@umami/test-utils"; -import { TEZ, parseContractPkh } from "@umami/tezos"; +import { TEZ, mockImplicitAddress, parseContractPkh } from "@umami/tezos"; -import { SignPage } from "./SignPage"; -import { render, screen, waitFor } from "../../../testUtils"; -import { type SignPageProps } from "../utils"; +import { LocalSignPage } from "./LocalSignPage"; +import { type LocalSignPageProps } from "./utils"; +import { render, screen, waitFor } from "../../testUtils"; jest.mock("@chakra-ui/react", () => ({ ...jest.requireActual("@chakra-ui/react"), useBreakpointValue: jest.fn(), })); -const fixture = (props: SignPageProps<{ token: FA12TokenBalance | FA2TokenBalance }>) => ( +const fixture = (props: LocalSignPageProps) => ( {}}> - + ); @@ -35,7 +33,33 @@ beforeEach(() => { const mockAccount = mockMnemonicAccount(0); const mockFAToken = mockFA2Token(0, mockAccount); -describe("", () => { + +describe("", () => { + describe("fee", () => { + it("displays the fee in tez", async () => { + const store = makeStore(); + addTestAccount(store, mockMnemonicAccount(0)); + const props: LocalSignPageProps = { + operations: { + ...makeAccountOperations(mockImplicitAccount(0), mockImplicitAccount(0), [ + { + type: "tez", + amount: "1000000", + recipient: mockImplicitAddress(1), + }, + ]), + estimates: [executeParams({ fee: 1234567 })], + }, + operationType: "tez", + }; + render(fixture(props), { store }); + + await waitFor(() => expect(screen.getByTestId("fee")).toHaveTextContent(`1.234567 ${TEZ}`)); + }); + }); +}); + +describe("", () => { const sender = mockImplicitAccount(0); const operations = { ...makeAccountOperations(sender, mockImplicitAccount(0), [ @@ -56,12 +80,10 @@ describe("", () => { }; describe("fee", () => { it("displays the fee in tez", async () => { - const props: SignPageProps<{ - token: FA12TokenBalance | FA2TokenBalance; - }> = { + const props: LocalSignPageProps = { operations, - mode: "single", - data: { token: mockFAToken }, + operationType: "token", + token: mockFAToken, }; render(fixture(props), { store }); @@ -71,12 +93,10 @@ describe("", () => { describe("token", () => { it("displays the correct symbol", async () => { - const props: SignPageProps<{ - token: FA12TokenBalance | FA2TokenBalance; - }> = { + const props: LocalSignPageProps = { operations, - mode: "single", - data: { token: mockFAToken }, + operationType: "token", + token: mockFAToken, }; render(fixture(props), { store }); @@ -88,12 +108,10 @@ describe("", () => { }); it("displays the correct amount", async () => { - const props: SignPageProps<{ - token: FA12TokenBalance | FA2TokenBalance; - }> = { + const props: LocalSignPageProps = { operations, - mode: "single", - data: { token: mockFA2Token(0, mockAccount, 1, 0) }, + operationType: "token", + token: mockFA2Token(0, mockAccount, 1, 0), }; render(fixture(props), { store }); diff --git a/apps/web/src/components/SendFlow/LocalSignPage.tsx b/apps/web/src/components/SendFlow/LocalSignPage.tsx new file mode 100644 index 0000000000..e4028a42a4 --- /dev/null +++ b/apps/web/src/components/SendFlow/LocalSignPage.tsx @@ -0,0 +1,138 @@ +import { + Flex, + FormControl, + FormLabel, + ModalBody, + ModalContent, + ModalFooter, + VStack, + useBreakpointValue, +} from "@chakra-ui/react"; +import { + type FA12TokenBalance, + type FA2TokenBalance, + type TezTransfer, + type TokenTransfer, +} from "@umami/core"; +import { type Address } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; +import { FormProvider } from "react-hook-form"; + +import { SignButton } from "./SignButton"; +import { SignPageFee } from "./SignPageFee"; +import { SignPageHeader } from "./SignPageHeader"; +import { type LocalSignPageProps, useSignPageHelpers } from "./utils"; +import { AddressTile } from "../AddressTile"; +import { TezTile, TokenTile } from "../AssetTiles"; +import { AdvancedSettingsAccordion } from "../AdvancedSettingsAccordion"; + +export const LocalSignPage = (props: LocalSignPageProps) => { + const { operations: initialOperations, token, operationType } = props; + const { fee, operations, estimationFailed, isLoading, form, signer, onSign } = + useSignPageHelpers(initialOperations); + const hideBalance = useBreakpointValue({ base: true, md: false }); + + const operation = operations.operations[0]; + + const fields: Record = {}; + + switch (operationType) { + case "tez": + fields["mutezAmount"] = (operation as TezTransfer).amount; + fields["to"] = (operation as TezTransfer).recipient; + fields["from"] = operations.sender.address; + break; + case "token": + if (!token) { + throw new CustomError("Token is required for token operation"); + } + fields["amount"] = (operation as TokenTransfer).amount; + fields["to"] = (operation as TokenTransfer).recipient; + fields["from"] = operations.sender.address; + fields["token"] = token; + break; + } + + const AddressLabelAndTile = (heading: string, address: Address | undefined) => { + if (!address) { + return null; + } + return ( + + {heading} + + + ); + }; + + const Fee = () => ( + + + + ); + + const MutezAndFee = (mutezAmount: string | undefined) => { + if (!mutezAmount) { + return null; + } + return ( + + Amount + + + + ); + }; + + const TokenAndFee = (token: FA12TokenBalance | FA2TokenBalance | undefined, amount: string) => { + if (!token) { + return null; + } + return ( + + Amount + + + + ); + }; + + const formFields = { + mutezAmount: MutezAndFee(fields["mutezAmount"]), + from: AddressLabelAndTile("From", fields["from"]), + to: AddressLabelAndTile("To", fields["to"]), + token: TokenAndFee(fields["token"], fields["amount"]), + }; + + const renderField = (key: keyof typeof formFields) => formFields[key]; + + return ( + + +
+ + + + + {renderField("mutezAmount")} + {renderField("token")} + {renderField("from")} + {renderField("to")} + + + + + + + + +
+
+ ); +}; diff --git a/apps/web/src/components/SendFlow/Tez/FormPage.test.tsx b/apps/web/src/components/SendFlow/Tez/FormPage.test.tsx index bad1e4d215..ae2941aeac 100644 --- a/apps/web/src/components/SendFlow/Tez/FormPage.test.tsx +++ b/apps/web/src/components/SendFlow/Tez/FormPage.test.tsx @@ -17,7 +17,6 @@ import { CustomError } from "@umami/utils"; import { BigNumber } from "bignumber.js"; import { FormPage } from "./FormPage"; -import { SignPage } from "./SignPage"; import { act, dynamicModalContextMock, @@ -27,6 +26,7 @@ import { userEvent, waitFor, } from "../../../testUtils"; +import { LocalSignPage } from "../LocalSignPage"; jest.mock("@umami/core", () => ({ ...jest.requireActual("@umami/core"), @@ -262,10 +262,9 @@ describe("
", () => { await act(() => user.click(submitButton)); expect(dynamicModalContextMock.openWith).toHaveBeenCalledWith( - ); diff --git a/apps/web/src/components/SendFlow/Tez/FormPage.tsx b/apps/web/src/components/SendFlow/Tez/FormPage.tsx index a36dc0dd32..a29f662c78 100644 --- a/apps/web/src/components/SendFlow/Tez/FormPage.tsx +++ b/apps/web/src/components/SendFlow/Tez/FormPage.tsx @@ -15,7 +15,6 @@ import { useGetAccountBalanceDetails } from "@umami/state"; import { type RawPkh, TEZ, TEZ_DECIMALS, parsePkh, tezToMutez } from "@umami/tezos"; import { FormProvider, useForm } from "react-hook-form"; -import { SignPage } from "./SignPage"; import { useColor } from "../../../styles/useColor"; import { KnownAccountsAutocomplete } from "../../AddressAutocomplete"; import { TezTile } from "../../AssetTiles"; @@ -42,8 +41,7 @@ export type FormValues = { export const FormPage = ({ ...props }: FormPageProps) => { const color = useColor(); const openSignPage = useOpenSignPageFormAction({ - SignPage, - signPageExtraData: undefined, + operationType: "tez", FormPage, defaultFormPageProps: props, toOperation, diff --git a/apps/web/src/components/SendFlow/Tez/SignPage.test.tsx b/apps/web/src/components/SendFlow/Tez/SignPage.test.tsx deleted file mode 100644 index 04e15a667d..0000000000 --- a/apps/web/src/components/SendFlow/Tez/SignPage.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Modal } from "@chakra-ui/react"; -import { makeAccountOperations, mockImplicitAccount, mockMnemonicAccount } from "@umami/core"; -import { addTestAccount, makeStore } from "@umami/state"; -import { executeParams } from "@umami/test-utils"; -import { TEZ, mockImplicitAddress } from "@umami/tezos"; - -import { SignPage } from "./SignPage"; -import { render, screen, waitFor } from "../../../testUtils"; -import { type SignPageProps } from "../utils"; - -jest.mock("@chakra-ui/react", () => ({ - ...jest.requireActual("@chakra-ui/react"), - useBreakpointValue: jest.fn(), -})); - -const fixture = (props: SignPageProps) => ( - {}}> - - -); - -describe("", () => { - describe("fee", () => { - it("displays the fee in tez", async () => { - const store = makeStore(); - addTestAccount(store, mockMnemonicAccount(0)); - const props: SignPageProps = { - operations: { - ...makeAccountOperations(mockImplicitAccount(0), mockImplicitAccount(0), [ - { - type: "tez", - amount: "1000000", - recipient: mockImplicitAddress(1), - }, - ]), - estimates: [executeParams({ fee: 1234567 })], - }, - mode: "single", - data: undefined, - }; - render(fixture(props), { store }); - - await waitFor(() => expect(screen.getByTestId("fee")).toHaveTextContent(`1.234567 ${TEZ}`)); - }); - }); -}); diff --git a/apps/web/src/components/SendFlow/Tez/SignPage.tsx b/apps/web/src/components/SendFlow/Tez/SignPage.tsx deleted file mode 100644 index 444dc7e666..0000000000 --- a/apps/web/src/components/SendFlow/Tez/SignPage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - Flex, - FormControl, - FormLabel, - ModalBody, - ModalContent, - ModalFooter, - useBreakpointValue, -} from "@chakra-ui/react"; -import { type TezTransfer } from "@umami/core"; -import { FormProvider } from "react-hook-form"; - -import { AddressTile } from "../../AddressTile/AddressTile"; -import { AdvancedSettingsAccordion } from "../../AdvancedSettingsAccordion"; -import { TezTile } from "../../AssetTiles/TezTile"; -import { SignButton } from "../SignButton"; -import { SignPageFee } from "../SignPageFee"; -import { SignPageHeader } from "../SignPageHeader"; -import { type SignPageProps, useSignPageHelpers } from "../utils"; - -export const SignPage = (props: SignPageProps) => { - const { operations: initialOperations } = props; - const { fee, operations, estimationFailed, isLoading, form, signer, onSign } = - useSignPageHelpers(initialOperations); - const hideBalance = useBreakpointValue({ base: true, md: false }); - - const { amount: mutezAmount, recipient } = operations.operations[0] as TezTransfer; - - return ( - - - - - - - Amount - - - - - - - From - - - - To - - - - - - - - - - - ); -}; diff --git a/apps/web/src/components/SendFlow/Token/FormPage.test.tsx b/apps/web/src/components/SendFlow/Token/FormPage.test.tsx index 11eba0205f..6fce4fd51b 100644 --- a/apps/web/src/components/SendFlow/Token/FormPage.test.tsx +++ b/apps/web/src/components/SendFlow/Token/FormPage.test.tsx @@ -13,7 +13,6 @@ import { executeParams } from "@umami/test-utils"; import { parseContractPkh } from "@umami/tezos"; import { FormPage, type FormValues } from "./FormPage"; -import { SignPage } from "./SignPage"; import { act, dynamicModalContextMock, @@ -23,6 +22,7 @@ import { userEvent, waitFor, } from "../../../testUtils"; +import { LocalSignPage } from "../LocalSignPage"; import { type FormPagePropsWithSender } from "../utils"; jest.mock("@umami/core", () => ({ @@ -241,11 +241,11 @@ describe("", () => { await act(() => user.click(submitButton)); expect(dynamicModalContextMock.openWith).toHaveBeenCalledWith( - ); expect(mockToast).not.toHaveBeenCalled(); diff --git a/apps/web/src/components/SendFlow/Token/FormPage.tsx b/apps/web/src/components/SendFlow/Token/FormPage.tsx index 7439c3a95b..4d27d10d82 100644 --- a/apps/web/src/components/SendFlow/Token/FormPage.tsx +++ b/apps/web/src/components/SendFlow/Token/FormPage.tsx @@ -23,7 +23,6 @@ import { import { type RawPkh, parseContractPkh, parsePkh } from "@umami/tezos"; import { FormProvider, useForm } from "react-hook-form"; -import { SignPage } from "./SignPage"; import { KnownAccountsAutocomplete } from "../../AddressAutocomplete"; import { TokenTile } from "../../AssetTiles"; import { FormPageHeader } from "../FormPageHeader"; @@ -51,8 +50,8 @@ export const FormPage = ( ) => { const { token } = props; const openSignPage = useOpenSignPageFormAction({ - SignPage, - signPageExtraData: { token }, + operationType: "token", + token, FormPage, defaultFormPageProps: props, toOperation: toOperation(token), diff --git a/apps/web/src/components/SendFlow/Token/SignPage.tsx b/apps/web/src/components/SendFlow/Token/SignPage.tsx deleted file mode 100644 index cae07bbcb6..0000000000 --- a/apps/web/src/components/SendFlow/Token/SignPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { - Flex, - FormControl, - FormLabel, - ModalBody, - ModalContent, - ModalFooter, - useBreakpointValue, -} from "@chakra-ui/react"; -import { type FA12TokenBalance, type FA2TokenBalance, type TokenTransfer } from "@umami/core"; -import { FormProvider } from "react-hook-form"; - -import { AddressTile } from "../../AddressTile/AddressTile"; -import { AdvancedSettingsAccordion } from "../../AdvancedSettingsAccordion"; -import { TokenTile } from "../../AssetTiles"; -import { SignButton } from "../SignButton"; -import { SignPageFee } from "../SignPageFee"; -import { SignPageHeader } from "../SignPageHeader"; -import { type SignPageProps, useSignPageHelpers } from "../utils"; - -export const SignPage = (props: SignPageProps<{ token: FA12TokenBalance | FA2TokenBalance }>) => { - const { - operations: initialOperations, - data: { token }, - } = props; - const { fee, operations, estimationFailed, isLoading, form, signer, onSign } = - useSignPageHelpers(initialOperations); - const hideBalance = useBreakpointValue({ base: true, md: false }); - const { amount, recipient } = operations.operations[0] as TokenTransfer; - - return ( - - -
- - - - Amount - - - - - - - From - - - - To - - - - - - - - -
-
- ); -}; diff --git a/apps/web/src/components/SendFlow/onSubmitFormActionHooks.tsx b/apps/web/src/components/SendFlow/onSubmitFormActionHooks.tsx index 7d290ac354..3d65a716c8 100644 --- a/apps/web/src/components/SendFlow/onSubmitFormActionHooks.tsx +++ b/apps/web/src/components/SendFlow/onSubmitFormActionHooks.tsx @@ -1,6 +1,12 @@ import { useToast } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; -import { type EstimatedAccountOperations, type Operation, estimate } from "@umami/core"; +import { + type EstimatedAccountOperations, + type FA12TokenBalance, + type FA2TokenBalance, + type Operation, + estimate, +} from "@umami/core"; import { estimateAndUpdateBatch, useAppDispatch, @@ -9,12 +15,8 @@ import { } from "@umami/state"; import { type FunctionComponent } from "react"; -import { - type BaseFormValues, - type FormPageProps, - type SignPageProps, - useMakeFormOperations, -} from "./utils"; +import { LocalSignPage } from "./LocalSignPage"; +import { type BaseFormValues, type FormPageProps, useMakeFormOperations } from "./utils"; // This file defines hooks to create actions when form is submitted. @@ -23,14 +25,11 @@ type OnSubmitFormAction = ( ) => Promise; type UseOpenSignPageArgs< - ExtraData, FormValues extends BaseFormValues, FormProps extends FormPageProps, > = { - // Sign page component to render. - SignPage: FunctionComponent>; - // Extra data to pass to the Sign page component (e.g. NFT or Token) - signPageExtraData: ExtraData; + operationType: "token" | "tez"; + token?: FA12TokenBalance | FA2TokenBalance; // Form page component to render when the user goes back from the sign page. FormPage: FunctionComponent; // Form page props, used to render the form page again when the user goes back from the sign page @@ -42,16 +41,15 @@ type UseOpenSignPageArgs< // Hook to open the sign page that knows how to get back to the form page. export const useOpenSignPageFormAction = < - SignPageData, FormValues extends BaseFormValues, FormProps extends FormPageProps, >({ - SignPage, - signPageExtraData, + operationType, + token, FormPage, defaultFormPageProps, toOperation, -}: UseOpenSignPageArgs): OnSubmitFormAction => { +}: UseOpenSignPageArgs): OnSubmitFormAction => { const { openWith } = useDynamicModalContext(); const makeFormOperations = useMakeFormOperations(toOperation); const network = useSelectedNetwork(); @@ -61,8 +59,7 @@ export const useOpenSignPageFormAction = < const estimatedOperations = await estimate(operations, network); return openWith( - openWith( ) } - mode="single" + operationType={operationType} operations={estimatedOperations} + token={token} /> ); }; diff --git a/apps/web/src/components/SendFlow/utils.tsx b/apps/web/src/components/SendFlow/utils.tsx index 6c5961a1f3..13be353432 100644 --- a/apps/web/src/components/SendFlow/utils.tsx +++ b/apps/web/src/components/SendFlow/utils.tsx @@ -6,6 +6,8 @@ import { type Account, type AccountOperations, type EstimatedAccountOperations, + type FA12TokenBalance, + type FA2TokenBalance, type ImplicitAccount, type Operation, estimate, @@ -45,6 +47,13 @@ export type BaseFormValues = { sender: RawPkh }; export type SignPageMode = "single" | "batch"; +export type LocalSignPageProps = { + goBack?: () => void; + operationType: "token" | "tez"; + token?: FA12TokenBalance | FA2TokenBalance; + operations: EstimatedAccountOperations; +}; + export type SignPageProps = { goBack?: () => void; operations: EstimatedAccountOperations;