diff --git a/apps/wallet/src/ui/app/components/receipt-card/StatusIcon.tsx b/apps/wallet/src/ui/app/components/receipt-card/StatusIcon.tsx deleted file mode 100644 index 2dde1c6a873..00000000000 --- a/apps/wallet/src/ui/app/components/receipt-card/StatusIcon.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { ThumbUpFill32 } from '@iota/icons'; -import cl from 'clsx'; - -interface StatusIconProps { - status: boolean; -} - -export function StatusIcon({ status }: StatusIconProps) { - return ( -
-
- -
-
- ); -} diff --git a/apps/wallet/src/ui/app/components/receipt-card/index.tsx b/apps/wallet/src/ui/app/components/receipt-card/index.tsx index 88ddd1b34ef..10401f5e340 100644 --- a/apps/wallet/src/ui/app/components/receipt-card/index.tsx +++ b/apps/wallet/src/ui/app/components/receipt-card/index.tsx @@ -3,15 +3,22 @@ // SPDX-License-Identifier: Apache-2.0 import { useRecognizedPackages } from '_src/ui/app/hooks/useRecognizedPackages'; -import { useTransactionSummary, STAKING_REQUEST_EVENT, UNSTAKING_REQUEST_EVENT } from '@iota/core'; +import { + useTransactionSummary, + STAKING_REQUEST_EVENT, + UNSTAKING_REQUEST_EVENT, + formatDate, +} from '@iota/core'; import { type IotaTransactionBlockResponse } from '@iota/iota-sdk/client'; -import { DateCard } from '../../shared/date-card'; import { TransactionSummary } from '../../shared/transaction-summary'; import { StakeTxn } from './StakeTxn'; -import { StatusIcon } from './StatusIcon'; import { UnStakeTxn } from './UnstakeTxn'; +import { InfoBox, InfoBoxStyle, InfoBoxType } from '@iota/apps-ui-kit'; +import { CheckmarkFilled } from '@iota/ui-icons'; +import cl from 'clsx'; import { ExplorerLinkCard } from '../../shared/transaction-summary/cards/ExplorerLink'; +import { GasFees } from '../../pages/approval-request/transaction-request/GasFees'; interface TransactionStatusProps { success: boolean; @@ -19,14 +26,19 @@ interface TransactionStatusProps { } function TransactionStatus({ success, timestamp }: TransactionStatusProps) { + const txnDate = timestamp ? formatDate(Number(timestamp)) : ''; return ( -
- - - {success ? 'Transaction Success' : 'Transaction Failed'} - - {timestamp && } -
+ + } + > ); } @@ -43,6 +55,7 @@ export function ReceiptCard({ txn, activeAddress }: ReceiptCardProps) { currentAddress: activeAddress, recognizedPackagesList, }); + const isSender = txn.transaction?.data.sender === activeAddress; if (!summary) return null; @@ -50,26 +63,31 @@ export function ReceiptCard({ txn, activeAddress }: ReceiptCardProps) { const unstakeTxn = events?.find(({ type }) => type === UNSTAKING_REQUEST_EVENT); + const renderExplorerLinkCard = () => ( + + ); + // todo: re-using the existing staking cards for now if (stakedTxn || unstakeTxn) return (
{stakedTxn ? : null} {unstakeTxn ? : null} - + {renderExplorerLinkCard()}
); return ( -
- - +
+
+ + + {isSender && } +
+
{renderExplorerLinkCard()}
); } diff --git a/apps/wallet/src/ui/app/components/transactions-card/TxnImage.tsx b/apps/wallet/src/ui/app/components/transactions-card/TxnImage.tsx deleted file mode 100644 index eb33c3daa5a..00000000000 --- a/apps/wallet/src/ui/app/components/transactions-card/TxnImage.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { useGetNFTMeta } from '@iota/core'; -import { Text } from '_app/shared/text'; -import { NftImage } from '_components'; -import { cx } from 'class-variance-authority'; - -interface TxnImageProps { - id: string; - actionLabel?: string; -} - -//TODO merge all NFT image displays -export function TxnImage({ id, actionLabel }: TxnImageProps) { - const { data: nftMeta } = useGetNFTMeta(id); - - return nftMeta?.imageUrl ? ( -
- {actionLabel ? ( - - {actionLabel} - - ) : null} -
- -
- {nftMeta.name && ( - - {nftMeta.name} - - )} - {nftMeta.description && ( - - {nftMeta.description} - - )} -
-
-
- ) : null; -} diff --git a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/GasFees.tsx b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/GasFees.tsx index e8d61a7a23c..2460016d0fb 100644 --- a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/GasFees.tsx +++ b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/GasFees.tsx @@ -2,56 +2,52 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useTransactionData, useTransactionGasBudget } from '_src/ui/app/hooks'; -import { GAS_SYMBOL } from '_src/ui/app/redux/slices/iota-objects/Coin'; -import { type TransactionBlock } from '@iota/iota-sdk/transactions'; -import { formatAddress } from '@iota/iota-sdk/utils'; - -import { DescriptionItem, DescriptionList } from './DescriptionList'; -import { SummaryCard } from './SummaryCard'; +import { TitleSize, Badge, BadgeType, Title, Panel } from '@iota/apps-ui-kit'; +import { Collapsible } from '_src/ui/app/shared/collapse'; +import { GasSummary } from '_src/ui/app/shared/transaction-summary/cards/GasSummary'; +import { type GasSummaryType } from '@iota/core'; interface GasFeesProps { sender?: string; - transaction: TransactionBlock; + gasSummary?: GasSummaryType; + isEstimate?: boolean; + isPending?: boolean; + isError?: boolean; } +const DEFAULT_TITLE = 'Gas Fees'; -export function GasFees({ sender, transaction }: GasFeesProps) { - const { data: transactionData } = useTransactionData(sender, transaction); - const { data: gasBudget, isPending, isError } = useTransactionGasBudget(sender, transaction); - const isSponsored = - transactionData?.gasConfig.owner && - transactionData.sender !== transactionData.gasConfig.owner; +export function GasFees({ sender, gasSummary, isEstimate, isPending, isError }: GasFeesProps) { + const title = isEstimate ? `Est. ${DEFAULT_TITLE}` : DEFAULT_TITLE; + const trailingElement = + gasSummary?.isSponsored && gasSummary.owner ? ( +
+ +
+ ) : null; return ( - - Sponsored + +
+ ( + + )} + > + <div className="flex flex-col gap-y-sm p-md"> + <GasSummary + sender={sender} + gasSummary={gasSummary} + isPending={isPending} + isError={isError} + /> </div> - ) : null - } - initialExpanded - > - <DescriptionList> - <DescriptionItem title="You Pay"> - {isPending - ? 'Estimating...' - : isError - ? 'Gas estimation failed' - : `${isSponsored ? 0 : gasBudget} ${GAS_SYMBOL}`} - </DescriptionItem> - {isSponsored && ( - <> - <DescriptionItem title="Sponsor Pays"> - {gasBudget ? `${gasBudget} ${GAS_SYMBOL}` : '-'} - </DescriptionItem> - <DescriptionItem title="Sponsor"> - {formatAddress(transactionData!.gasConfig.owner!)} - </DescriptionItem> - </> - )} - </DescriptionList> - </SummaryCard> + </Collapsible> + </div> + </Panel> ); } diff --git a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/index.tsx b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/index.tsx index 4c4645f8591..a430c6fd180 100644 --- a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/index.tsx +++ b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/index.tsx @@ -100,13 +100,18 @@ export function TransactionRequest({ txRequest }: TransactionRequestProps) { isDryRun isLoading={isDryRunLoading} isError={isDryRunError} - showGasSummary={false} summary={summary} /> </div> <section className=" -mx-6 bg-white"> <div className="flex flex-col gap-4 p-6"> - <GasFees sender={addressForTransaction} transaction={transaction} /> + <GasFees + sender={addressForTransaction} + gasSummary={summary?.gas} + isEstimate + isError={isError} + isPending={isDryRunLoading} + /> <TransactionDetails sender={addressForTransaction} transaction={transaction} diff --git a/apps/wallet/src/ui/app/shared/ExpandableList.tsx b/apps/wallet/src/ui/app/shared/ExpandableList.tsx index 70070b4ebc8..475fe06300f 100644 --- a/apps/wallet/src/ui/app/shared/ExpandableList.tsx +++ b/apps/wallet/src/ui/app/shared/ExpandableList.tsx @@ -2,12 +2,11 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ChevronDown12 } from '@iota/icons'; import clsx from 'clsx'; import { useMemo, useState, type ReactNode } from 'react'; -import { Link } from './Link'; -import { Text } from './text'; +import { TriangleDown } from '@iota/ui-icons'; +import { Button, ButtonSize, ButtonType } from '@iota/apps-ui-kit'; interface ExpandableListProps { items: ReactNode[]; @@ -30,21 +29,27 @@ export function ExpandableList({ items, defaultItemsToShow }: ExpandableListProp <div key={index}>{item}</div> ))} {items.length > defaultItemsToShow && ( - <div className="flex w-full cursor-pointer items-center"> - <Link - onClick={handleShowAllClick} - after={ - <ChevronDown12 - height={12} - width={12} - className={clsx('text-steel hover:text-steel-dark', { - 'rotate-180': showAll, - })} + <div className="flex w-full cursor-pointer items-center justify-center"> + <Button + size={ButtonSize.Small} + type={ButtonType.Ghost} + onClick={(e) => { + e.stopPropagation(); + handleShowAllClick(); + }} + text={showAll ? 'Show Less' : 'Show All'} + iconAfterText + icon={ + <TriangleDown + className={clsx( + 'ml-xxxs h-5 w-5 text-neutral-60', + showAll + ? 'rotate-180 transition-transform ease-linear' + : 'rotate-0 transition-transform ease-linear', + )} /> } - > - <Text variant="bodySmall">{showAll ? 'Show Less' : 'Show All'}</Text> - </Link> + /> </div> )} </> diff --git a/apps/wallet/src/ui/app/shared/collapse/index.tsx b/apps/wallet/src/ui/app/shared/collapse/index.tsx index 40bf27ba342..c44607bc686 100644 --- a/apps/wallet/src/ui/app/shared/collapse/index.tsx +++ b/apps/wallet/src/ui/app/shared/collapse/index.tsx @@ -9,7 +9,6 @@ interface CollapsibleProps { title?: string; defaultOpen?: boolean; children: ReactNode | ReactNode[]; - shade?: 'lighter' | 'darker'; isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; titleSize?: TitleSize; @@ -24,7 +23,6 @@ export function Collapsible({ defaultOpen, isOpen, onOpenChange, - shade = 'lighter', titleSize = TitleSize.Small, render, hideArrow, diff --git a/apps/wallet/src/ui/app/shared/date-card/index.tsx b/apps/wallet/src/ui/app/shared/date-card/index.tsx deleted file mode 100644 index 36530d589d2..00000000000 --- a/apps/wallet/src/ui/app/shared/date-card/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { Text } from '_app/shared/text'; -import { formatDate } from '_helpers'; - -interface DateCardProps { - timestamp: number; - size: 'sm' | 'md'; -} - -export function DateCard({ timestamp, size }: DateCardProps) { - const txnDate = formatDate(timestamp, ['month', 'day', 'hour', 'minute']); - - return ( - <Text - color="steel-dark" - weight={size === 'sm' ? 'medium' : 'normal'} - variant={size === 'sm' ? 'subtitleSmallExtra' : 'pBodySmall'} - > - {txnDate} - </Text> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/Card.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/Card.tsx deleted file mode 100644 index ce9b6e3ba5e..00000000000 --- a/apps/wallet/src/ui/app/shared/transaction-summary/Card.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 -import { Heading } from '_src/ui/app/shared/heading'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { type AnchorHTMLAttributes, type ElementType, type ReactNode } from 'react'; - -const cardStyles = cva( - ['bg-white relative flex flex-col p-4.5 w-full shadow-card-soft rounded-2xl'], - { - variants: { - as: { - div: '', - a: 'no-underline text-hero-dark hover:text-hero visited:text-hero-dark', - }, - }, - }, -); - -interface CardProps extends VariantProps<typeof cardStyles> { - heading?: string; - after?: ReactNode; - children: ReactNode; - footer?: ReactNode; -} - -type ExtendedCardProps = CardProps & AnchorHTMLAttributes<HTMLAnchorElement>; - -export function SummaryCardFooter({ children }: { children: ReactNode }) { - return ( - <div className="bg-iota/10 -mx-4.5 -mb-4.5 flex items-center justify-between rounded-b-2xl px-4 py-2 "> - {children} - </div> - ); -} - -export function Card({ - as = 'div', - heading, - children, - after, - footer = null, - ...props -}: ExtendedCardProps) { - const Component = as as ElementType; - return ( - <Component className={cardStyles({ as })} {...props}> - {heading && ( - <div className="mb-4 flex items-center justify-between last-of-type:mb-0"> - <Heading variant="heading6" color="steel-darker"> - {heading} - </Heading> - {after && <div>{after}</div>} - </div> - )} - {children} - {footer} - </Component> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/OwnerFooter.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/OwnerFooter.tsx deleted file mode 100644 index 29169f50e95..00000000000 --- a/apps/wallet/src/ui/app/shared/transaction-summary/OwnerFooter.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { ExplorerLink, ExplorerLinkType } from '_components'; -import { useActiveAddress } from '_src/ui/app/hooks'; -import { getOwnerDisplay } from '@iota/core'; - -import { Text } from '../text'; -import { SummaryCardFooter } from './Card'; - -export function OwnerFooter({ owner, ownerType }: { owner?: string; ownerType?: string }) { - const address = useActiveAddress(); - const { ownerDisplay, isOwner } = getOwnerDisplay(owner, ownerType, address); - - return ( - <SummaryCardFooter> - <Text variant="pBody" weight="medium" color="steel-dark"> - Owner - </Text> - <div className="flex justify-end"> - {isOwner ? ( - <Text variant="body" weight="medium" color="hero-dark"> - {ownerDisplay} - </Text> - ) : ( - <ExplorerLink - type={ExplorerLinkType.Address} - title={owner} - address={owner} - className="text-hero-dark font-mono text-body font-medium no-underline" - > - {ownerDisplay} - </ExplorerLink> - )} - </div> - </SummaryCardFooter> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/BalanceChanges.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/BalanceChanges.tsx index 980264dd8fb..e6858f10121 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/BalanceChanges.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/cards/BalanceChanges.tsx @@ -1,21 +1,17 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { Alert, CoinIcon } from '_components'; -import { Text } from '_src/ui/app/shared/text'; + import { - CoinFormat, getRecognizedUnRecognizedTokenChanges, - useCoinMetadata, - useFormatCoin, type BalanceChange, type BalanceChangeSummary, } from '@iota/core'; -import classNames from 'clsx'; import { useMemo } from 'react'; -import { Card } from '../Card'; -import { OwnerFooter } from '../OwnerFooter'; +import { Badge, BadgeType, Divider, Header, KeyValueInfo, Panel } from '@iota/apps-ui-kit'; +import { useAddressLink } from '_src/ui/app/hooks/useAddressLink'; +import { CoinItem } from '_src/ui/app/components'; interface BalanceChangesProps { changes?: BalanceChangeSummary; @@ -23,41 +19,16 @@ interface BalanceChangesProps { function BalanceChangeEntry({ change }: { change: BalanceChange }) { const { amount, coinType, unRecognizedToken } = change; - const isPositive = BigInt(amount) > 0n; - const [formatted, symbol] = useFormatCoin(amount, coinType, CoinFormat.FULL); - const { data: coinMetaData } = useCoinMetadata(coinType); return ( - <div className="flex flex-col gap-2"> - <div className="flex justify-between"> - <div className="flex items-center gap-2"> - <div className="w-5"> - <CoinIcon coinType={coinType} /> - </div> - <div className="flex flex-wrap gap-2 gap-y-1 truncate"> - <Text variant="pBody" weight="semibold" color="steel-darker"> - {coinMetaData?.name || symbol} - </Text> - {unRecognizedToken && ( - <Alert mode="warning" spacing="sm" showIcon={false}> - <div className="item-center max-w-[70px] overflow-hidden truncate whitespace-nowrap text-captionSmallExtra font-medium uppercase leading-none tracking-wider"> - Unrecognized - </div> - </Alert> - )} - </div> - </div> - <div className="flex w-full justify-end text-right"> - <Text - variant="pBody" - weight="medium" - color={isPositive ? 'success-dark' : 'issue-dark'} - > - {isPositive ? '+' : ''} - {formatted} {symbol} - </Text> - </div> - </div> - </div> + <CoinItem + coinType={coinType} + balance={BigInt(amount)} + icon={ + unRecognizedToken ? ( + <Badge type={BadgeType.PrimarySoft} label="Unrecognized" /> + ) : undefined + } + /> ); } @@ -68,39 +39,47 @@ function BalanceChangeEntries({ changes }: { changes: BalanceChange[] }) { ); return ( - <div className="flex flex-col gap-2"> - <div className="flex flex-col gap-4 pb-3"> - {recognizedTokenChanges.map((change) => ( - <BalanceChangeEntry change={change} key={change.coinType + change.amount} /> - ))} - {unRecognizedTokenChanges.length > 0 && ( - <div - className={classNames( - 'flex flex-col gap-2 pt-2', - recognizedTokenChanges?.length && 'border-gray-45 border-t', - )} - > - {unRecognizedTokenChanges.map((change, index) => ( - <BalanceChangeEntry change={change} key={change.coinType + index} /> - ))} - </div> - )} - </div> - </div> + <> + {recognizedTokenChanges.map((change) => ( + <BalanceChangeEntry change={change} key={change.coinType + change.amount} /> + ))} + {unRecognizedTokenChanges.length > 0 && ( + <> + {unRecognizedTokenChanges.map((change, index) => ( + <BalanceChangeEntry change={change} key={change.coinType + index} /> + ))} + </> + )} + </> ); } export function BalanceChanges({ changes }: BalanceChangesProps) { if (!changes) return null; + return ( <> - {Object.entries(changes).map(([owner, changes]) => ( - <Card heading="Balance Changes" key={owner} footer={<OwnerFooter owner={owner} />}> - <div className="flex flex-col gap-4 pb-3"> - <BalanceChangeEntries changes={changes} /> - </div> - </Card> - ))} + {Object.entries(changes).map(([owner, changes]) => { + const ownerAddress = useAddressLink(owner); + + return ( + <Panel key={owner} hasBorder> + <div className="flex flex-col gap-y-sm overflow-hidden rounded-xl"> + <Header title="Balance Changes" /> + <BalanceChangeEntries changes={changes} /> + <div className="flex flex-col gap-y-sm px-md pb-md"> + <Divider /> + <KeyValueInfo + keyText="Owner" + valueText={ownerAddress.address} + valueLink={ownerAddress.explorerHref} + fullwidth + /> + </div> + </div> + </Panel> + ); + })} </> ); } diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/CoinStack.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/CoinStack.tsx deleted file mode 100644 index 39937a31606..00000000000 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/CoinStack.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { CoinIcon } from '_components'; - -import { Text } from '../../text'; - -export interface CoinsStackProps { - coinTypes: string[]; -} - -const MAX_COINS_TO_DISPLAY = 4; - -export function CoinsStack({ coinTypes }: CoinsStackProps) { - return ( - <div className="flex"> - {coinTypes.length > MAX_COINS_TO_DISPLAY && ( - <Text variant="bodySmall" weight="medium" color="steel-dark"> - +{coinTypes.length - MAX_COINS_TO_DISPLAY} - </Text> - )} - {coinTypes.slice(0, MAX_COINS_TO_DISPLAY).map((coinType, i) => ( - <div key={coinType} className={i === 0 ? '' : '-ml-1'}> - <CoinIcon coinType={coinType} /> - </div> - ))} - </div> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/GasSummary.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/GasSummary.tsx index 13941c5b185..7d4022ed3c7 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/GasSummary.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/cards/GasSummary.tsx @@ -2,22 +2,32 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useActiveAddress } from '_src/ui/app/hooks'; import { useFormatCoin, type GasSummaryType } from '@iota/core'; import { formatAddress, IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; import { KeyValueInfo } from '@iota/apps-ui-kit'; import { useAddressLink } from '_src/ui/app/hooks/useAddressLink'; +import { useActiveAddress } from '_src/ui/app/hooks'; interface GasSummaryProps { + sender?: string | null; gasSummary?: GasSummaryType; + isPending?: boolean; + isError?: boolean; } -export function GasSummary({ gasSummary }: GasSummaryProps) { +export function GasSummary({ sender, gasSummary, isPending, isError }: GasSummaryProps) { + const activeAddress = useActiveAddress(); + const address = sender || activeAddress; const [gas, symbol] = useFormatCoin(gasSummary?.totalGas, IOTA_TYPE_ARG); - const address = useActiveAddress(); const gasOwnerLink = useAddressLink(gasSummary?.owner || null); + const gasValueText = isPending + ? 'Estimating...' + : isError + ? 'Gas estimation failed' + : `${gasSummary?.isSponsored ? 0 : gas}`; + if (!gasSummary) return <KeyValueInfo keyText="Gas fee" valueText="0" supportingLabel={symbol} fullwidth />; @@ -26,8 +36,9 @@ export function GasSummary({ gasSummary }: GasSummaryProps) { {address === gasSummary?.owner && ( <KeyValueInfo keyText="Gas fee" - valueText={gasSummary?.isSponsored ? '0' : gas} + valueText={gasValueText} supportingLabel={symbol} + fullwidth /> )} {gasSummary?.isSponsored && gasSummary.owner && ( @@ -36,11 +47,13 @@ export function GasSummary({ gasSummary }: GasSummaryProps) { keyText="Sponsored fee" valueText={gas} supportingLabel={symbol} + fullwidth /> <KeyValueInfo keyText="Sponsor" valueText={formatAddress(gasSummary.owner)} valueLink={gasOwnerLink.explorerHref} + fullwidth /> </> )} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/ObjectChanges.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/ObjectChanges.tsx index cd922bf76c6..9457ba4a6be 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/ObjectChanges.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/cards/ObjectChanges.tsx @@ -1,9 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ExplorerLink, ExplorerLinkType } from '_components'; -import { Text } from '_src/ui/app/shared/text'; -import { Disclosure } from '@headlessui/react'; +import { ExplorerLinkType } from '_components'; import { getObjectChangeLabel, type ObjectChangesByOwner, @@ -11,26 +9,26 @@ import { type IotaObjectChangeTypes, type IotaObjectChangeWithDisplay, } from '@iota/core'; -import { ChevronDown12, ChevronRight12 } from '@iota/icons'; import { formatAddress } from '@iota/iota-sdk/utils'; import cx from 'clsx'; import { ExpandableList } from '../../ExpandableList'; -import { Card } from '../Card'; -import { OwnerFooter } from '../OwnerFooter'; import { ObjectChangeDisplay } from './objectSummary/ObjectChangeDisplay'; - -interface ChevronDownProps { - expanded: boolean; -} - -function ChevronDown({ expanded }: ChevronDownProps) { - return expanded ? ( - <ChevronDown12 className="text-gray-45" /> - ) : ( - <ChevronRight12 className="text-gray-45" /> - ); -} +import { Collapsible } from '../../collapse'; +import { + Badge, + BadgeType, + Divider, + KeyValueInfo, + Panel, + Title, + TitleSize, +} from '@iota/apps-ui-kit'; +import { useAddressLink } from '_src/ui/app/hooks/useAddressLink'; +import { useState } from 'react'; +import { useExplorerLink } from '_src/ui/app/hooks/useExplorerLink'; +import { Link } from 'react-router-dom'; +import { TriangleDown } from '@iota/ui-icons'; interface ObjectDetailProps { change: IotaObjectChangeWithDisplay; @@ -42,95 +40,80 @@ export function ObjectDetail({ change, display }: ObjectDetailProps) { if (change.type === 'transferred' || change.type === 'published') { return null; } + const [open, setOpen] = useState(false); + + const objectLink = useExplorerLink({ + type: ExplorerLinkType.Object, + objectID: change.objectId || '', + }); const [packageId, moduleName, typeName] = change.objectType?.split('<')[0]?.split('::') || []; + const packageIdLink = useExplorerLink({ + type: ExplorerLinkType.Object, + objectID: packageId || '', + }); + const moduleLink = useExplorerLink({ + type: ExplorerLinkType.Object, + objectID: packageId || '', + moduleName, + }); return ( - <Disclosure> - {({ open }) => ( - <div className="flex flex-col gap-1"> - <div className="grid cursor-pointer grid-cols-2 overflow-auto"> - <Disclosure.Button className="ouline-none text-steel-dark hover:text-steel-darker flex cursor-pointer select-none items-center gap-1 border-none bg-transparent p-0"> - <Text variant="pBody" weight="medium"> - Object - </Text> - {open ? ( - <ChevronDown12 className="text-gray-45" /> - ) : ( - <ChevronRight12 className="text-gray-45" /> - )} - </Disclosure.Button> - {change.objectId && ( - <div className="justify-self-end"> - <ExplorerLink - type={ExplorerLinkType.Object} - objectID={change.objectId} - className="text-hero-dark no-underline" - > - <Text variant="body" weight="medium" truncate mono> - {formatAddress(change.objectId)} - </Text> - </ExplorerLink> - </div> - )} + <Collapsible + hideBorder + onOpenChange={(isOpen) => setOpen(isOpen)} + hideArrow + render={() => ( + <div className="flex w-full flex-row items-center justify-between"> + <Title + size={TitleSize.Small} + title="Object" + trailingElement={ + <TriangleDown + className={cx( + 'ml-xxxs h-5 w-5 text-neutral-60', + open + ? 'rotate-0 transition-transform ease-linear' + : '-rotate-90 transition-transform ease-linear', + )} + /> + } + /> + <div className="flex flex-row items-center gap-xxs pr-md"> + <Badge type={BadgeType.PrimarySoft} label={typeName} /> + <Link + to={objectLink || ''} + target="_blank" + rel="noopener noreferrer" + className="text-body-md text-primary-30 dark:text-primary-80" + > + {formatAddress(change.objectId)} + </Link> </div> - <Disclosure.Panel> - <div className="flex flex-col gap-1"> - <div className="relative grid grid-cols-2 overflow-auto"> - <Text variant="pBody" weight="medium" color="steel-dark"> - Package - </Text> - <div className="flex justify-end"> - <ExplorerLink - type={ExplorerLinkType.Object} - objectID={packageId} - className="text-hero-dark justify-self-end overflow-auto text-captionSmall no-underline" - > - <Text variant="pBody" weight="medium" truncate mono> - {packageId} - </Text> - </ExplorerLink> - </div> - </div> - <div className="grid grid-cols-2 overflow-auto"> - <Text variant="pBody" weight="medium" color="steel-dark"> - Module - </Text> - <div className="flex justify-end"> - <ExplorerLink - type={ExplorerLinkType.Object} - objectID={packageId} - moduleName={moduleName} - className="text-hero-dark justify-self-end overflow-auto no-underline" - > - <Text variant="pBody" weight="medium" truncate mono> - {moduleName} - </Text> - </ExplorerLink> - </div> - </div> - <div className="grid grid-cols-2 overflow-auto"> - <Text variant="pBody" weight="medium" color="steel-dark"> - Type - </Text> - <div className="flex justify-end"> - <ExplorerLink - type={ExplorerLinkType.Object} - objectID={packageId} - moduleName={moduleName} - className="text-hero-dark justify-self-end overflow-auto no-underline" - > - <Text variant="pBody" weight="medium" truncate mono> - {typeName} - </Text> - </ExplorerLink> - </div> - </div> - </div> - </Disclosure.Panel> </div> )} - </Disclosure> + > + <div className="flex flex-col gap-y-sm px-md"> + <KeyValueInfo + keyText="Package" + valueText={formatAddress(packageId)} + valueLink={packageIdLink || ''} + fullwidth + /> + <KeyValueInfo + keyText="Module" + valueText={moduleName} + valueLink={moduleLink || ''} + fullwidth + /> + <KeyValueInfo + keyText="Type" + valueText={typeName} + valueLink={moduleLink || ''} + fullwidth + /> + </div> + </Collapsible> ); } @@ -143,77 +126,77 @@ export function ObjectChangeEntry({ changes, type }: ObjectChangeEntryProps) { return ( <> {Object.entries(changes).map(([owner, changes]) => { + const ownerAddress = useAddressLink(owner); + const label = getObjectChangeLabel(type); + const [open, setOpen] = useState(true); + return ( - <Card - footer={<OwnerFooter owner={owner} ownerType={changes.ownerType} />} - key={`${type}-${owner}`} - heading="Changes" - > - <Disclosure defaultOpen> - {({ open }) => ( - <div className={cx({ 'gap-4': open }, 'flex flex-col pb-3')}> - <Disclosure.Button - as="div" - className="flex w-full cursor-pointer flex-col gap-2" - > - <div className="flex w-full items-center gap-2"> - <Text - variant="body" - weight="semibold" - color={ - type === 'created' - ? 'success-dark' - : 'steel-darker' + <Panel key={`${type}-${owner}`} hasBorder> + <div className="flex flex-col gap-y-sm overflow-hidden rounded-xl"> + <Collapsible + hideBorder + defaultOpen + onOpenChange={(isOpen) => setOpen(isOpen)} + render={() => ( + <Title + size={TitleSize.Small} + title="Object Changes" + trailingElement={ + <div className="ml-1 flex"> + <Badge type={BadgeType.PrimarySoft} label={label} /> + </div> + } + /> + )} + > + <> + {!!changes.changesWithDisplay.length && ( + <div className="flex flex-1 flex-col gap-2 overflow-y-auto"> + <ExpandableList + defaultItemsToShow={5} + items={ + open + ? changes.changesWithDisplay.map( + (change) => ( + <ObjectChangeDisplay + change={change} + /> + ), + ) + : [] } - > - {getObjectChangeLabel(type)} - </Text> - <div className="bg-gray-40 h-px w-full" /> - <ChevronDown expanded={open} /> + /> </div> - </Disclosure.Button> - <Disclosure.Panel as="div" className="flex flex-col gap-4"> - <> - {!!changes.changesWithDisplay.length && ( - <div className="flex gap-2 overflow-y-auto"> - <ExpandableList - defaultItemsToShow={5} - items={ - open - ? changes.changesWithDisplay.map( - (change) => ( - <ObjectChangeDisplay - change={change} - /> - ), - ) - : [] - } - /> - </div> - )} + )} - <div className="flex w-full flex-col gap-2"> - <ExpandableList - defaultItemsToShow={5} - items={ - open - ? changes.changes.map((change) => ( - <ObjectDetail - ownerKey={owner} - change={change} - /> - )) - : [] - } - /> - </div> - </> - </Disclosure.Panel> - </div> - )} - </Disclosure> - </Card> + <div className="flex w-full flex-col gap-2"> + <ExpandableList + defaultItemsToShow={5} + items={ + open + ? changes.changes.map((change) => ( + <ObjectDetail + ownerKey={owner} + change={change} + /> + )) + : [] + } + /> + </div> + </> + </Collapsible> + <div className="flex flex-col gap-y-sm px-md pb-md"> + <Divider /> + <KeyValueInfo + keyText="Owner" + valueText={ownerAddress.address} + valueLink={ownerAddress.explorerHref} + fullwidth + /> + </div> + </div> + </Panel> ); })} </> diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/SummaryCard.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/SummaryCard.tsx deleted file mode 100644 index 29309677f6b..00000000000 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/SummaryCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 -import { ChevronDown16, ChevronRight16 } from '@iota/icons'; -import clsx from 'clsx'; -import { useState, type ReactNode } from 'react'; - -import { Text } from '../../../shared/text'; - -interface SummaryCardProps { - header: ReactNode; - children: ReactNode; - badge?: ReactNode; - initialExpanded?: boolean; -} - -export function SummaryCard({ - children, - header, - badge, - initialExpanded = false, -}: SummaryCardProps) { - const [expanded, setExpanded] = useState(initialExpanded); - - return ( - <div - className={clsx( - 'overflow-hidden rounded-2xl border border-solid', - expanded ? 'border-gray-45' : 'border-gray-40', - )} - > - <button - onClick={() => setExpanded((expanded) => !expanded)} - className="bg-gray-40 relative flex w-full cursor-pointer items-center gap-1.5 border-none px-4 py-2 text-left" - > - <div className="flex-1"> - <Text variant="captionSmall" weight="semibold" color="steel-darker"> - {header} - </Text> - </div> - - {badge} - - <div className="text-steel flex items-center justify-center"> - {expanded ? <ChevronDown16 /> : <ChevronRight16 />} - </div> - </button> - {expanded && <div className="px-4 py-3">{children}</div>} - </div> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/TotalAmount.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/TotalAmount.tsx deleted file mode 100644 index f5a3be5cc29..00000000000 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/TotalAmount.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Mysten Labs, Inc. -// Modifications Copyright (c) 2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 -import { Heading } from '_src/ui/app/shared/heading'; -import { Text } from '_src/ui/app/shared/text'; -import { useFormatCoin } from '@iota/core'; - -import { Card } from '../Card'; - -interface TotalAmountProps { - amount?: string; - coinType?: string; -} - -export function TotalAmount({ amount, coinType }: TotalAmountProps) { - const [formatted, symbol] = useFormatCoin(amount, coinType); - if (!amount) return null; - return ( - <Card> - <div className="flex items-center justify-between"> - <Text color="steel-darker" variant="pBody"> - Total Amount - </Text> - <div className="flex items-center gap-0.5"> - <Heading color="steel-darker" variant="heading2"> - {formatted} - </Heading> - <Text color="steel-darker" variant="body" weight="medium"> - {symbol} - </Text> - </div> - </div> - </Card> - ); -} diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx index e07060e6ada..a45ad65bc68 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/cards/objectSummary/ObjectChangeDisplay.tsx @@ -2,33 +2,37 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { ExplorerLink, ExplorerLinkType, NftImage } from '_components'; +import { ExplorerLinkType } from '_components'; import { type IotaObjectChangeWithDisplay } from '@iota/core'; -import { formatAddress } from '@iota/iota-sdk/utils'; -import { Text } from '../../../text'; +import { Card, CardAction, CardActionType, CardBody, CardImage, CardType } from '@iota/apps-ui-kit'; +import { ImageIcon } from '../../../image-icon'; +import { ArrowTopRight } from '@iota/ui-icons'; +import { useExplorerLink } from '_src/ui/app/hooks/useExplorerLink'; export function ObjectChangeDisplay({ change }: { change: IotaObjectChangeWithDisplay }) { const display = change?.display?.data; + const name = display?.name ?? ''; const objectId = 'objectId' in change && change?.objectId; + const explorerHref = useExplorerLink({ + type: ExplorerLinkType.Object, + objectID: objectId?.toString() ?? '', + }); if (!display) return null; + + function handleOpen() { + const newWindow = window.open(explorerHref!, '_blank', 'noopener,noreferrer'); + if (newWindow) newWindow.opener = null; + } + return ( - <div className="group relative w-32 min-w-min cursor-pointer whitespace-nowrap"> - <NftImage title={display.name ?? ''} src={display.image_url ?? ''} /> - {objectId && ( - <div className="full absolute bottom-2 left-1/2 -translate-x-1/2 justify-center rounded-lg bg-white/90 px-2 py-1 opacity-0 transition-opacity group-hover:opacity-100"> - <ExplorerLink - type={ExplorerLinkType.Object} - objectID={objectId} - className="text-hero-dark no-underline" - > - <Text variant="pBodySmall" truncate mono> - {formatAddress(objectId)} - </Text> - </ExplorerLink> - </div> - )} - </div> + <Card type={CardType.Default} onClick={handleOpen}> + <CardImage> + <ImageIcon src={display.image_url ?? ''} label={name} fallback="NFT" /> + </CardImage> + <CardBody title={name} subtitle={display.description ?? ''} /> + {objectId && <CardAction type={CardActionType.Link} icon={<ArrowTopRight />} />} + </Card> ); } diff --git a/apps/wallet/src/ui/app/shared/transaction-summary/index.tsx b/apps/wallet/src/ui/app/shared/transaction-summary/index.tsx index ceb45223314..2c9b5f519f3 100644 --- a/apps/wallet/src/ui/app/shared/transaction-summary/index.tsx +++ b/apps/wallet/src/ui/app/shared/transaction-summary/index.tsx @@ -2,58 +2,43 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 import { type TransactionSummary as TransactionSummaryType } from '@iota/core'; -import clsx from 'clsx'; -import { LoadingIndicator } from '_components'; import { Heading } from '../heading'; import { BalanceChanges } from './cards/BalanceChanges'; -import { ExplorerLinkCard } from './cards/ExplorerLink'; -import { GasSummary } from './cards/GasSummary'; import { ObjectChanges } from './cards/ObjectChanges'; +import { Loader } from '@iota/ui-icons'; export function TransactionSummary({ summary, isLoading, isError, isDryRun = false, - /* todo: remove this, we're using it until we update tx approval page */ - showGasSummary = false, }: { summary: TransactionSummaryType; isLoading?: boolean; isDryRun?: boolean; isError?: boolean; - showGasSummary?: boolean; }) { if (isError) return null; return ( - <section className="bg-iota/10 -mx-6 min-h-full"> + <> {isLoading ? ( <div className="flex items-center justify-center p-10"> - <LoadingIndicator /> + <Loader className="animate-spin" /> </div> ) : ( - <div> - <div className={clsx('px-5 py-8', { 'py-6': isDryRun })}> - <div className="flex flex-col gap-4"> - {isDryRun && ( - <div className="pl-4.5"> - <Heading variant="heading6" color="steel-darker"> - Do you approve these actions? - </Heading> - </div> - )} - <BalanceChanges changes={summary?.balanceChanges} /> - <ObjectChanges changes={summary?.objectSummary} /> - {showGasSummary && <GasSummary gasSummary={summary?.gas} />} - <ExplorerLinkCard - digest={summary?.digest} - timestamp={summary?.timestamp ?? undefined} - /> + <div className="flex flex-col gap-3"> + {isDryRun && ( + <div className="pl-4.5"> + <Heading variant="heading6" color="steel-darker"> + Do you approve these actions? + </Heading> </div> - </div> + )} + <BalanceChanges changes={summary?.balanceChanges} /> + <ObjectChanges changes={summary?.objectSummary} /> </div> )} - </section> + </> ); }