diff --git a/apps/wallet-dashboard/app/(protected)/migrations/page.tsx b/apps/wallet-dashboard/app/(protected)/migrations/page.tsx index b042465c49e..e2cd767e663 100644 --- a/apps/wallet-dashboard/app/(protected)/migrations/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/migrations/page.tsx @@ -25,12 +25,14 @@ import { STARDUST_BASIC_OUTPUT_TYPE, STARDUST_NFT_OUTPUT_TYPE, useFormatCoin } f import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { StardustOutputMigrationStatus } from '@/lib/enums'; import { MigrationObjectsPanel, MigrationDialog } from '@/components'; +import { useRouter } from 'next/navigation'; function MigrationDashboardPage(): JSX.Element { const account = useCurrentAccount(); const address = account?.address || ''; const queryClient = useQueryClient(); const iotaClient = useIotaClient(); + const router = useRouter(); const [isMigrationDialogOpen, setIsMigrationDialogOpen] = useState(false); const [selectedStardustObjectsCategory, setSelectedStardustObjectsCategory] = useState< StardustOutputMigrationStatus | undefined @@ -139,6 +141,11 @@ function MigrationDashboardPage(): JSX.Element { setSelectedStardustObjectsCategory(undefined); } + function handleMigrationDialogClose() { + setIsMigrationDialogOpen(false); + router.push('/'); + } + return (
)} diff --git a/apps/wallet-dashboard/components/Dialogs/MigrationDialog.tsx b/apps/wallet-dashboard/components/Dialogs/MigrationDialog.tsx deleted file mode 100644 index f1f661c2db4..00000000000 --- a/apps/wallet-dashboard/components/Dialogs/MigrationDialog.tsx +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { VirtualList } from '@/components'; -import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit'; -import { IotaObjectData } from '@iota/iota-sdk/client'; -import { useMigrationTransaction } from '@/hooks/useMigrationTransaction'; -import { - Button, - Dialog, - Header, - InfoBox, - InfoBoxStyle, - InfoBoxType, - KeyValueInfo, - LoadingIndicator, - Panel, - Title, - TitleSize, -} from '@iota/apps-ui-kit'; -import { useGroupedMigrationObjectsByExpirationDate } from '@/hooks'; -import { Loader, Warning } from '@iota/ui-icons'; -import { DialogLayout, DialogLayoutBody, DialogLayoutFooter } from './layout'; -import { MigrationObjectDetailsCard } from '../migration/migration-object-details-card'; -import { Collapsible, useFormatCoin } from '@iota/core'; -import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; -import { summarizeMigratableObjectValues } from '@/lib/utils'; -import toast from 'react-hot-toast'; - -interface MigrationDialogProps { - basicOutputObjects: IotaObjectData[] | undefined; - nftOutputObjects: IotaObjectData[] | undefined; - onSuccess: (digest: string) => void; - setOpen: (bool: boolean) => void; - open: boolean; - isTimelocked: boolean; -} - -export function MigrationDialog({ - basicOutputObjects = [], - nftOutputObjects = [], - onSuccess, - open, - setOpen, - isTimelocked, -}: MigrationDialogProps): JSX.Element { - const account = useCurrentAccount(); - const { - data: migrateData, - isPending: isMigrationPending, - isError: isMigrationError, - } = useMigrationTransaction(account?.address || '', basicOutputObjects, nftOutputObjects); - - const { - data: resolvedObjects = [], - isLoading, - error: isGroupedMigrationError, - } = useGroupedMigrationObjectsByExpirationDate( - [...basicOutputObjects, ...nftOutputObjects], - isTimelocked, - ); - - const { mutateAsync: signAndExecuteTransaction, isPending: isSendingTransaction } = - useSignAndExecuteTransaction(); - const { totalNotOwnedStorageDepositReturnAmount } = summarizeMigratableObjectValues({ - basicOutputs: basicOutputObjects, - nftOutputs: nftOutputObjects, - address: account?.address || '', - }); - - const [gasFee, gasFeeSymbol] = useFormatCoin(migrateData?.gasBudget, IOTA_TYPE_ARG); - const [totalStorageDepositReturnAmountFormatted, totalStorageDepositReturnAmountSymbol] = - useFormatCoin(totalNotOwnedStorageDepositReturnAmount.toString(), IOTA_TYPE_ARG); - - async function handleMigrate(): Promise { - if (!migrateData) return; - signAndExecuteTransaction( - { - transaction: migrateData.transaction, - }, - { - onSuccess: (tx) => { - onSuccess(tx.digest); - }, - }, - ) - .then(() => { - toast.success('Migration transaction has been sent'); - }) - .catch(() => { - toast.error('Migration transaction was not sent'); - }); - } - - return ( - - -
setOpen(false)} titleCentered /> - -
- {isGroupedMigrationError && !isLoading && ( - } - /> - )} - {isLoading ? ( - - ) : ( - <> - ( - - )} - > - <div className="h-[600px] pb-md--rs"> - <VirtualList - heightClassName="h-full" - overflowClassName="overflow-y-auto" - items={resolvedObjects} - estimateSize={() => 58} - render={(migrationObject) => ( - <MigrationObjectDetailsCard - migrationObject={migrationObject} - isTimelocked={isTimelocked} - /> - )} - /> - </div> - </Collapsible> - <Panel hasBorder> - <div className="flex flex-col gap-y-sm p-md"> - <KeyValueInfo - keyText="Legacy storage deposit" - value={totalStorageDepositReturnAmountFormatted || '-'} - supportingLabel={totalStorageDepositReturnAmountSymbol} - fullwidth - /> - <KeyValueInfo - keyText="Gas Fees" - value={gasFee || '-'} - supportingLabel={gasFeeSymbol} - fullwidth - /> - </div> - </Panel> - </> - )} - </div> - </DialogLayoutBody> - <DialogLayoutFooter> - <Button - text="Migrate" - disabled={isMigrationPending || isMigrationError || isSendingTransaction} - onClick={handleMigrate} - icon={ - isMigrationPending || isSendingTransaction ? ( - <Loader - className="h-4 w-4 animate-spin" - data-testid="loading-indicator" - /> - ) : null - } - iconAfterText - fullWidth - /> - </DialogLayoutFooter> - </DialogLayout> - </Dialog> - ); -} diff --git a/apps/wallet-dashboard/components/Dialogs/index.ts b/apps/wallet-dashboard/components/Dialogs/index.ts index db380d17927..1475376b5ec 100644 --- a/apps/wallet-dashboard/components/Dialogs/index.ts +++ b/apps/wallet-dashboard/components/Dialogs/index.ts @@ -7,4 +7,4 @@ export * from './Staking'; export * from './unstake'; export * from './vesting'; export * from './settings'; -export * from './MigrationDialog'; +export * from './migration'; diff --git a/apps/wallet-dashboard/components/Dialogs/migration/MigrationDialog.tsx b/apps/wallet-dashboard/components/Dialogs/migration/MigrationDialog.tsx new file mode 100644 index 00000000000..3fd91b0eb35 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/MigrationDialog.tsx @@ -0,0 +1,88 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; +import { useCurrentAccount, useSignAndExecuteTransaction } from '@iota/dapp-kit'; +import { IotaObjectData } from '@iota/iota-sdk/client'; +import { useMigrationTransaction } from '@/hooks/useMigrationTransaction'; +import { Dialog } from '@iota/apps-ui-kit'; +import toast from 'react-hot-toast'; +import { TransactionDialogView } from '../TransactionDialog'; +import { MigrationDialogView } from './enums'; +import { ConfirmMigrationView } from './views'; + +interface MigrationDialogProps { + handleClose: () => void; + basicOutputObjects: IotaObjectData[] | undefined; + nftOutputObjects: IotaObjectData[] | undefined; + onSuccess: (digest: string) => void; + setOpen: (bool: boolean) => void; + open: boolean; + isTimelocked: boolean; +} + +export function MigrationDialog({ + handleClose, + basicOutputObjects = [], + nftOutputObjects = [], + onSuccess, + open, + setOpen, + isTimelocked, +}: MigrationDialogProps): JSX.Element { + const account = useCurrentAccount(); + const [txDigest, setTxDigest] = useState<string>(''); + const [view, setView] = useState<MigrationDialogView>(MigrationDialogView.Confirmation); + + const { + data: migrateData, + isPending: isMigrationPending, + isError: isMigrationError, + } = useMigrationTransaction(account?.address || '', basicOutputObjects, nftOutputObjects); + + const { mutateAsync: signAndExecuteTransaction, isPending: isSendingTransaction } = + useSignAndExecuteTransaction(); + + async function handleMigrate(): Promise<void> { + if (!migrateData) return; + signAndExecuteTransaction( + { + transaction: migrateData.transaction, + }, + { + onSuccess: (tx) => { + onSuccess(tx.digest); + setTxDigest(tx.digest); + setView(MigrationDialogView.TransactionDetails); + }, + }, + ) + .then(() => { + toast.success('Migration transaction has been sent'); + }) + .catch(() => { + toast.error('Migration transaction was not sent'); + }); + } + + return ( + <Dialog open={open} onOpenChange={setOpen}> + {view === MigrationDialogView.Confirmation && ( + <ConfirmMigrationView + basicOutputObjects={basicOutputObjects} + nftOutputObjects={nftOutputObjects} + onSuccess={handleMigrate} + setOpen={setOpen} + isTimelocked={isTimelocked} + migrateData={migrateData} + isMigrationPending={isMigrationPending} + isMigrationError={isMigrationError} + isSendingTransaction={isSendingTransaction} + /> + )} + {view === MigrationDialogView.TransactionDetails && ( + <TransactionDialogView txDigest={txDigest} onClose={handleClose} /> + )} + </Dialog> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/migration/enums/index.ts b/apps/wallet-dashboard/components/Dialogs/migration/enums/index.ts new file mode 100644 index 00000000000..6f408e39b8c --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/enums/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './view.enums'; diff --git a/apps/wallet-dashboard/components/Dialogs/migration/enums/view.enums.ts b/apps/wallet-dashboard/components/Dialogs/migration/enums/view.enums.ts new file mode 100644 index 00000000000..5b16d31b836 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/enums/view.enums.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export enum MigrationDialogView { + Confirmation = 'Confirmation', + TransactionDetails = 'TransactionDetails', +} diff --git a/apps/wallet-dashboard/components/Dialogs/migration/index.ts b/apps/wallet-dashboard/components/Dialogs/migration/index.ts new file mode 100644 index 00000000000..41dd3ff2b30 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './MigrationDialog'; + +export * from './views'; diff --git a/apps/wallet-dashboard/components/Dialogs/migration/views/ConfirmMigrationView.tsx b/apps/wallet-dashboard/components/Dialogs/migration/views/ConfirmMigrationView.tsx new file mode 100644 index 00000000000..7066a922187 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/views/ConfirmMigrationView.tsx @@ -0,0 +1,169 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { MigrationObjectLoading, VirtualList } from '@/components'; +import { useCurrentAccount } from '@iota/dapp-kit'; +import { IotaObjectData } from '@iota/iota-sdk/client'; +import { + Button, + Header, + InfoBox, + InfoBoxStyle, + InfoBoxType, + KeyValueInfo, + Panel, + Skeleton, + Title, + TitleSize, +} from '@iota/apps-ui-kit'; +import { useGroupedMigrationObjectsByExpirationDate } from '@/hooks'; +import { Loader, Warning } from '@iota/ui-icons'; +import { Collapsible, useFormatCoin } from '@iota/core'; +import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; +import { summarizeMigratableObjectValues } from '@/lib/utils'; +import { MigrationObjectDetailsCard } from '@/components/migration/migration-object-details-card'; +import { DialogLayout, DialogLayoutBody, DialogLayoutFooter } from '../../layout'; +import { Transaction } from '@iota/iota-sdk/transactions'; + +interface ConfirmMigrationViewProps { + basicOutputObjects: IotaObjectData[] | undefined; + nftOutputObjects: IotaObjectData[] | undefined; + onSuccess: () => void; + setOpen: (bool: boolean) => void; + isTimelocked: boolean; + migrateData: + | { + transaction: Transaction; + gasBudget: string | number | null; + } + | undefined; + isMigrationPending: boolean; + isMigrationError: boolean; + isSendingTransaction: boolean; +} + +export function ConfirmMigrationView({ + basicOutputObjects = [], + nftOutputObjects = [], + onSuccess, + setOpen, + isTimelocked, + migrateData, + isMigrationPending, + isMigrationError, + isSendingTransaction, +}: ConfirmMigrationViewProps): JSX.Element { + const account = useCurrentAccount(); + + const { + data: resolvedObjects = [], + isLoading, + error: isGroupedMigrationError, + } = useGroupedMigrationObjectsByExpirationDate( + [...basicOutputObjects, ...nftOutputObjects], + isTimelocked, + ); + + const { totalNotOwnedStorageDepositReturnAmount } = summarizeMigratableObjectValues({ + basicOutputs: basicOutputObjects, + nftOutputs: nftOutputObjects, + address: account?.address || '', + }); + + const [gasFee, gasFeeSymbol] = useFormatCoin(migrateData?.gasBudget, IOTA_TYPE_ARG); + const [totalStorageDepositReturnAmountFormatted, totalStorageDepositReturnAmountSymbol] = + useFormatCoin(totalNotOwnedStorageDepositReturnAmount.toString(), IOTA_TYPE_ARG); + + return ( + <DialogLayout> + <Header title="Confirmation" onClose={() => setOpen(false)} titleCentered /> + <DialogLayoutBody> + <div className="flex h-full flex-col gap-y-md overflow-y-auto"> + {isGroupedMigrationError && !isLoading && ( + <InfoBox + title="Error" + supportingText="Failed to load migration objects" + style={InfoBoxStyle.Elevated} + type={InfoBoxType.Error} + icon={<Warning />} + /> + )} + {isLoading ? ( + <> + <Panel hasBorder> + <div className="flex flex-col gap-y-sm p-md"> + <Skeleton widthClass="w-40" heightClass="h-3.5" /> + <MigrationObjectLoading /> + </div> + </Panel> + <Panel hasBorder> + <div className="flex flex-col gap-y-md p-md"> + <Skeleton widthClass="w-full" heightClass="h-3.5" /> + <Skeleton widthClass="w-full" heightClass="h-3.5" /> + </div> + </Panel> + </> + ) : ( + <> + <Collapsible + defaultOpen + render={() => ( + <Title size={TitleSize.Small} title="Assets to Migrate" /> + )} + > + <div className="h-[500px] pb-md--rs xl:h-[600px]"> + <VirtualList + heightClassName="h-full" + overflowClassName="overflow-y-auto" + items={resolvedObjects} + estimateSize={() => 58} + render={(migrationObject) => ( + <MigrationObjectDetailsCard + migrationObject={migrationObject} + isTimelocked={isTimelocked} + /> + )} + /> + </div> + </Collapsible> + <Panel hasBorder> + <div className="flex flex-col gap-y-sm p-md"> + <KeyValueInfo + keyText="Legacy storage deposit" + value={totalStorageDepositReturnAmountFormatted || '-'} + supportingLabel={totalStorageDepositReturnAmountSymbol} + fullwidth + /> + <KeyValueInfo + keyText="Gas Fees" + value={gasFee || '-'} + supportingLabel={gasFeeSymbol} + fullwidth + /> + </div> + </Panel> + </> + )} + </div> + </DialogLayoutBody> + <DialogLayoutFooter> + <Button + text="Migrate" + disabled={isMigrationPending || isMigrationError || isSendingTransaction} + onClick={onSuccess} + icon={ + isMigrationPending || isSendingTransaction ? ( + <Loader + className="h-4 w-4 animate-spin" + data-testid="loading-indicator" + /> + ) : null + } + iconAfterText + fullWidth + /> + </DialogLayoutFooter> + </DialogLayout> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/migration/views/index.ts b/apps/wallet-dashboard/components/Dialogs/migration/views/index.ts new file mode 100644 index 00000000000..b5a03528f1e --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/migration/views/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './ConfirmMigrationView'; diff --git a/apps/wallet-dashboard/components/migration/MigrationObjectLoading.tsx b/apps/wallet-dashboard/components/migration/MigrationObjectLoading.tsx new file mode 100644 index 00000000000..297d1a8fa81 --- /dev/null +++ b/apps/wallet-dashboard/components/migration/MigrationObjectLoading.tsx @@ -0,0 +1,27 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Card, CardImage, ImageShape, Skeleton } from '@iota/apps-ui-kit'; + +export function MigrationObjectLoading() { + return ( + <div className="flex h-full max-h-full w-full flex-col overflow-hidden"> + {new Array(10).fill(0).map((_, index) => ( + <Card key={index}> + <CardImage shape={ImageShape.SquareRounded}> + <div className="h-10 w-10 animate-pulse bg-neutral-90 dark:bg-neutral-12" /> + <Skeleton widthClass="w-10" heightClass="h-10" isRounded={false} /> + </CardImage> + <div className="flex flex-col gap-y-xs"> + <Skeleton widthClass="w-40" heightClass="h-3.5" /> + <Skeleton widthClass="w-32" heightClass="h-3" hasSecondaryColors /> + </div> + <div className="ml-auto flex flex-col gap-y-xs"> + <Skeleton widthClass="w-20" heightClass="h-3.5" /> + <Skeleton widthClass="w-16" heightClass="h-3" hasSecondaryColors /> + </div> + </Card> + ))} + </div> + ); +} diff --git a/apps/wallet-dashboard/components/migration/MigrationObjectsPanel.tsx b/apps/wallet-dashboard/components/migration/MigrationObjectsPanel.tsx index 915cc56dda5..a3864b329cf 100644 --- a/apps/wallet-dashboard/components/migration/MigrationObjectsPanel.tsx +++ b/apps/wallet-dashboard/components/migration/MigrationObjectsPanel.tsx @@ -12,15 +12,11 @@ import { StardustOutputDetailsFilter } from '@/lib/enums'; import { Button, ButtonType, - Card, - CardImage, Chip, - ImageShape, InfoBox, InfoBoxStyle, InfoBoxType, Panel, - Skeleton, Title, } from '@iota/apps-ui-kit'; import type { IotaObjectData } from '@iota/iota-sdk/client'; @@ -30,6 +26,7 @@ import { useState } from 'react'; import { MigrationObjectDetailsCard } from './migration-object-details-card'; import VirtualList from '../VirtualList'; import { filterMigrationObjects } from '@/lib/utils'; +import { MigrationObjectLoading } from './MigrationObjectLoading'; const FILTERS = { migratable: STARDUST_MIGRATABLE_OBJECTS_FILTER_LIST, @@ -83,7 +80,7 @@ export function MigrationObjectsPanel({ </div> <div className="flex min-h-0 flex-col py-sm"> <div className="h-full flex-1 overflow-auto"> - {isLoading && <LoadingPanel />} + {isLoading && <MigrationObjectLoading />} {isErrored && !isLoading && ( <div className="flex h-full max-h-full w-full flex-col items-center"> <InfoBox @@ -116,26 +113,3 @@ export function MigrationObjectsPanel({ </div> ); } - -function LoadingPanel() { - return ( - <div className="flex h-full max-h-full w-full flex-col overflow-hidden"> - {new Array(10).fill(0).map((_, index) => ( - <Card key={index}> - <CardImage shape={ImageShape.SquareRounded}> - <div className="h-10 w-10 animate-pulse bg-neutral-90 dark:bg-neutral-12" /> - <Skeleton widthClass="w-10" heightClass="h-10" isRounded={false} /> - </CardImage> - <div className="flex flex-col gap-y-xs"> - <Skeleton widthClass="w-40" heightClass="h-3.5" /> - <Skeleton widthClass="w-32" heightClass="h-3" hasSecondaryColors /> - </div> - <div className="ml-auto flex flex-col gap-y-xs"> - <Skeleton widthClass="w-20" heightClass="h-3.5" /> - <Skeleton widthClass="w-16" heightClass="h-3" hasSecondaryColors /> - </div> - </Card> - ))} - </div> - ); -} diff --git a/apps/wallet-dashboard/components/migration/index.ts b/apps/wallet-dashboard/components/migration/index.ts index cf43709989c..2690e18d11b 100644 --- a/apps/wallet-dashboard/components/migration/index.ts +++ b/apps/wallet-dashboard/components/migration/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export * from './MigrationObjectsPanel'; +export * from './MigrationObjectLoading';