-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement AssetTransfer module component + tests & stories
- Loading branch information
1 parent
1af7dba
commit 4f41a71
Showing
5 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
src/modules/components/asset/assetTransfer/assetTransfer.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { AssetTransfer } from './assetTransfer'; | ||
|
||
const meta: Meta<typeof AssetTransfer> = { | ||
title: 'Modules/Components/Asset/AssetTransfer', | ||
component: AssetTransfer, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/P0GeJKqILL7UXvaqu5Jj7V/Aragon-ODS?type=design&node-id=14385%3A24287&mode=dev&t=IX3Fa96hiwUEtcoA-1', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof AssetTransfer>; | ||
|
||
export const Default: Story = {}; | ||
|
||
export const Loaded: Story = { | ||
args: { | ||
senderAddress: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', | ||
recipientAddress: '0x1D03D98c0aac1f83860cec5156116FE68725642E', | ||
senderEnsName: 'vitalik.eth', | ||
tokenIconSrc: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png?1696501628', | ||
tokenSymbol: 'ETH', | ||
tokenAmount: 1, | ||
tokenName: 'Ethereum', | ||
hash: '0xf006e9454ad77c5e8e6f54106c6939d3d8b68ae16fc216d67c752f54adb21fc6', | ||
tokenPrice: 3850, | ||
chainId: 1, | ||
}, | ||
render: (props) => <AssetTransfer {...props} />, | ||
}; | ||
|
||
export default meta; |
97 changes: 97 additions & 0 deletions
97
src/modules/components/asset/assetTransfer/assetTransfer.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { NumberFormat, formatterUtils } from '../../../../core'; | ||
import { OdsModulesProvider } from '../../odsModulesProvider'; | ||
import { AssetTransfer, type IAssetTransferProps } from './assetTransfer'; | ||
|
||
jest.mock('../../member/memberAvatar', () => ({ MemberAvatar: () => <div data-testid="member-avatar-mock" /> })); | ||
|
||
describe('<AssetTransfer /> component', () => { | ||
const createTestComponent = (props?: Partial<IAssetTransferProps>) => { | ||
const minimumProps: IAssetTransferProps = { | ||
recipientAddress: '0x1D03D98c0aac1f83860cec5156116FE68725642E', | ||
senderAddress: '0x1D03D98c0aac1f83860cec5156116FE687259999', | ||
tokenIconSrc: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png?1696501628', | ||
tokenSymbol: 'ETH', | ||
tokenAmount: 1, | ||
tokenName: 'Ethereum', | ||
hash: '0xf006e9454ad77c5e8e6f54106c6939d3d8b68ae16fc216d67c752f54adb21fc6', | ||
tokenPrice: 3850, | ||
chainId: 1, | ||
...props, | ||
}; | ||
|
||
return ( | ||
<OdsModulesProvider> | ||
<AssetTransfer {...minimumProps} /> | ||
</OdsModulesProvider> | ||
); | ||
}; | ||
|
||
it('renders with minimum props', () => { | ||
const tokenName = 'Bitcoin'; | ||
render(createTestComponent({ tokenName })); | ||
|
||
expect(screen.getByText('Bitcoin')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders correctly with optional props, preferring ENS over address when available', () => { | ||
const senderEnsName = 'sender.eth'; | ||
const recipientEnsName = 'recipient.eth'; | ||
render(createTestComponent({ senderEnsName, recipientEnsName })); | ||
|
||
expect(screen.getByText('sender.eth')).toBeInTheDocument(); | ||
expect(screen.getByText('recipient.eth')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the formatted fiat estimate', () => { | ||
const tokenPrice = 100; | ||
const tokenAmount = 10; | ||
|
||
const formattedEstimate = formatterUtils.formatNumber(tokenPrice * tokenAmount, { | ||
format: NumberFormat.FIAT_TOTAL_SHORT, | ||
}); | ||
render(createTestComponent({ tokenPrice, tokenAmount })); | ||
const formattedUsdEstimate = screen.getByText(formattedEstimate as string); | ||
expect(formattedUsdEstimate).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the token value and symbol with sign', () => { | ||
const tokenSymbol = 'ETH'; | ||
const tokenAmount = 10; | ||
|
||
render(createTestComponent({ tokenSymbol, tokenAmount })); | ||
const tokenPrintout = screen.getByText('+10 ETH'); | ||
expect(tokenPrintout).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders sender and recipient addresses when ENS names are not provided', () => { | ||
render(createTestComponent()); | ||
|
||
expect(screen.getByText('0x1D…642E')).toBeInTheDocument(); | ||
expect(screen.getByText('0x1D…9999')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders both avatar elements for the from and to addresses', () => { | ||
render(createTestComponent()); | ||
|
||
expect(screen.getAllByTestId('member-avatar-mock')).toHaveLength(2); | ||
}); | ||
|
||
it('configures and applies the correct links for sender, recipient, transaction', () => { | ||
const senderAddress = '0x415c8893D514F9BC5211d36eEDA4183226b84AA7'; | ||
const recipientAddress = '0xFf00000000000000000000000000000000081457'; | ||
const hash = '0x0ca620e2dd3147658b8a042b3e7b7cd6f5fa043bf3625140c0dbddcabf47dfb9'; | ||
|
||
render(createTestComponent({ senderAddress, recipientAddress, hash })); | ||
|
||
const links = screen.getAllByRole('link'); | ||
|
||
const expectedSenderLink = `https://etherscan.io/address/${senderAddress}`; | ||
const expectedRecipientLink = `https://etherscan.io/address/${recipientAddress}`; | ||
const expectedTransactionLink = `https://etherscan.io/tx/${hash}`; | ||
|
||
expect(links[0]).toHaveAttribute('href', expectedSenderLink); | ||
expect(links[1]).toHaveAttribute('href', expectedRecipientLink); | ||
expect(links[2]).toHaveAttribute('href', expectedTransactionLink); | ||
}); | ||
}); |
187 changes: 187 additions & 0 deletions
187
src/modules/components/asset/assetTransfer/assetTransfer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import classNames from 'classnames'; | ||
import { type Hash } from 'viem'; | ||
import { useConfig } from 'wagmi'; | ||
import { Avatar, AvatarIcon, Icon, IconType, NumberFormat, formatterUtils } from '../../../../core'; | ||
import { type IWeb3ComponentProps } from '../../../types'; | ||
import { addressUtils } from '../../../utils'; | ||
import { MemberAvatar } from '../../member'; | ||
|
||
export interface IAssetTransferProps extends IWeb3ComponentProps { | ||
/** | ||
* Address of the transaction sender. | ||
*/ | ||
senderAddress: Hash; | ||
/** | ||
* Address of the transaction recipient. | ||
*/ | ||
recipientAddress: Hash; | ||
/** | ||
* ENS name of the transaction sender. | ||
*/ | ||
senderEnsName?: string; | ||
/** | ||
* ENS name of the transaction recipient. | ||
*/ | ||
recipientEnsName?: string; | ||
/** | ||
* Name of the token transferred. | ||
*/ | ||
tokenName: string; | ||
/** | ||
* Icon URL of the tranferred token. | ||
*/ | ||
tokenIconSrc?: string; | ||
/** | ||
* Amount of tokens transferred. | ||
*/ | ||
tokenAmount: number; | ||
/** | ||
* Symbol of the token transferred. Example: ETH, DAI, etc. | ||
*/ | ||
tokenSymbol: string; | ||
/** | ||
* Price per token in fiat. | ||
*/ | ||
tokenPrice: number | string; | ||
/** | ||
* Transaction hash. | ||
*/ | ||
hash: string; | ||
/** | ||
* Chain ID of the transaction. | ||
*/ | ||
chainId: number; | ||
} | ||
|
||
export const AssetTransfer: React.FC<IAssetTransferProps> = (props) => { | ||
const { | ||
senderAddress, | ||
recipientAddress, | ||
senderEnsName, | ||
recipientEnsName, | ||
tokenName, | ||
tokenIconSrc, | ||
tokenAmount, | ||
tokenSymbol, | ||
tokenPrice, | ||
chainId, | ||
hash, | ||
wagmiConfig: wagmiConfigProps, | ||
} = props; | ||
const wagmiConfigProvider = useConfig(); | ||
|
||
const wagmiConfig = wagmiConfigProps ?? wagmiConfigProvider; | ||
|
||
const processedChainId = chainId ?? wagmiConfig.chains[0].id; | ||
|
||
const currentChain = wagmiConfig.chains.find(({ id }) => id === processedChainId); | ||
const blockExplorerUrl = currentChain?.blockExplorers?.default.url; | ||
|
||
const blockExplorerAssembledHref = blockExplorerUrl && hash ? `${blockExplorerUrl}/tx/${hash}` : undefined; | ||
|
||
const resolvedSenderHandle = | ||
senderEnsName != null && senderEnsName !== '' ? senderEnsName : addressUtils.truncateAddress(senderAddress); | ||
|
||
const resolvedRecipientHandle = | ||
recipientEnsName != null && recipientEnsName !== '' | ||
? recipientEnsName | ||
: addressUtils.truncateAddress(recipientAddress); | ||
|
||
const resolvedSenderLink = | ||
blockExplorerUrl && senderAddress ? `${blockExplorerUrl}/address/${senderAddress}` : undefined; | ||
const resolvedRecipientLink = | ||
blockExplorerUrl && recipientAddress ? `${blockExplorerUrl}/address/${recipientAddress}` : undefined; | ||
|
||
const formattedTokenValue = formatterUtils.formatNumber(tokenAmount && tokenAmount > 0 ? tokenAmount : null, { | ||
format: NumberFormat.TOKEN_AMOUNT_SHORT, | ||
withSign: true, | ||
}); | ||
const fiatValue = Number(tokenAmount ?? 0) * Number(tokenPrice ?? 0); | ||
const formattedFiatValue = formatterUtils.formatNumber(fiatValue, { | ||
format: NumberFormat.FIAT_TOTAL_SHORT, | ||
}); | ||
const formattedTokenAmount = formattedTokenValue && tokenSymbol ? `${formattedTokenValue} ${tokenSymbol}` : `-`; | ||
|
||
return ( | ||
<div className="flex h-full w-[320px] flex-col gap-y-2 md:w-[640px] md:gap-y-3"> | ||
<div className="relative flex h-full flex-col rounded-xl border-[1px] border-neutral-100 md:flex-row"> | ||
<a | ||
href={resolvedSenderLink} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className={classNames( | ||
'flex h-20 w-full items-center space-x-4 rounded-l-xl px-4 py-7', //base | ||
'hover:border-neutral-200 hover:shadow-neutral-md', //hover | ||
'focus:outline-none focus-visible:rounded-l-xl focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', //focus | ||
'active:border-[1px] active:border-neutral-300', //active | ||
'md:w-1/2 md:p-6', //responsive | ||
)} | ||
> | ||
<MemberAvatar responsiveSize={{ md: 'md' }} ensName={senderEnsName} address={senderAddress} /> | ||
<div className="flex flex-col"> | ||
<p className="text-xs font-normal leading-tight text-neutral-500 md:text-sm">From</p> | ||
<div className="flex items-center space-x-1"> | ||
<p className="text-sm font-normal leading-tight text-neutral-800 md:text-base"> | ||
{resolvedSenderHandle} | ||
</p> | ||
<Icon icon={IconType.LINK_EXTERNAL} size="sm" className="text-neutral-300" /> | ||
</div> | ||
</div> | ||
</a> | ||
<div className="border-t-[1px] border-neutral-100 md:border-l-[1px]" /> | ||
<AvatarIcon | ||
icon={IconType.CHEVRON_DOWN} | ||
size="sm" | ||
className={classNames( | ||
'absolute left-4 top-1/2 -translate-y-1/2 bg-neutral-50 text-neutral-300', //base | ||
'md:left-1/2 md:-translate-x-1/2 md:-rotate-90', //responsive | ||
)} | ||
/> | ||
<a | ||
href={resolvedRecipientLink} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className={classNames( | ||
'flex h-20 w-full items-center space-x-4 rounded-r-xl px-4 py-7', //base | ||
'hover:border-neutral-200 hover:shadow-neutral-md', //hover | ||
'focus:outline-none focus-visible:rounded-r-xl focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', //focus | ||
'active:border-[1px] active:border-neutral-300', //active | ||
'md:w-1/2 md:p-6 md:pl-8', //responsive | ||
)} | ||
> | ||
<MemberAvatar responsiveSize={{ md: 'md' }} ensName={recipientEnsName} address={recipientAddress} /> | ||
<div className="flex flex-col"> | ||
<p className="text-xs font-normal leading-tight text-neutral-500 md:text-sm">To</p> | ||
<div className="flex items-center space-x-1"> | ||
<p className="text-sm font-normal leading-tight text-neutral-800 md:text-base"> | ||
{resolvedRecipientHandle} | ||
</p> | ||
<Icon icon={IconType.LINK_EXTERNAL} size="sm" className="text-neutral-300" /> | ||
</div> | ||
</div> | ||
</a> | ||
</div> | ||
<a | ||
href={blockExplorerAssembledHref} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className={classNames( | ||
'flex h-16 w-full items-center justify-between rounded-xl border-[1px] border-neutral-100 px-4', | ||
'hover:border-neutral-200 hover:shadow-neutral-md', | ||
'focus:outline-none focus-visible:rounded-xl focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', | ||
'active:border-neutral-300', | ||
'md:h-20 md:px-6', | ||
)} | ||
> | ||
<div className="flex items-center space-x-3 md:space-x-4"> | ||
<Avatar responsiveSize={{ md: 'md' }} src={tokenIconSrc} /> | ||
<p className="text-sm leading-tight text-neutral-800 md:text-base">{tokenName}</p> | ||
</div> | ||
<div className="flex flex-col items-end justify-end"> | ||
<p className="text-sm leading-tight text-neutral-800 md:text-base">{formattedTokenAmount}</p> | ||
<p className="text-sm leading-tight text-neutral-500 md:text-base">{formattedFiatValue}</p> | ||
</div> | ||
</a> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './assetTransfer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './assetDataListItem'; | ||
export * from './assetTransfer'; |