-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(wallet-dashboard): style selected visual Assets. #4085
base: develop
Are you sure you want to change the base?
Changes from 10 commits
4074f2b
9bb5dd0
1f2eb82
f9530b1
934cd96
d055db0
2a2daeb
6304492
c22a756
124ddec
fc404d8
7bd0c26
61a13f8
ed3e381
336468e
4ce390b
16110bb
0bd5c50
84b2dba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export * from './Collapsible'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import { useGetNFTMeta, useOwnedNFT, useNFTBasicData, useGetKioskContents } from './'; | ||
import { formatAddress } from '@iota/iota-sdk/utils'; | ||
import { isAssetTransferable, truncateString } from '../utils'; | ||
|
||
type NftFields = { | ||
metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } }; | ||
}; | ||
|
||
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 isTransferable = isAssetTransferable(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: [] as string[], values: [] as string[] }, | ||
); | ||
|
||
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, | ||
}; | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name should be singular, since it refers to a specific Asset, not multiple. Maybe even |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import React 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 './hooks'; | ||
import { useCreateSendAssetTransaction, useNotifications } from '@/hooks'; | ||
import { NotificationType } from '@/stores/notificationStore'; | ||
import { ASSETS_ROUTE } from '@/lib/constants/routes.constants'; | ||
import { useRouter } from 'next/navigation'; | ||
|
||
interface AssetsDialogProps { | ||
isOpen: boolean; | ||
handleClose: () => void; | ||
asset: IotaObjectData | null; | ||
view: AssetsDialogView; | ||
setView: (view: AssetsDialogView | undefined) => void; | ||
} | ||
|
||
export interface FormValues { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this interface needs too be exported I would say to rename it to something more specific |
||
to: string; | ||
} | ||
|
||
const INITIAL_VALUES: FormValues = { | ||
to: '', | ||
}; | ||
|
||
export function AssetsDialog({ | ||
isOpen, | ||
handleClose, | ||
asset, | ||
setView, | ||
view, | ||
}: AssetsDialogProps): JSX.Element { | ||
const account = useCurrentAccount(); | ||
const activeAddress = account?.address ?? ''; | ||
const objectId = asset?.objectId ?? ''; | ||
const router = useRouter(); | ||
const { addNotification } = useNotifications(); | ||
const validationSchema = createNftSendValidationSchema(activeAddress, objectId); | ||
|
||
function handleDetailsSend() { | ||
setView(AssetsDialogView.Send); | ||
} | ||
|
||
const { mutation: sendAsset } = useCreateSendAssetTransaction( | ||
objectId, | ||
onSendAssetSuccess, | ||
onSendAssetError, | ||
); | ||
|
||
const formik = useFormik<FormValues>({ | ||
initialValues: INITIAL_VALUES, | ||
validationSchema: validationSchema, | ||
onSubmit: onSubmit, | ||
validateOnChange: true, | ||
}); | ||
|
||
function onSendAssetSuccess() { | ||
addNotification('Transfer transaction successful', NotificationType.Success); | ||
router.push(ASSETS_ROUTE.path + '/assets'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not necessary, it's the same view, isn't it? |
||
} | ||
|
||
function onSendAssetError() { | ||
addNotification('Transfer transaction failed', NotificationType.Error); | ||
} | ||
|
||
async function onSubmit(values: FormValues) { | ||
try { | ||
await sendAsset.mutateAsync(values.to); | ||
} catch (error) { | ||
addNotification('Transfer transaction failed', NotificationType.Error); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After sending, first the dialog should be closed and then shown a notification, no? |
||
|
||
function onSendClose() { | ||
setView(undefined); | ||
} | ||
|
||
function onSendBack() { | ||
setView(AssetsDialogView.Details); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel it's a bit weird that the functions are defined by their view, rather than what they do, like, the function |
||
|
||
return ( | ||
<Dialog open={isOpen} onOpenChange={() => handleClose()}> | ||
<FormikProvider value={formik}> | ||
<> | ||
{view === AssetsDialogView.Details && asset && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This way asset will always be defined |
||
<DetailsView | ||
asset={asset} | ||
handleClose={handleClose} | ||
handleSend={handleDetailsSend} | ||
/> | ||
)} | ||
{view === AssetsDialogView.Send && asset && ( | ||
<SendView asset={asset} onClose={onSendClose} onBack={onSendBack} /> | ||
)} | ||
</> | ||
</FormikProvider> | ||
</Dialog> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export * from './useAssetsDialog'; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a hook if its a simple useState and is only used in 1 place? Seems unnecessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it has logic before, but it was moved. I'll remove hook for now. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import React from 'react'; | ||
|
||
export enum AssetsDialogView { | ||
Details = 'Details', | ||
Send = 'Send', | ||
} | ||
|
||
export function useAssetsDialog() { | ||
const [view, setView] = React.useState<AssetsDialogView | undefined>(AssetsDialogView.Details); | ||
|
||
return { | ||
view, | ||
setView, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
// Copyright (c) 2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export * from './AssetsDialog'; | ||
export * from './hooks'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Line 8 and this one could share a type