-
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-2797 - Implement TransactionDataListItem.Structure module c…
…omponent (#135)
- Loading branch information
1 parent
613663c
commit 9eaf8aa
Showing
9 changed files
with
371 additions
and
2 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
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
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 './transactionDataListItem'; |
10 changes: 10 additions & 0 deletions
10
src/modules/components/transaction/transactionDataListItem/index.ts
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,10 @@ | ||
import { TransactionDataListItemStructure as Structure } from './transactionDataListItemStructure/transactionDataListItemStructure'; | ||
|
||
export const TransactionDataListItem = { | ||
Structure, | ||
}; | ||
export { | ||
ITransactionDataListItemProps, | ||
TransactionStatus, | ||
TransactionType, | ||
} from './transactionDataListItemStructure/transactionDataListItemStructure.api'; |
6 changes: 6 additions & 0 deletions
6
.../components/transaction/transactionDataListItem/transactionDataListItemStructure/index.ts
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,6 @@ | ||
export { TransactionDataListItemStructure } from './transactionDataListItemStructure'; | ||
export { | ||
ITransactionDataListItemProps, | ||
TransactionStatus, | ||
TransactionType, | ||
} from './transactionDataListItemStructure.api'; |
104 changes: 104 additions & 0 deletions
104
...transactionDataListItem/transactionDataListItemStructure/transactionDataListItem.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,104 @@ | ||
import { render, screen, waitFor } from '@testing-library/react'; | ||
import * as wagmi from 'wagmi'; | ||
import { DataList, NumberFormat, formatterUtils } from '../../../../../core'; | ||
import { TransactionDataListItemStructure } from './transactionDataListItemStructure'; | ||
import { | ||
TransactionStatus, | ||
TransactionType, | ||
type ITransactionDataListItemProps, | ||
} from './transactionDataListItemStructure.api'; | ||
|
||
describe('<TransactionDataListItem.Structure /> component', () => { | ||
const useChainsMock = jest.spyOn(wagmi, 'useChains'); | ||
|
||
beforeEach(() => { | ||
useChainsMock.mockReturnValue([ | ||
{ | ||
id: 1, | ||
blockExplorers: { | ||
default: { name: 'Etherscan', url: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/api' }, | ||
}, | ||
name: 'Chain Name', | ||
nativeCurrency: { | ||
decimals: 18, | ||
name: 'Ether', | ||
symbol: 'ETH', | ||
}, | ||
rpcUrls: { default: { http: ['https://cloudflare-eth.com'] } }, | ||
}, | ||
]); | ||
}); | ||
|
||
afterEach(() => { | ||
useChainsMock.mockReset(); | ||
}); | ||
|
||
const createTestComponent = (props?: Partial<ITransactionDataListItemProps>) => { | ||
const defaultProps: ITransactionDataListItemProps = { | ||
chainId: 1, | ||
hash: '0x123', | ||
date: '2023-01-01T00:00:00Z', | ||
...props, | ||
}; | ||
return ( | ||
<DataList.Root entityLabel="Daos"> | ||
<DataList.Container> | ||
<TransactionDataListItemStructure {...defaultProps} /> | ||
</DataList.Container> | ||
</DataList.Root> | ||
); | ||
}; | ||
|
||
it('renders the transaction type heading', () => { | ||
const type = TransactionType.ACTION; | ||
render(createTestComponent({ type })); | ||
const transactionTypeHeading = screen.getByText('Smart contract action'); | ||
expect(transactionTypeHeading).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the token value and symbol in a deposit', () => { | ||
const tokenSymbol = 'ETH'; | ||
const tokenAmount = 10; | ||
const type = TransactionType.DEPOSIT; | ||
render(createTestComponent({ tokenSymbol, tokenAmount, type })); | ||
const tokenPrintout = screen.getByText('10 ETH'); | ||
expect(tokenPrintout).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the formatted USD estimate', () => { | ||
const tokenPrice = 100; | ||
const tokenAmount = 10; | ||
const type = TransactionType.DEPOSIT; | ||
const formattedEstimate = formatterUtils.formatNumber(tokenPrice * tokenAmount, { | ||
format: NumberFormat.FIAT_TOTAL_SHORT, | ||
}); | ||
render(createTestComponent({ tokenPrice, tokenAmount, type })); | ||
const formattedUsdEstimate = screen.getByText(formattedEstimate as string); | ||
expect(formattedUsdEstimate).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders a failed transaction indicator alongside the transaction type', () => { | ||
render(createTestComponent({ type: TransactionType.DEPOSIT, status: TransactionStatus.FAILED })); | ||
const failedTransactionText = screen.getByText('Deposit'); | ||
expect(failedTransactionText).toBeInTheDocument(); | ||
const closeIcon = screen.getByTestId('CLOSE'); | ||
expect(closeIcon).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the provided timestamp correctly', () => { | ||
const date = '2000-01-01T00:00:00Z'; | ||
render(createTestComponent({ date })); | ||
expect(screen.getByText(date)).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders with the correct block explorer URL', async () => { | ||
const chainId = 1; | ||
const hash = '0x123'; | ||
render(createTestComponent({ chainId, hash })); | ||
|
||
await waitFor(() => { | ||
const linkElement = screen.getByRole<HTMLAnchorElement>('link'); | ||
expect(linkElement).toHaveAttribute('href', 'https://etherscan.io/tx/0x123'); | ||
}); | ||
}); | ||
}); |
55 changes: 55 additions & 0 deletions
55
...tionDataListItem/transactionDataListItemStructure/transactionDataListItemStructure.api.ts
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,55 @@ | ||
import { type Hash } from 'viem'; | ||
import { type IDataListItemProps } from '../../../../../core'; | ||
|
||
export enum TransactionStatus { | ||
PENDING = 'PENDING', | ||
SUCCESS = 'SUCCESS', | ||
FAILED = 'FAILED', | ||
} | ||
|
||
export enum TransactionType { | ||
DEPOSIT = 'DEPOSIT', | ||
WITHDRAW = 'WITHDRAW', | ||
ACTION = 'ACTION', | ||
} | ||
|
||
export interface ITransactionDataListItemProps extends IDataListItemProps { | ||
/** | ||
* The chain ID of the transaction. | ||
*/ | ||
chainId: number; | ||
/** | ||
* The address of the token. | ||
*/ | ||
tokenAddress?: string; | ||
/** | ||
* The symbol of the token, e.g. 'ETH' as a string | ||
*/ | ||
tokenSymbol?: string; | ||
/** | ||
* The token value in the transaction. | ||
*/ | ||
tokenAmount?: number | string; | ||
/** | ||
* The estimated fiat value of the transaction. | ||
*/ | ||
tokenPrice?: number | string; | ||
/** | ||
* The type of transaction. | ||
* @default TransactionType.ACTION | ||
*/ | ||
type?: TransactionType; | ||
/** | ||
* The current status of a blockchain transaction on the network. | ||
* @default TransactionStatus.PENDING | ||
*/ | ||
status?: TransactionStatus; | ||
/** | ||
* The Unix timestamp of the transaction. | ||
*/ | ||
date: string; | ||
/** | ||
* The transaction hash. | ||
*/ | ||
hash: Hash; | ||
} |
71 changes: 71 additions & 0 deletions
71
...ataListItem/transactionDataListItemStructure/transactionDataListItemStructure.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,71 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { DataList } from '../../../../../core'; | ||
import { TransactionDataListItemStructure } from './transactionDataListItemStructure'; | ||
import { TransactionStatus, TransactionType } from './transactionDataListItemStructure.api'; | ||
|
||
const meta: Meta<typeof TransactionDataListItemStructure> = { | ||
title: 'Modules/Components/Transaction/TransactionDataListItem.Structure', | ||
component: TransactionDataListItemStructure, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/P0GeJKqILL7UXvaqu5Jj7V/v1.1.0?type=design&node-id=445-5113&mode=design&t=qzF3muTU7z33q8EX-4', | ||
}, | ||
}, | ||
argTypes: { | ||
hash: { | ||
control: 'text', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof TransactionDataListItemStructure>; | ||
|
||
/** | ||
* Default usage example of the TransactionDataList module component. | ||
*/ | ||
export const Default: Story = { | ||
render: (args) => ( | ||
<DataList.Root entityLabel="Transactions"> | ||
<DataList.Container> | ||
<TransactionDataListItemStructure {...args} /> | ||
</DataList.Container> | ||
</DataList.Root> | ||
), | ||
}; | ||
|
||
export const Withdraw: Story = { | ||
args: { | ||
status: TransactionStatus.SUCCESS, | ||
type: TransactionType.WITHDRAW, | ||
tokenAmount: 10, | ||
tokenSymbol: 'ETH', | ||
}, | ||
render: (args) => ( | ||
<DataList.Root entityLabel="Transactions"> | ||
<DataList.Container> | ||
<TransactionDataListItemStructure {...args} /> | ||
</DataList.Container> | ||
</DataList.Root> | ||
), | ||
}; | ||
|
||
export const Failed: Story = { | ||
args: { | ||
status: TransactionStatus.FAILED, | ||
type: TransactionType.DEPOSIT, | ||
tokenSymbol: 'ETH', | ||
tokenAmount: 10, | ||
tokenPrice: 100, | ||
}, | ||
render: (args) => ( | ||
<DataList.Root entityLabel="Transactions"> | ||
<DataList.Container> | ||
<TransactionDataListItemStructure {...args} /> | ||
</DataList.Container> | ||
</DataList.Root> | ||
), | ||
}; | ||
|
||
export default meta; |
121 changes: 121 additions & 0 deletions
121
...sactionDataListItem/transactionDataListItemStructure/transactionDataListItemStructure.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,121 @@ | ||
import classNames from 'classnames'; | ||
import { useChains } from 'wagmi'; | ||
import { | ||
AvatarIcon, | ||
DataList, | ||
IconType, | ||
NumberFormat, | ||
Spinner, | ||
formatterUtils, | ||
type AvatarIconVariant, | ||
} from '../../../../../core'; | ||
import { | ||
TransactionStatus, | ||
TransactionType, | ||
type ITransactionDataListItemProps, | ||
} from './transactionDataListItemStructure.api'; | ||
|
||
const txHeadingStringList: Record<TransactionType, string> = { | ||
[TransactionType.DEPOSIT]: 'Deposit', | ||
[TransactionType.WITHDRAW]: 'Withdraw', | ||
[TransactionType.ACTION]: 'Smart contract action', | ||
}; | ||
|
||
const txIconTypeList: Record<TransactionType, IconType> = { | ||
[TransactionType.DEPOSIT]: IconType.DEPOSIT, | ||
[TransactionType.WITHDRAW]: IconType.WITHDRAW, | ||
[TransactionType.ACTION]: IconType.BLOCKCHAIN_SMARTCONTRACT, | ||
}; | ||
|
||
const txVariantList: Record<TransactionType, AvatarIconVariant> = { | ||
[TransactionType.DEPOSIT]: 'success', | ||
[TransactionType.WITHDRAW]: 'warning', | ||
[TransactionType.ACTION]: 'info', | ||
}; | ||
|
||
export const TransactionDataListItemStructure: React.FC<ITransactionDataListItemProps> = (props) => { | ||
const { | ||
chainId, | ||
tokenAddress, | ||
tokenSymbol, | ||
tokenAmount, | ||
tokenPrice, | ||
type = TransactionType.ACTION, | ||
status = TransactionStatus.PENDING, | ||
// TO-DO: implement formatter decision | ||
date, | ||
hash, | ||
href, | ||
className, | ||
...otherProps | ||
} = props; | ||
const chains = useChains(); | ||
|
||
const matchingChain = chains?.find((chain) => chain.id === chainId); | ||
const blockExplorerBaseUrl = matchingChain?.blockExplorers?.default?.url; | ||
const blockExplorerAssembledHref = blockExplorerBaseUrl ? `${blockExplorerBaseUrl}/tx/${hash}` : undefined; | ||
|
||
const parsedHref = blockExplorerAssembledHref ?? href; | ||
|
||
const formattedTokenValue = formatterUtils.formatNumber(tokenAmount, { | ||
format: NumberFormat.TOKEN_AMOUNT_SHORT, | ||
}); | ||
|
||
const fiatValue = Number(tokenAmount ?? 0) * Number(tokenPrice ?? 0); | ||
const formattedTokenPrice = formatterUtils.formatNumber(fiatValue, { | ||
format: NumberFormat.FIAT_TOTAL_SHORT, | ||
}); | ||
|
||
const formattedTokenAmount = | ||
type === TransactionType.ACTION || tokenAmount == null ? '-' : `${formattedTokenValue} ${tokenSymbol}`; | ||
|
||
return ( | ||
<DataList.Item | ||
className={classNames('px-4 py-0 md:px-6', className)} | ||
href={parsedHref} | ||
target="_blank" | ||
{...otherProps} | ||
> | ||
<div className="flex w-full justify-between py-3 md:py-4"> | ||
<div className="flex items-center gap-x-3 md:gap-x-4"> | ||
{status === TransactionStatus.SUCCESS && ( | ||
<AvatarIcon | ||
className="shrink-0" | ||
variant={txVariantList[type]} | ||
icon={txIconTypeList[type]} | ||
responsiveSize={{ md: 'md' }} | ||
/> | ||
)} | ||
{status === TransactionStatus.FAILED && ( | ||
<AvatarIcon | ||
className="shrink-0" | ||
variant="critical" | ||
icon={IconType.CLOSE} | ||
responsiveSize={{ md: 'md' }} | ||
/> | ||
)} | ||
{status === TransactionStatus.PENDING && ( | ||
<div className="flex size-6 shrink-0 items-center justify-center md:size-8"> | ||
<Spinner className="transition" variant="neutral" responsiveSize={{ md: 'lg' }} /> | ||
</div> | ||
)} | ||
<div className="flex w-full flex-col items-start gap-y-0.5"> | ||
<span className="text-sm font-normal leading-tight text-neutral-800 md:text-base"> | ||
{txHeadingStringList[type]} | ||
</span> | ||
<p className="text-sm font-normal leading-tight text-neutral-500 md:text-base">{date}</p> | ||
</div> | ||
</div> | ||
|
||
<div className="flex flex-col items-end gap-y-0.5"> | ||
<span className="text-sm font-normal leading-tight text-neutral-800 md:text-base"> | ||
{formattedTokenAmount} | ||
</span> | ||
<span className="text-sm font-normal leading-tight text-neutral-500 md:text-base"> | ||
{formattedTokenPrice} | ||
</span> | ||
</div> | ||
</div> | ||
</DataList.Item> | ||
); | ||
}; |