diff --git a/apps/wallet/src/ui/app/shared/collapse/index.tsx b/apps/core/src/components/collapsible/Collapsible.tsx similarity index 100% rename from apps/wallet/src/ui/app/shared/collapse/index.tsx rename to apps/core/src/components/collapsible/Collapsible.tsx diff --git a/apps/core/src/components/collapsible/index.ts b/apps/core/src/components/collapsible/index.ts new file mode 100644 index 00000000000..dbcea5060fa --- /dev/null +++ b/apps/core/src/components/collapsible/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './Collapsible'; diff --git a/apps/core/src/components/index.ts b/apps/core/src/components/index.ts index a2aa837dfc6..9f005714aa5 100644 --- a/apps/core/src/components/index.ts +++ b/apps/core/src/components/index.ts @@ -5,5 +5,5 @@ export * from './coin'; export * from './icon'; export * from './Inputs'; export * from './QR'; - +export * from './collapsible'; export * from './providers'; diff --git a/apps/core/src/hooks/index.ts b/apps/core/src/hooks/index.ts index 026f01c6656..39389bf9b1a 100644 --- a/apps/core/src/hooks/index.ts +++ b/apps/core/src/hooks/index.ts @@ -43,5 +43,8 @@ export * from './useTransactionData'; export * from './useGetStakingValidatorDetails'; export * from './useCursorPagination'; export * from './useTheme'; +export * from './useNFTBasicData'; +export * from './useOwnedNFT'; +export * from './useNftDetails'; export * from './stake'; diff --git a/apps/wallet/src/ui/app/hooks/useFileExtensionType.ts b/apps/core/src/hooks/useFileExtensionType.ts similarity index 100% rename from apps/wallet/src/ui/app/hooks/useFileExtensionType.ts rename to apps/core/src/hooks/useFileExtensionType.ts diff --git a/apps/wallet/src/ui/app/hooks/useMediaUrl.ts b/apps/core/src/hooks/useMediaUrl.ts similarity index 100% rename from apps/wallet/src/ui/app/hooks/useMediaUrl.ts rename to apps/core/src/hooks/useMediaUrl.ts diff --git a/apps/wallet/src/ui/app/hooks/useNFTBasicData.ts b/apps/core/src/hooks/useNFTBasicData.ts similarity index 92% rename from apps/wallet/src/ui/app/hooks/useNFTBasicData.ts rename to apps/core/src/hooks/useNFTBasicData.ts index 2b9360bf94a..227a9707de1 100644 --- a/apps/wallet/src/ui/app/hooks/useNFTBasicData.ts +++ b/apps/core/src/hooks/useNFTBasicData.ts @@ -7,7 +7,7 @@ import type { IotaObjectData } from '@iota/iota-sdk/client'; import useFileExtensionType from './useFileExtensionType'; import useMediaUrl from './useMediaUrl'; -export default function useNFTBasicData(nftObj: IotaObjectData | null) { +export function useNFTBasicData(nftObj: IotaObjectData | null) { const nftObjectID = nftObj?.objectId || null; const filePath = useMediaUrl(nftObj?.content || null); let objType = null; diff --git a/apps/core/src/hooks/useNftDetails.ts b/apps/core/src/hooks/useNftDetails.ts new file mode 100644 index 00000000000..6e1941e4579 --- /dev/null +++ b/apps/core/src/hooks/useNftDetails.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +import { + useGetNFTMeta, + useOwnedNFT, + useNFTBasicData, + useGetKioskContents, + useIsAssetTransferable, +} from './'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { truncateString } from '../utils'; + +type NftField = { keys: string[]; values: string[] }; + +type NftFields = { + metadata?: { fields?: { attributes?: { fields?: NftField } } }; +}; + +export function useNftDetails(nftId: string, accountAddress: string | null) { + const { data: objectData, isPending: isNftLoading } = useOwnedNFT(nftId || '', accountAddress); + const { data } = useGetKioskContents(accountAddress); + + const isContainedInKiosk = data?.lookup.get(nftId!); + const kioskItem = data?.list.find((k) => k.data?.objectId === nftId); + + const { data: isTransferable } = useIsAssetTransferable(objectData); + + const { nftFields } = useNFTBasicData(objectData); + + const { data: nftMeta, isPending: isPendingMeta } = useGetNFTMeta(nftId); + + const nftName = nftMeta?.name || formatAddress(nftId); + const nftImageUrl = nftMeta?.imageUrl || ''; + + // Extract either the attributes, or use the top-level NFT fields: + const { keys: metaKeys, values: metaValues } = + (nftFields as NftFields)?.metadata?.fields?.attributes?.fields || + Object.entries(nftFields ?? {}) + .filter(([key]) => key !== 'id') + .reduce( + (acc, [key, value]) => { + acc.keys.push(key); + acc.values.push(value as string); + return acc; + }, + { keys: [], values: [] }, + ); + + const ownerAddress = + (objectData?.owner && + typeof objectData?.owner === 'object' && + 'AddressOwner' in objectData.owner && + objectData.owner.AddressOwner) || + ''; + + function formatMetaValue(value: string | object) { + if (typeof value === 'object') { + return { + value: JSON.stringify(value), + valueLink: undefined, + }; + } else { + if (value.includes('http')) { + return { + value: value.startsWith('http') + ? truncateString(value, 20, 8) + : formatAddress(value), + valueLink: value, + }; + } + return { + value: value, + valueLink: undefined, + }; + } + } + + return { + objectData, + isNftLoading, + nftName, + nftImageUrl, + ownerAddress, + isTransferable, + metaKeys, + metaValues, + formatMetaValue, + isContainedInKiosk, + kioskItem, + nftMeta, + isPendingMeta, + }; +} diff --git a/apps/wallet/src/ui/app/hooks/useOwnedNFT.ts b/apps/core/src/hooks/useOwnedNFT.ts similarity index 94% rename from apps/wallet/src/ui/app/hooks/useOwnedNFT.ts rename to apps/core/src/hooks/useOwnedNFT.ts index 0e308a484ce..a7f96ac25d2 100644 --- a/apps/wallet/src/ui/app/hooks/useOwnedNFT.ts +++ b/apps/core/src/hooks/useOwnedNFT.ts @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetKioskContents, useGetObject } from '@iota/core'; +import { useGetKioskContents, useGetObject } from './'; import { useMemo } from 'react'; export function useOwnedNFT(nftObjectId: string | null, address: string | null) { diff --git a/apps/core/src/utils/index.ts b/apps/core/src/utils/index.ts index 4d57564fa92..9973f811e10 100644 --- a/apps/core/src/utils/index.ts +++ b/apps/core/src/utils/index.ts @@ -20,6 +20,7 @@ export * from './getDelegationDataByStakeId'; export * from './api-env'; export * from './getExplorerPaths'; export * from './getExplorerLink'; +export * from './truncateString'; export * from './stake'; export * from './transaction'; diff --git a/apps/wallet/src/ui/app/helpers/truncateString.ts b/apps/core/src/utils/truncateString.ts similarity index 100% rename from apps/wallet/src/ui/app/helpers/truncateString.ts rename to apps/core/src/utils/truncateString.ts diff --git a/apps/ui-kit/src/lib/components/organisms/accordion/Accordion.tsx b/apps/ui-kit/src/lib/components/organisms/accordion/Accordion.tsx index ce4d80f97c6..1489c52c1d5 100644 --- a/apps/ui-kit/src/lib/components/organisms/accordion/Accordion.tsx +++ b/apps/ui-kit/src/lib/components/organisms/accordion/Accordion.tsx @@ -7,7 +7,7 @@ import { ArrowDown } from '@iota/ui-icons'; import { Button, ButtonType } from '@/lib'; import { ICON_STYLE } from './accordion.classes'; -interface AccordionHeaderProps { +export interface AccordionHeaderProps { /** * Flag for show/hide content */ diff --git a/apps/wallet-dashboard/app/(protected)/assets/page.tsx b/apps/wallet-dashboard/app/(protected)/assets/page.tsx index 4ff62fed3f8..0e76fadc1bf 100644 --- a/apps/wallet-dashboard/app/(protected)/assets/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/assets/page.tsx @@ -10,6 +10,7 @@ import { IotaObjectData } from '@iota/iota-sdk/client'; import { useState } from 'react'; import { AssetCategory } from '@/lib/enums'; import { AssetList } from '@/components/AssetsList'; +import { AssetDialog } from '@/components/Dialogs/Assets'; const OBJECTS_PER_REQ = 50; @@ -25,6 +26,7 @@ const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [ ]; export default function AssetsDashboardPage(): React.JSX.Element { + const [selectedAsset, setSelectedAsset] = useState(null); const [selectedCategory, setSelectedCategory] = useState(AssetCategory.Visual); const account = useCurrentAccount(); const { data, isFetching, fetchNextPage, hasNextPage } = useGetOwnedObjects( @@ -49,6 +51,10 @@ export default function AssetsDashboardPage(): React.JSX.Element { } } + function onClickAsset(asset: IotaObjectData) { + setSelectedAsset(asset); + } + return ( @@ -67,10 +73,14 @@ export default function AssetsDashboardPage(): React.JSX.Element { <AssetList assets={assets} selectedCategory={selectedCategory} + onClick={onClickAsset} hasNextPage={hasNextPage} isFetchingNextPage={isFetching} fetchNextPage={fetchNextPage} /> + {selectedAsset && ( + <AssetDialog onClose={() => setSelectedAsset(null)} asset={selectedAsset} /> + )} </div> </Panel> ); diff --git a/apps/wallet-dashboard/components/AssetsList.tsx b/apps/wallet-dashboard/components/AssetsList.tsx index 0835f0a98e5..adb9e251581 100644 --- a/apps/wallet-dashboard/components/AssetsList.tsx +++ b/apps/wallet-dashboard/components/AssetsList.tsx @@ -15,6 +15,7 @@ interface AssetListProps { hasNextPage: boolean; isFetchingNextPage: boolean; fetchNextPage: () => void; + onClick: (asset: IotaObjectData) => void; } const ASSET_LAYOUT: Record<AssetCategory, string> = { @@ -29,6 +30,7 @@ export function AssetList({ hasNextPage, isFetchingNextPage, fetchNextPage, + onClick, }: AssetListProps): React.JSX.Element { const observerElem = useRef<HTMLDivElement | null>(null); const { isIntersecting } = useOnScreen(observerElem); @@ -43,7 +45,12 @@ export function AssetList({ return ( <div className={cl('max-h-[600px]', ASSET_LAYOUT[selectedCategory])}> {assets.map((asset) => ( - <AssetTileLink key={asset.digest} asset={asset} type={selectedCategory} /> + <AssetTileLink + key={asset.digest} + asset={asset} + type={selectedCategory} + onClick={onClick} + /> ))} <div ref={observerElem}> {isSpinnerVisible ? ( diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/AssetDialog.tsx b/apps/wallet-dashboard/components/Dialogs/Assets/AssetDialog.tsx new file mode 100644 index 00000000000..ef8e68072b6 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/AssetDialog.tsx @@ -0,0 +1,89 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; +import { Dialog } from '@iota/apps-ui-kit'; +import { FormikProvider, useFormik } from 'formik'; +import { useCurrentAccount } from '@iota/dapp-kit'; +import { createNftSendValidationSchema } from '@iota/core'; +import { DetailsView, SendView } from './views'; +import { IotaObjectData } from '@iota/iota-sdk/client'; +import { AssetsDialogView } from './constants'; +import { useCreateSendAssetTransaction, useNotifications } from '@/hooks'; +import { NotificationType } from '@/stores/notificationStore'; + +interface AssetsDialogProps { + onClose: () => void; + asset: IotaObjectData; +} + +interface FormValues { + to: string; +} + +const INITIAL_VALUES: FormValues = { + to: '', +}; + +export function AssetDialog({ onClose: onCloseCb, asset }: AssetsDialogProps): JSX.Element { + const [view, setView] = useState<AssetsDialogView>(AssetsDialogView.Details); + const account = useCurrentAccount(); + const activeAddress = account?.address ?? ''; + const objectId = asset?.objectId ?? ''; + const { addNotification } = useNotifications(); + const validationSchema = createNftSendValidationSchema(activeAddress, objectId); + + const { mutation: sendAsset } = useCreateSendAssetTransaction( + objectId, + onSendAsset, + onSendAsset, + ); + + const formik = useFormik<FormValues>({ + initialValues: INITIAL_VALUES, + validationSchema: validationSchema, + onSubmit: onSubmit, + validateOnChange: true, + }); + + function onSendAsset() { + setView(AssetsDialogView.Details); + onCloseCb(); + } + + async function onSubmit(values: FormValues) { + try { + await sendAsset.mutateAsync(values.to); + addNotification('Transfer transaction successful', NotificationType.Success); + } catch (error) { + console.log(error); + addNotification('Transfer transaction failed', NotificationType.Error); + } + } + + function onDetailsSend() { + setView(AssetsDialogView.Send); + } + + function onSendViewBack() { + setView(AssetsDialogView.Details); + } + function onClose() { + setView(AssetsDialogView.Details); + onCloseCb(); + } + return ( + <Dialog open onOpenChange={onClose}> + <FormikProvider value={formik}> + <> + {view === AssetsDialogView.Details && ( + <DetailsView asset={asset} onClose={onClose} onSend={onDetailsSend} /> + )} + {view === AssetsDialogView.Send && ( + <SendView asset={asset} onClose={onClose} onBack={onSendViewBack} /> + )} + </> + </FormikProvider> + </Dialog> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/constants/AssetsDialogView.ts b/apps/wallet-dashboard/components/Dialogs/Assets/constants/AssetsDialogView.ts new file mode 100644 index 00000000000..88cb34c15b1 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/constants/AssetsDialogView.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export enum AssetsDialogView { + Details = 'Details', + Send = 'Send', +} diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/constants/index.ts b/apps/wallet-dashboard/components/Dialogs/Assets/constants/index.ts new file mode 100644 index 00000000000..76b42558333 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/constants/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './AssetsDialogView'; diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/index.ts b/apps/wallet-dashboard/components/Dialogs/Assets/index.ts new file mode 100644 index 00000000000..3a6df1b0b55 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './AssetDialog'; +export * from './constants'; diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/views/DetailsView.tsx b/apps/wallet-dashboard/components/Dialogs/Assets/views/DetailsView.tsx new file mode 100644 index 00000000000..ceed115228f --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/views/DetailsView.tsx @@ -0,0 +1,183 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { ExplorerLinkType, useNftDetails, Collapsible } from '@iota/core'; +import { + Button, + ButtonType, + Header, + KeyValueInfo, + VisualAssetCard, + VisualAssetType, +} from '@iota/apps-ui-kit'; +import Link from 'next/link'; +import { formatAddress } from '@iota/iota-sdk/utils'; +import { Layout, LayoutBody, LayoutFooter } from '../../Staking/views/Layout'; +import { IotaObjectData } from '@iota/iota-sdk/client'; +import { ExplorerLink } from '@/components/ExplorerLink'; +import { useCurrentAccount } from '@iota/dapp-kit'; + +interface DetailsViewProps { + asset: IotaObjectData; + onClose: () => void; + onSend: () => void; +} + +export function DetailsView({ onClose, asset, onSend }: DetailsViewProps) { + const account = useCurrentAccount(); + + const senderAddress = account?.address ?? ''; + const objectId = asset.objectId; + + const { + nftName, + nftImageUrl, + nftMeta, + ownerAddress, + isTransferable, + metaKeys, + metaValues, + formatMetaValue, + isContainedInKiosk, + kioskItem, + } = useNftDetails(objectId, senderAddress); + + function handleMoreAboutKiosk() { + window.open('https://wiki.iota.org/', '_blank'); + } + + function handleMarketplace() { + window.open('https://wiki.iota.org/', '_blank'); + } + + return ( + <Layout> + <Header title="Asset" onClose={onClose} titleCentered /> + <LayoutBody> + <div className="flex w-full flex-col items-center justify-center gap-xs"> + <div className="w-[172px]"> + <VisualAssetCard + assetSrc={nftImageUrl} + assetTitle={nftName} + assetType={VisualAssetType.Image} + altText={nftName || 'NFT'} + isHoverable={false} + /> + </div> + <ExplorerLink type={ExplorerLinkType.Object} objectID={objectId}> + <Button type={ButtonType.Ghost} text="View on Explorer" /> + </ExplorerLink> + <div className="flex w-full flex-col gap-md"> + <div className="flex flex-col gap-xxxs"> + <span className="text-title-lg text-neutral-10 dark:text-neutral-92"> + {nftMeta?.name} + </span> + {nftMeta?.description ? ( + <span className="text-body-md text-neutral-60"> + {nftMeta?.description} + </span> + ) : null} + </div> + + {(nftMeta?.projectUrl || !!nftMeta?.creator) && ( + <div className="flex flex-col gap-xs"> + {nftMeta?.projectUrl && ( + <KeyValueInfo + keyText="Website" + value={ + <Link href={nftMeta?.projectUrl}> + {nftMeta?.projectUrl} + </Link> + } + fullwidth + /> + )} + {nftMeta?.creator && ( + <KeyValueInfo + keyText="Creator" + value={nftMeta?.creator ?? '-'} + fullwidth + /> + )} + </div> + )} + + <Collapsible defaultOpen title="Details"> + <div className="flex flex-col gap-xs px-md pb-xs pt-sm"> + {ownerAddress && ( + <KeyValueInfo + keyText="Owner" + value={ + <ExplorerLink + type={ExplorerLinkType.Address} + address={ownerAddress} + > + {formatAddress(ownerAddress)} + </ExplorerLink> + } + fullwidth + /> + )} + {objectId && ( + <KeyValueInfo + keyText="Object ID" + value={formatAddress(objectId)} + fullwidth + /> + )} + </div> + </Collapsible> + {metaKeys.length ? ( + <Collapsible defaultOpen title="Attributes"> + <div className="flex flex-col gap-xs px-md pb-xs pt-sm"> + {metaKeys.map((aKey, idx) => { + const { value, valueLink } = formatMetaValue( + metaValues[idx], + ); + return ( + <KeyValueInfo + key={idx} + keyText={aKey} + value={ + valueLink ? ( + <Link key={aKey} href={valueLink || ''}> + {value} + </Link> + ) : ( + value + ) + } + fullwidth + /> + ); + })} + </div> + </Collapsible> + ) : null} + </div> + </div> + </LayoutBody> + <LayoutFooter> + <div className="flex flex-col"> + {isContainedInKiosk && kioskItem?.isLocked ? ( + <div className="flex flex-col gap-2"> + <Button + type={ButtonType.Secondary} + onClick={handleMoreAboutKiosk} + text="Learn more about Kiosks" + /> + <Button + type={ButtonType.Primary} + onClick={handleMarketplace} + text="Marketplace" + /> + </div> + ) : ( + <Button disabled={!isTransferable} onClick={onSend} text="Send" fullWidth /> + )} + </div> + </LayoutFooter> + </Layout> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/views/SendView.tsx b/apps/wallet-dashboard/components/Dialogs/Assets/views/SendView.tsx new file mode 100644 index 00000000000..973d0407fd9 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/views/SendView.tsx @@ -0,0 +1,70 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { AddressInput, useNftDetails } from '@iota/core'; +import { useFormikContext } from 'formik'; +import { Layout, LayoutBody, LayoutFooter } from '../../Staking/views/Layout'; +import { + Button, + ButtonHtmlType, + Header, + VisualAssetCard, + VisualAssetType, + Title, +} from '@iota/apps-ui-kit'; +import { Loader } from '@iota/ui-icons'; +import { useCurrentAccount } from '@iota/dapp-kit'; +import { IotaObjectData } from '@iota/iota-sdk/client'; + +interface SendViewProps { + asset: IotaObjectData; + onClose: () => void; + onBack: () => void; +} + +export function SendView({ asset, onClose, onBack }: SendViewProps) { + const { isValid, dirty, isSubmitting, submitForm } = useFormikContext(); + + const account = useCurrentAccount(); + + const senderAddress = account?.address ?? ''; + const objectId = asset?.objectId || ''; + + const { nftName, nftImageUrl } = useNftDetails(objectId, senderAddress); + return ( + <Layout> + <Header title="Send asset" onClose={onClose} titleCentered onBack={onBack} /> + <LayoutBody> + <div className="flex w-full flex-col items-center justify-center gap-xs"> + <div className="w-[172px]"> + <VisualAssetCard + assetSrc={nftImageUrl} + assetTitle={nftName} + assetType={VisualAssetType.Image} + altText={nftName || 'NFT'} + isHoverable={false} + /> + </div> + <div className="flex w-full flex-col gap-md"> + <div className="flex flex-col items-center gap-xxxs"> + <Title title={nftName} /> + </div> + <AddressInput name="to" placeholder="Enter Address" /> + </div> + </div> + </LayoutBody> + <LayoutFooter> + <Button + fullWidth + htmlType={ButtonHtmlType.Submit} + disabled={!(isValid && dirty) || isSubmitting} + text="Send" + icon={isSubmitting ? <Loader className="animate-spin" /> : undefined} + iconAfterText + onClick={submitForm} + /> + </LayoutFooter> + </Layout> + ); +} diff --git a/apps/wallet-dashboard/components/Dialogs/Assets/views/index.ts b/apps/wallet-dashboard/components/Dialogs/Assets/views/index.ts new file mode 100644 index 00000000000..fcc3856cac3 --- /dev/null +++ b/apps/wallet-dashboard/components/Dialogs/Assets/views/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from './DetailsView'; +export * from './SendView'; diff --git a/apps/wallet-dashboard/components/tiles/AssetTileLink.tsx b/apps/wallet-dashboard/components/tiles/AssetTileLink.tsx index 3bf06075604..64c4d805513 100644 --- a/apps/wallet-dashboard/components/tiles/AssetTileLink.tsx +++ b/apps/wallet-dashboard/components/tiles/AssetTileLink.tsx @@ -3,50 +3,30 @@ 'use client'; -import { ASSETS_ROUTE } from '@/lib/constants/routes.constants'; import { AssetCategory } from '@/lib/enums'; import { VisibilityOff } from '@iota/ui-icons'; import { VisualAssetTile } from '.'; import { IotaObjectData } from '@iota/iota-sdk/client'; import { NonVisualAssetCard } from './NonVisualAssetTile'; -import { useExplorerLinkGetter } from '@/hooks'; -import Link from 'next/link'; -import { ExplorerLinkType } from '@iota/core'; interface AssetTileLinkProps { asset: IotaObjectData; type: AssetCategory; + onClick: (asset: IotaObjectData) => void; } -export function AssetTileLink({ asset, type }: AssetTileLinkProps): React.JSX.Element { - const getExplorerLink = useExplorerLinkGetter(); - const linkProps = getAssetLinkProps(asset); - - function getAssetLinkProps(asset: IotaObjectData): React.ComponentProps<typeof Link> { - if (type === AssetCategory.Visual) { - return { href: ASSETS_ROUTE.path + `/${asset.objectId}` }; - } else { - const explorerLink = - getExplorerLink({ - type: ExplorerLinkType.Object, - objectID: asset.objectId, - }) ?? ''; - - return { - href: explorerLink, - target: '_blank', - rel: 'noopener noreferrer', - }; - } +export function AssetTileLink({ asset, type, onClick }: AssetTileLinkProps): React.JSX.Element { + function handleClick() { + onClick(asset); } return ( - <Link {...linkProps}> + <> {type === AssetCategory.Visual ? ( - <VisualAssetTile asset={asset} icon={<VisibilityOff />} /> + <VisualAssetTile asset={asset} icon={<VisibilityOff />} onClick={handleClick} /> ) : ( - <NonVisualAssetCard asset={asset} /> + <NonVisualAssetCard asset={asset} onClick={handleClick} /> )} - </Link> + </> ); } diff --git a/apps/wallet-dashboard/providers/AppProviders.tsx b/apps/wallet-dashboard/providers/AppProviders.tsx index f6712288174..27744398f86 100644 --- a/apps/wallet-dashboard/providers/AppProviders.tsx +++ b/apps/wallet-dashboard/providers/AppProviders.tsx @@ -9,6 +9,7 @@ import { IotaClientProvider, lightTheme, darkTheme, WalletProvider } from '@iota import { getAllNetworks, getDefaultNetwork } from '@iota/iota-sdk/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; +import { KioskClientProvider } from '@iota/core'; import { growthbook } from '@/lib/utils'; import { Popup } from '@/components/Popup'; import { ThemeProvider } from '@iota/core'; @@ -24,26 +25,28 @@ export function AppProviders({ children }: React.PropsWithChildren) { <GrowthBookProvider growthbook={growthbook}> <QueryClientProvider client={queryClient}> <IotaClientProvider networks={allNetworks} defaultNetwork={defaultNetwork}> - <WalletProvider - autoConnect={true} - theme={[ - { - variables: lightTheme, - }, - { - selector: '.dark', - variables: darkTheme, - }, - ]} - > - <ThemeProvider appId="iota-dashboard"> - <PopupProvider> - {children} - <Toaster /> - <Popup /> - </PopupProvider> - </ThemeProvider> - </WalletProvider> + <KioskClientProvider> + <WalletProvider + autoConnect={true} + theme={[ + { + variables: lightTheme, + }, + { + selector: '.dark', + variables: darkTheme, + }, + ]} + > + <ThemeProvider appId="iota-dashboard"> + <PopupProvider> + {children} + <Toaster /> + <Popup /> + </PopupProvider> + </ThemeProvider> + </WalletProvider> + </KioskClientProvider> </IotaClientProvider> </QueryClientProvider> </GrowthBookProvider> diff --git a/apps/wallet/src/ui/app/helpers/formatAccountName.ts b/apps/wallet/src/ui/app/helpers/formatAccountName.ts index dff1c73ba80..c512a363841 100644 --- a/apps/wallet/src/ui/app/helpers/formatAccountName.ts +++ b/apps/wallet/src/ui/app/helpers/formatAccountName.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { formatAddress } from '@iota/iota-sdk/utils'; -import { truncateString } from './truncateString'; +import { truncateString } from '@iota/core'; export function formatAccountName( nickname: string | undefined | null, diff --git a/apps/wallet/src/ui/app/helpers/index.ts b/apps/wallet/src/ui/app/helpers/index.ts index aa14d5f7511..fde8b2c4f27 100644 --- a/apps/wallet/src/ui/app/helpers/index.ts +++ b/apps/wallet/src/ui/app/helpers/index.ts @@ -7,5 +7,4 @@ export { default as notEmpty } from './notEmptyCheck'; // export { getEventsSummary } from './getEventsSummary'; export { getAmount } from './getAmount'; export { checkStakingTxn } from './checkStakingTxn'; -export { truncateString } from './truncateString'; export { formatAccountName } from './formatAccountName'; diff --git a/apps/wallet/src/ui/app/hooks/index.ts b/apps/wallet/src/ui/app/hooks/index.ts index 33244c68262..f788f5dce44 100644 --- a/apps/wallet/src/ui/app/hooks/index.ts +++ b/apps/wallet/src/ui/app/hooks/index.ts @@ -6,15 +6,11 @@ export { default as useAppDispatch } from './useAppDispatch'; export { default as useAppSelector } from './useAppSelector'; export { default as useInitializedGuard } from './useInitializedGuard'; export { default as useFullscreenGuard } from './useFullscreenGuard'; -export { default as useMediaUrl } from './useMediaUrl'; export { default as useOnClickOutside } from './useOnClickOutside'; export { default as useOnKeyboardEvent } from './useOnKeyboardEvent'; -export { default as useFileExtensionType } from './useFileExtensionType'; -export { default as useNFTBasicData } from './useNFTBasicData'; export { useTransactionDryRun } from './useTransactionDryRun'; export { useGetTxnRecipientAddress } from './useGetTxnRecipientAddress'; export { useGetTransferAmount } from './useGetTransferAmount'; -export { useOwnedNFT } from './useOwnedNFT'; export { useCopyToClipboard } from './useCopyToClipboard'; export * from './useExplorerLink'; diff --git a/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroup.tsx b/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroup.tsx index b6cd64ae3b6..c1a63ae97cd 100644 --- a/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroup.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/manage/AccountGroup.tsx @@ -14,9 +14,8 @@ import { Button, ButtonSize, ButtonType, Dropdown, ListItem } from '@iota/apps-u import { Add, MoreHoriz, TriangleDown } from '@iota/ui-icons'; import { OutsideClickHandler } from '_components/OutsideClickHandler'; import { AccountGroupItem } from '_pages/accounts/manage/AccountGroupItem'; -import { Collapsible } from '_app/shared/collapse'; import { useFeature } from '@growthbook/growthbook-react'; -import { Feature } from '@iota/core'; +import { Feature, Collapsible } from '@iota/core'; import { useActiveAccount } from '_app/hooks/useActiveAccount'; const ACCOUNT_TYPE_TO_LABEL: Record<AccountType, string> = { 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 2460016d0fb..31351c29c2e 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 @@ -3,9 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 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'; +import { type GasSummaryType, Collapsible } from '@iota/core'; interface GasFeesProps { sender?: string; diff --git a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/Command.tsx b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/Command.tsx index 72075262870..f3106a47831 100644 --- a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/Command.tsx +++ b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/Command.tsx @@ -5,7 +5,7 @@ import { TypeTagSerializer, type TypeTag } from '@iota/iota-sdk/bcs'; import { type TransactionArgument, type Commands } from '@iota/iota-sdk/transactions/'; import { formatAddress, normalizeIotaAddress, toB64 } from '@iota/iota-sdk/utils'; -import { Collapsible } from '_src/ui/app/shared/collapse'; +import { Collapsible } from '@iota/core'; import { TitleSize } from '@iota/apps-ui-kit'; type TransactionType = ReturnType<(typeof Commands)[keyof typeof Commands]>; diff --git a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/index.tsx b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/index.tsx index 368174dc241..518a0929061 100644 --- a/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/index.tsx +++ b/apps/wallet/src/ui/app/pages/approval-request/transaction-request/TransactionDetails/index.tsx @@ -6,7 +6,7 @@ import { useTransactionData } from '_src/ui/app/hooks'; import { type Transaction } from '@iota/iota-sdk/transactions'; import { Command } from './Command'; import { Input } from './Input'; -import { Collapsible } from '_src/ui/app/shared/collapse'; +import { Collapsible } from '@iota/core'; import { ButtonSegment, ButtonSegmentType, diff --git a/apps/wallet/src/ui/app/pages/home/kiosk-details/index.tsx b/apps/wallet/src/ui/app/pages/home/kiosk-details/index.tsx index 1d3d10dc9fe..826dc5e2317 100644 --- a/apps/wallet/src/ui/app/pages/home/kiosk-details/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/kiosk-details/index.tsx @@ -12,8 +12,7 @@ import { PageTemplate, } from '_components'; import { useUnlockedGuard } from '_src/ui/app/hooks/useUnlockedGuard'; -import { Collapsible } from '_src/ui/app/shared/collapse'; -import { useGetKioskContents } from '@iota/core'; +import { useGetKioskContents, Collapsible } from '@iota/core'; import { formatAddress } from '@iota/iota-sdk/utils'; import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import cl from 'clsx'; diff --git a/apps/wallet/src/ui/app/pages/home/nft-details/index.tsx b/apps/wallet/src/ui/app/pages/home/nft-details/index.tsx index 3879f492cea..5302a358912 100644 --- a/apps/wallet/src/ui/app/pages/home/nft-details/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/nft-details/index.tsx @@ -3,61 +3,38 @@ // SPDX-License-Identifier: Apache-2.0 import { useActiveAddress } from '_app/hooks/useActiveAddress'; -import { Collapsible } from '_app/shared/collapse'; import { ExplorerLink, ExplorerLinkType, Loading, NFTDisplayCard, PageTemplate } from '_components'; -import { useNFTBasicData, useOwnedNFT } from '_hooks'; import { useUnlockedGuard } from '_src/ui/app/hooks/useUnlockedGuard'; -import { useIsAssetTransferable, useGetKioskContents, useGetNFTMeta } from '@iota/core'; +import { useNFTBasicData, useNftDetails, Collapsible, useIsAssetTransferable } from '@iota/core'; import { formatAddress } from '@iota/iota-sdk/utils'; import cl from 'clsx'; import { Link, Navigate, useNavigate, useSearchParams } from 'react-router-dom'; import { Button, ButtonType, KeyValueInfo } from '@iota/apps-ui-kit'; -import { truncateString } from '_src/ui/app/helpers'; - -type NftFields = { - metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } }; -}; function NFTDetailsPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const nftId = searchParams.get('objectId'); const accountAddress = useActiveAddress(); - const { data: objectData, isPending: isNftLoading } = useOwnedNFT(nftId || '', accountAddress); + const { + nftMeta, + isPendingMeta, + isNftLoading, + ownerAddress, + objectData, + metaKeys, + metaValues, + formatMetaValue, + isContainedInKiosk, + kioskItem, + } = useNftDetails(nftId || '', accountAddress); const { data: isAssetTransferable, isLoading: isCheckingAssetTransferability } = useIsAssetTransferable(objectData); - const { nftFields, fileExtensionType, filePath } = useNFTBasicData(objectData); - const address = useActiveAddress(); - const { data } = useGetKioskContents(address); - - const isContainedInKiosk = data?.lookup.get(nftId!); - const kioskItem = data?.list.find((k) => k.data?.objectId === nftId); + const { fileExtensionType, filePath } = useNFTBasicData(objectData); - // Extract either the attributes, or use the top-level NFT fields: - const metaFields = - (nftFields as NftFields)?.metadata?.fields?.attributes?.fields || - Object.entries(nftFields ?? {}) - .filter(([key]) => key !== 'id') - .reduce( - (acc, [key, value]) => { - acc.keys.push(key); - acc.values.push(value as string); - return acc; - }, - { keys: [] as string[], values: [] as string[] }, - ); - const metaKeys: string[] = metaFields ? metaFields.keys : []; - const metaValues = metaFields ? metaFields.values : []; - const { data: nftDisplayData, isPending: isPendingDisplay } = useGetNFTMeta(nftId || ''); - const ownerAddress = - (objectData?.owner && - typeof objectData?.owner === 'object' && - 'AddressOwner' in objectData.owner && - objectData.owner.AddressOwner) || - ''; const isGuardLoading = useUnlockedGuard(); const isPending = - isNftLoading || isPendingDisplay || isGuardLoading || isCheckingAssetTransferability; + isNftLoading || isPendingMeta || isGuardLoading || isCheckingAssetTransferability; function handleMoreAboutKiosk() { window.open('https://docs.iota.org/references/ts-sdk/kiosk/', '_blank'); @@ -72,28 +49,6 @@ function NFTDetailsPage() { navigate(`/nft-transfer/${nftId}`); } - function formatMetaValue(value: string | object) { - if (typeof value === 'object') { - return { - value: JSON.stringify(value), - valueLink: undefined, - }; - } else { - if (value.includes('http')) { - return { - value: value.startsWith('http') - ? truncateString(value, 20, 8) - : formatAddress(value), - valueLink: value, - }; - } - return { - value: value, - valueLink: undefined, - }; - } - } - return ( <PageTemplate title="Visual Asset" @@ -128,39 +83,36 @@ function NFTDetailsPage() { <div className="flex flex-col gap-md"> <div className="flex flex-col gap-xxxs"> <span className="text-title-lg text-neutral-10 dark:text-neutral-92"> - {nftDisplayData?.name} + {nftMeta?.name} </span> - {nftDisplayData?.description ? ( + {nftMeta?.description ? ( <span className="text-body-md text-neutral-60"> - {nftDisplayData?.description} + {nftMeta?.description} </span> ) : null} </div> - {nftDisplayData?.projectUrl || - (nftDisplayData?.creator && ( - <div className="flex flex-col gap-xs"> - {nftDisplayData?.projectUrl && ( - <KeyValueInfo - keyText="Website" - value={ - <Link - to={nftDisplayData?.projectUrl} - > - {nftDisplayData?.projectUrl} - </Link> - } - fullwidth - /> - )} - {nftDisplayData?.creator && ( - <KeyValueInfo - keyText="Creator" - value={nftDisplayData?.creator ?? '-'} - fullwidth - /> - )} - </div> - ))} + {(nftMeta?.projectUrl || nftMeta?.creator) && ( + <div className="flex flex-col gap-xs"> + {nftMeta?.projectUrl && ( + <KeyValueInfo + keyText="Website" + value={ + <Link to={nftMeta?.projectUrl}> + {nftMeta?.projectUrl} + </Link> + } + fullwidth + /> + )} + {nftMeta?.creator && ( + <KeyValueInfo + keyText="Creator" + value={nftMeta?.creator ?? '-'} + fullwidth + /> + )} + </div> + )} </div> <div className="flex flex-col gap-md"> <Collapsible defaultOpen title="Details"> diff --git a/apps/wallet/src/ui/app/pages/home/nft-transfer/index.tsx b/apps/wallet/src/ui/app/pages/home/nft-transfer/index.tsx index bae076ae8a2..8d4c920ec38 100644 --- a/apps/wallet/src/ui/app/pages/home/nft-transfer/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/nft-transfer/index.tsx @@ -4,11 +4,10 @@ import { useActiveAddress } from '_app/hooks/useActiveAddress'; import { Loading, NFTDisplayCard, Overlay } from '_components'; -import { useOwnedNFT } from '_hooks'; import { useUnlockedGuard } from '_src/ui/app/hooks/useUnlockedGuard'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { TransferNFTForm } from './TransferNFTForm'; -import { useIsAssetTransferable } from '@iota/core'; +import { useOwnedNFT, useIsAssetTransferable } from '@iota/core'; function NftTransferPage() { const { nftId } = useParams(); diff --git a/apps/wallet/src/ui/app/pages/home/tokens/TokenList.tsx b/apps/wallet/src/ui/app/pages/home/tokens/TokenList.tsx index 7952e1c8db2..d2b1a8dba3b 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/TokenList.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/TokenList.tsx @@ -2,7 +2,7 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { Collapsible } from '_src/ui/app/shared/collapse'; +import { Collapsible } from '@iota/core'; import { type ReactNode } from 'react'; type TokenListProps = { 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 86febdbe570..0f1e92eddfc 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 @@ -8,12 +8,12 @@ import { type ObjectChangeSummary, type IotaObjectChangeTypes, type IotaObjectChangeWithDisplay, + Collapsible, } from '@iota/core'; import { formatAddress } from '@iota/iota-sdk/utils'; import cx from 'clsx'; import { ExpandableList } from '../../ExpandableList'; import { ObjectChangeDisplay } from './objectSummary/ObjectChangeDisplay'; -import { Collapsible } from '../../collapse'; import { Badge, BadgeType,