-
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: APP-2977 - Implement AssetTransfer module component (#134)
- Loading branch information
1 parent
f291676
commit 9697374
Showing
12 changed files
with
410 additions
and
4 deletions.
There are no files selected for viewing
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
52 changes: 52 additions & 0 deletions
52
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,52 @@ | ||
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>; | ||
|
||
/** | ||
* Default usage example of the AssetTransfer component. | ||
*/ | ||
export const Default: Story = { | ||
args: { | ||
sender: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', name: 'vitalik.eth' }, | ||
recipient: { | ||
address: '0x168dAa4529bf88369ac8c1ABA5A2ad8CF2A61Fb9', | ||
name: 'decentralizedtransactions.eth', | ||
}, | ||
assetIconSrc: 'https://assets.coingecko.com/coins/images/279/large/ethereum.png?1696501628', | ||
assetSymbol: 'ETH', | ||
assetAmount: 1, | ||
assetName: 'Ethereum', | ||
hash: '0xf006e9454ad77c5e8e6f54106c6939d3d8b68ae16fc216d67c752f54adb21fc6', | ||
assetFiatPrice: 3850, | ||
chainId: 1, | ||
}, | ||
}; | ||
|
||
/** | ||
* Fallback usage example of the AssetTransfer component with only required props. | ||
*/ | ||
export const Fallback: Story = { | ||
args: { | ||
sender: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' }, | ||
recipient: { address: '0x168dAa4529bf88369ac8c1ABA5A2ad8CF2A61Fb9' }, | ||
assetName: 'Ethereum', | ||
assetSymbol: 'ETH', | ||
assetAmount: 1, | ||
hash: '0xf006e9454ad77c5e8e6f54106c6939d3d8b68ae16fc216d67c752f54adb21fc6', | ||
}, | ||
}; | ||
|
||
export default meta; |
68 changes: 68 additions & 0 deletions
68
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,68 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { OdsModulesProvider } from '../../odsModulesProvider'; | ||
import { AssetTransfer, type IAssetTransferProps } from './assetTransfer'; | ||
|
||
jest.mock('./assetTransferAddress', () => ({ | ||
AssetTransferAddress: () => <div data-testid="asset-transfer-address" />, | ||
})); | ||
|
||
describe('<AssetTransfer /> component', () => { | ||
const createTestComponent = (props?: Partial<IAssetTransferProps>) => { | ||
const completeProps: IAssetTransferProps = { | ||
sender: { address: '0x1D03D98c0aac1f83860cec5156116FE68725642E' }, | ||
recipient: { address: '0x1D03D98c0aac1f83860cec5156116FE687259999' }, | ||
assetSymbol: 'ETH', | ||
assetAmount: 1, | ||
assetName: 'Ethereum', | ||
hash: '0xf006e9454ad77c5e8e6f54106c6939d3d8b68ae16fc216d67c752f54adb21fc6', | ||
...props, | ||
}; | ||
|
||
return ( | ||
<OdsModulesProvider> | ||
<AssetTransfer {...completeProps} /> | ||
</OdsModulesProvider> | ||
); | ||
}; | ||
|
||
it('renders with minimum props', () => { | ||
const assetName = 'Bitcoin'; | ||
render(createTestComponent({ assetName })); | ||
|
||
expect(screen.getByText('Bitcoin')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the formatted fiat estimate', () => { | ||
const assetFiatPrice = 100; | ||
const assetAmount = 10; | ||
|
||
render(createTestComponent({ assetFiatPrice, assetAmount })); | ||
const formattedUsdEstimate = screen.getByText('$1.00K'); | ||
expect(formattedUsdEstimate).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the asset value and symbol with sign', () => { | ||
const assetSymbol = 'ETH'; | ||
const assetAmount = 10; | ||
|
||
render(createTestComponent({ assetSymbol, assetAmount })); | ||
const assetPrintout = screen.getByText('+10 ETH'); | ||
expect(assetPrintout).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders both avatar elements for the from and to addresses', () => { | ||
render(createTestComponent()); | ||
|
||
expect(screen.getAllByTestId('asset-transfer-address')).toHaveLength(2); | ||
}); | ||
|
||
it('configures and applies the correct link for transfer tx', () => { | ||
const hash = '0x0ca620e2dd3147658b8a042b3e7b7cd6f5fa043bf3625140c0dbddcabf47dfb9'; | ||
render(createTestComponent({ hash })); | ||
|
||
const links = screen.getByRole('link'); | ||
const expectedTransactionLink = `https://etherscan.io/tx/${hash}`; | ||
|
||
expect(links).toHaveAttribute('href', expectedTransactionLink); | ||
}); | ||
}); |
123 changes: 123 additions & 0 deletions
123
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,123 @@ | ||
import classNames from 'classnames'; | ||
import { useConfig } from 'wagmi'; | ||
import { Avatar, AvatarIcon, IconType, NumberFormat, formatterUtils } from '../../../../core'; | ||
import { type ICompositeAddress, type IWeb3ComponentProps } from '../../../types'; | ||
import { AssetTransferAddress } from './assetTransferAddress'; | ||
|
||
export interface IAssetTransferProps extends IWeb3ComponentProps { | ||
/** | ||
* Address (& optional ENS Name) of the transaction sender. | ||
*/ | ||
sender: ICompositeAddress; | ||
/** | ||
* Address (& optional ENS Name) of the transaction recipient. | ||
*/ | ||
recipient: ICompositeAddress; | ||
/** | ||
* Name of the asset transferred. | ||
*/ | ||
assetName: string; | ||
/** | ||
* Icon URL of the tranferred asset. | ||
*/ | ||
assetIconSrc?: string; | ||
/** | ||
* Asset amount that was transferred. | ||
*/ | ||
assetAmount: number | string; | ||
/** | ||
* Symbol of the asset transferred. Example: ETH, DAI, etc. | ||
*/ | ||
assetSymbol: string; | ||
/** | ||
* Price per asset in fiat. | ||
*/ | ||
assetFiatPrice?: number | string; | ||
/** | ||
* Transaction hash. | ||
*/ | ||
hash: string; | ||
/** | ||
* Chain ID of the transaction. | ||
*/ | ||
chainId?: number; | ||
} | ||
|
||
export const AssetTransfer: React.FC<IAssetTransferProps> = (props) => { | ||
const { | ||
sender, | ||
recipient, | ||
assetName, | ||
assetIconSrc, | ||
assetAmount, | ||
assetSymbol, | ||
assetFiatPrice, | ||
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 ? `${blockExplorerUrl}/tx/${hash}` : undefined; | ||
|
||
const formattedTokenValue = formatterUtils.formatNumber(assetAmount, { | ||
format: NumberFormat.TOKEN_AMOUNT_SHORT, | ||
withSign: true, | ||
fallback: '-', | ||
}); | ||
const fiatValue = Number(assetAmount) * Number(assetFiatPrice); | ||
const formattedFiatValue = formatterUtils.formatNumber(fiatValue, { | ||
format: NumberFormat.FIAT_TOTAL_SHORT, | ||
fallback: ` `, | ||
}); | ||
const formattedTokenAmount = `${formattedTokenValue} ${assetSymbol}`; | ||
|
||
const assetTransferClassNames = classNames( | ||
'flex h-16 w-full items-center justify-between rounded-xl border border-neutral-100 bg-neutral-0 px-4', // base | ||
'hover:border-neutral-200 hover:shadow-neutral-md', // hover | ||
'focus:outline-none focus-visible:rounded-xl focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // focus | ||
'active:border-neutral-300', // active | ||
'md:h-20 md:px-6', // responsive | ||
); | ||
|
||
return ( | ||
<div className="flex size-full flex-col gap-y-2 md:gap-y-3"> | ||
<div className="relative flex h-full flex-col rounded-xl bg-neutral-0 md:flex-row"> | ||
<AssetTransferAddress txRole="sender" participant={sender} blockExplorerUrl={blockExplorerUrl} /> | ||
<div className="border-t border-neutral-100 md:border-l" /> | ||
<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 | ||
)} | ||
/> | ||
<AssetTransferAddress txRole="recipient" participant={recipient} blockExplorerUrl={blockExplorerUrl} /> | ||
</div> | ||
<a | ||
href={blockExplorerAssembledHref} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className={assetTransferClassNames} | ||
> | ||
<div className="flex items-center space-x-3 md:space-x-4"> | ||
<Avatar responsiveSize={{ md: 'md' }} src={assetIconSrc} /> | ||
<span className="text-sm leading-tight text-neutral-800 md:text-base">{assetName}</span> | ||
</div> | ||
<div className="flex flex-col items-end justify-end"> | ||
<span className="text-sm leading-tight text-neutral-800 md:text-base">{formattedTokenAmount}</span> | ||
<span className="text-sm leading-tight text-neutral-500 md:text-base">{formattedFiatValue}</span> | ||
</div> | ||
</a> | ||
</div> | ||
); | ||
}; |
77 changes: 77 additions & 0 deletions
77
...modules/components/asset/assetTransfer/assetTransferAddress/assetTransferAddress.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,77 @@ | ||
import '@testing-library/jest-dom'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { OdsModulesProvider } from '../../../odsModulesProvider'; | ||
import { AssetTransferAddress, type IAssetTransferAddressProps } from './assetTransferAddress'; | ||
|
||
jest.mock('../../../member/', () => ({ MemberAvatar: () => <div data-testid="member-avatar-mock" /> })); | ||
|
||
describe('<AssetTransferAddress /> component', () => { | ||
const createTestComponent = (props?: Partial<IAssetTransferAddressProps>) => { | ||
const completeProps = { | ||
txRole: 'sender' as const, | ||
participant: { address: '0x1D03D98c0aac1f83860cec5156116FE68725642E' }, | ||
// Optional, but setting blockExplorerUrl parameter to retrieve the link element through the getByRole utility | ||
blockExplorerUrl: 'https://etherscan.io', | ||
...props, | ||
}; | ||
return ( | ||
<OdsModulesProvider> | ||
<AssetTransferAddress {...completeProps} /> | ||
</OdsModulesProvider> | ||
); | ||
}; | ||
|
||
it('renders correctly as a sender', () => { | ||
const txRole = 'sender' as const; | ||
render(createTestComponent({ txRole })); | ||
|
||
const parentElement = screen.getByRole('link'); | ||
expect(parentElement).toHaveClass('rounded-t-xl md:rounded-l-xl md:rounded-r-none'); | ||
expect(screen.getByText('From')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders correctly as a recipient', () => { | ||
const txRole = 'recipient' as const; | ||
render(createTestComponent({ txRole })); | ||
|
||
const parentElement = screen.getByRole('link'); | ||
expect(parentElement).toHaveClass('rounded-b-xl md:rounded-r-xl md:rounded-l-none'); | ||
expect(screen.getByText('To')).toBeInTheDocument(); | ||
}); | ||
|
||
it('uses truncated address if ensName is undefined', () => { | ||
const participant = { address: '0x028F5Ca0b3A3A14e44AB8af660B53D1e428457e7' }; | ||
render(createTestComponent({ participant })); | ||
|
||
expect(screen.getByText('0x02…57e7')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders ENS name over address when available', () => { | ||
const participant = { address: '0x028F5Ca0b3A3A14e44AB8af660B53D1e428457e7', name: 'vitalik.eth' }; | ||
render(createTestComponent({ participant })); | ||
|
||
const ensName = screen.getByText('vitalik.eth'); | ||
expect(ensName).toBeInTheDocument(); | ||
const truncatedAddress = screen.queryByText('0x02…57e7'); | ||
expect(truncatedAddress).toBeNull(); | ||
}); | ||
|
||
it('does not create a link if blockExplorerUrl is undefined', () => { | ||
const blockExplorerUrl = undefined; | ||
render(createTestComponent({ blockExplorerUrl })); | ||
|
||
const possibleLinkElement = screen.queryByRole('link'); | ||
expect(possibleLinkElement).toBeNull(); | ||
}); | ||
|
||
it('creates a link if blockExplorerUrl is defined', () => { | ||
const blockExplorerUrl = 'https://etherscan.io'; | ||
render(createTestComponent({ blockExplorerUrl })); | ||
|
||
const possibleLinkElement = screen.getByRole('link'); | ||
expect(possibleLinkElement).toHaveAttribute( | ||
'href', | ||
'https://etherscan.io/address/0x1D03D98c0aac1f83860cec5156116FE68725642E', | ||
); | ||
}); | ||
}); |
Oops, something went wrong.