Skip to content

Commit

Permalink
feat: add wallet address field to tx history (#2080)
Browse files Browse the repository at this point in the history
  • Loading branch information
fionnachan authored Nov 29, 2024
1 parent fcecf30 commit 417283c
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ export const EmptyTransactionHistory = ({
</ContentWrapper>
)
}
return <ContentWrapper>Looks like no transactions here yet.</ContentWrapper>
return (
<ContentWrapper className="lg:text-center">
No {tabType} transactions.
</ContentWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,8 @@
import dayjs from 'dayjs'
import { useEffect, useMemo } from 'react'
import { Tab } from '@headlessui/react'
import { create } from 'zustand'
import { useAccount } from 'wagmi'

import { TransactionHistoryTable } from './TransactionHistoryTable'
import { TransactionStatusInfo } from '../TransactionHistory/TransactionStatusInfo'
import {
isTxClaimable,
isTxCompleted,
isTxExpired,
isTxFailed,
isTxPending
} from './helpers'
import { MergedTransaction } from '../../state/app/state'
import { TabButton } from '../common/Tab'
import { TransactionsTableDetails } from './TransactionsTableDetails'
import { useTransactionHistory } from '../../hooks/useTransactionHistory'

function useTransactionHistoryUpdater() {
const { address } = useAccount()

const transactionHistoryProps = useTransactionHistory(address, {
runFetcher: true
})

const { transactions, updatePendingTransaction } = transactionHistoryProps

const pendingTransactions = useMemo(() => {
return transactions.filter(isTxPending)
}, [transactions])

useEffect(() => {
const interval = setInterval(() => {
pendingTransactions.forEach(updatePendingTransaction)
}, 10_000)

return () => clearInterval(interval)
}, [pendingTransactions, updatePendingTransaction])

return transactionHistoryProps
}

const tabClasses =
'text-white px-3 mr-2 border-b-2 ui-selected:border-white ui-not-selected:border-transparent ui-not-selected:text-white/80 arb-hover'
import { TransactionHistorySearchBar } from './TransactionHistorySearchBar'
import { TransactionHistorySearchResults } from './TransactionHistorySearchResults'

type TxDetailsStore = {
tx: MergedTransaction | null
Expand Down Expand Up @@ -72,98 +31,11 @@ export const useTxDetailsStore = create<TxDetailsStore>(set => ({
}))

export const TransactionHistory = () => {
const { address } = useAccount()
const props = useTransactionHistoryUpdater()
const { transactions } = props

const oldestTxTimeAgoString = useMemo(() => {
return dayjs(transactions[transactions.length - 1]?.createdAt).toNow(true)
}, [transactions])

const groupedTransactions = useMemo(
() =>
transactions.reduce(
(acc, tx) => {
if (isTxCompleted(tx) || isTxExpired(tx)) {
acc.settled.push(tx)
}
if (isTxPending(tx)) {
acc.pending.push(tx)
}
if (isTxClaimable(tx)) {
acc.claimable.push(tx)
}
if (isTxFailed(tx)) {
acc.failed.push(tx)
}
return acc
},
{
settled: [] as MergedTransaction[],
pending: [] as MergedTransaction[],
claimable: [] as MergedTransaction[],
failed: [] as MergedTransaction[]
}
),
[transactions]
)

const pendingTransactions = [
...groupedTransactions.failed,
...groupedTransactions.pending,
...groupedTransactions.claimable
]

const settledTransactions = groupedTransactions.settled

return (
<div className="m-auto w-full max-w-[100vw] border-y border-white/30 bg-[#191919] py-4 pl-4 md:max-w-[1000px] md:rounded md:border-x md:pr-4">
<div className="pr-4 md:pr-0">
<TransactionStatusInfo />
</div>

<Tab.Group
key={address}
as="div"
className="h-full overflow-hidden rounded pr-4 md:pr-0"
>
<Tab.List className="mb-4 flex border-b border-white/30">
<TabButton
aria-label="show pending transactions"
className={tabClasses}
>
<span className="text-sm md:text-base">Pending transactions</span>
</TabButton>
<TabButton
aria-label="show settled transactions"
className={tabClasses}
>
<span className="text-sm md:text-base">Settled transactions</span>
</TabButton>
</Tab.List>
<TransactionHistorySearchBar />

<Tab.Panels className="h-full w-full overflow-hidden">
<Tab.Panel className="h-full w-full">
<TransactionHistoryTable
{...props}
address={address}
transactions={pendingTransactions}
selectedTabIndex={0}
oldestTxTimeAgoString={oldestTxTimeAgoString}
/>
</Tab.Panel>
<Tab.Panel className="h-full w-full">
<TransactionHistoryTable
{...props}
address={address}
transactions={settledTransactions}
selectedTabIndex={1}
oldestTxTimeAgoString={oldestTxTimeAgoString}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<TransactionsTableDetails address={address} />
<TransactionHistorySearchResults />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { create } from 'zustand'
import { isAddress } from 'ethers/lib/utils.js'
import { Address, useAccount } from 'wagmi'
import { useCallback, useEffect } from 'react'
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { twMerge } from 'tailwind-merge'

import { Button } from '../common/Button'

export enum TransactionHistorySearchError {
INVALID_ADDRESS = 'That doesn’t seem to be a valid address, please try again.'
}

type TransactionHistoryAddressStore = {
address: string
sanitizedAddress: Address | undefined
searchError: TransactionHistorySearchError | undefined
setAddress: (address: string) => void
setSanitizedAddress: (address: string) => void
setSearchError: (error: TransactionHistorySearchError | undefined) => void
}

export const useTransactionHistoryAddressStore =
create<TransactionHistoryAddressStore>(set => ({
address: '',
sanitizedAddress: undefined,
setAddress: (address: string) => set({ address }),
setSanitizedAddress: (address: string) => {
if (isAddress(address)) {
set({ sanitizedAddress: address })
}
},
searchError: undefined,
setSearchError: (error: TransactionHistorySearchError | undefined) =>
set({ searchError: error })
}))

export function TransactionHistorySearchBar() {
const { address, setAddress, setSanitizedAddress, setSearchError } =
useTransactionHistoryAddressStore()
const { address: connectedAddress } = useAccount()

useEffect(() => {
if (address === '' && connectedAddress) {
setSanitizedAddress(connectedAddress)
setSearchError(undefined)
}
}, [address, connectedAddress, setSanitizedAddress, setSearchError])

const searchTxForAddress = useCallback(() => {
if (address === '') {
return
}

if (!isAddress(address)) {
setSearchError(TransactionHistorySearchError.INVALID_ADDRESS)
return
}

setSanitizedAddress(address)
setSearchError(undefined)
}, [address, setSanitizedAddress, setSearchError])

return (
<div className="mb-4 flex flex-row items-stretch pr-4 md:pr-0">
<form
className={twMerge(
'flex w-full items-center justify-center overflow-hidden rounded border border-gray-dark bg-black text-white md:w-1/2'
)}
onSubmit={event => event.preventDefault()}
>
<MagnifyingGlassIcon className="ml-3 mr-1 h-3 w-3" />
<input
type="text"
value={address}
onChange={event => setAddress(event.target.value)}
inputMode="search"
placeholder="Search by address"
aria-label="Transaction history wallet address input"
className="h-full w-full bg-transparent py-1 pl-1 pr-3 text-sm font-light placeholder:text-white/60"
// stop password managers from autofilling
data-1p-ignore
data-lpignore="true"
data-form-type="other"
/>
<Button
variant="secondary"
className={twMerge(
'select-none rounded-l-none border-y-0 border-r-0 border-gray-dark bg-black py-[7px]',
'hover:bg-white/20 hover:opacity-100',
'disabled:border-y-0 disabled:border-r-0 disabled:border-l-gray-dark'
)}
onClick={searchTxForAddress}
disabled={!address}
>
Search
</Button>
</form>
</div>
)
}
Loading

0 comments on commit 417283c

Please sign in to comment.