Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions components/TableContainer/TableContainerGetter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
const [pageCount, setPageCount] = useState(0)
const [loading, setLoading] = useState(true)
const emptyMessageDisplay = emptyMessage ?? DEFAULT_EMPTY_TABLE_MESSAGE
const [hiddenColumns, setHiddenColumns] = useState({})

const triggerSort = (column: any): void => {
if (column.disableSortBy === true) return
if (column.disableSortBy === true || hiddenColumns[column.id]) return

const id = column.id
if (sortColumn === id) {
setSortDesc(!sortDesc)
Expand All @@ -47,6 +49,10 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
}
gotoPage(0)
}

const toggleColumn = (id: any): void => {
setHiddenColumns((prev) => ({ ...prev, [id]: !prev[id]}))
}

const {
getTableProps,
Expand Down Expand Up @@ -114,8 +120,15 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column: any) => (
<th {...column.getHeaderProps()} style={column.disableSortBy === true ? null : { cursor: 'pointer' }} onClick={() => { triggerSort(column) }}>
<div>
{column.render('Header')}
{generateSortingIndicator(column)}
{column.shrinkable && (
<span onClick={() => toggleColumn(column.id)} style={{ cursor: 'pointer' }}>
{hiddenColumns[column.id] ? <div style= {{marginRight: '5px'}} className='table-arrow-right' /> : <div style= {{marginRight: '5px'}} className='table-sort-arrow-down' />}
</span>
)}
{!column.shrinkable && generateSortingIndicator(column)}
</div>
</th>
))}
</tr>
Expand All @@ -129,9 +142,9 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell: any) => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
{row.cells.map((cell: any) =>
hiddenColumns[cell.column.id] ? <td> </td> : <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)}
</tr>
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useMemo } from 'react'
import style from './transaction.module.css'
import Image from 'next/image'
import XECIcon from 'assets/xec-logo.png'
import BCHIcon from 'assets/bch-logo.png'
import EyeIcon from 'assets/eye-icon.png'
import CheckIcon from 'assets/check-icon.png'
import XIcon from 'assets/x-icon.png'
import TableContainerGetter from '../../components/TableContainer/TableContainerGetter'
import TableContainerGetter from '../TableContainer/TableContainerGetter'
import { compareNumericString } from 'utils/index'
import moment from 'moment-timezone'
import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL } from 'constants/index'
Expand All @@ -15,26 +14,29 @@ interface IProps {
addressSyncing: {
[address: string]: boolean
}
paybuttonId: string
tableRefreshCount: number
timezone: string
}

function getGetterForAddress (addressString: string): Function {
function fetchTransactionsByPaybuttonId (paybuttonId: string): Function {
return async (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => {
const ok = await fetch(`/api/address/transactions/${addressString}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`, {
const response = await fetch(`/api/paybutton/transactions/${paybuttonId}?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`, {
headers: {
Timezone: moment.tz.guess()
}
})
const ok2 = await fetch(`/api/address/transactions/count/${addressString}`)
const responseCount = await fetch(`/api/paybutton/transactions/count/${paybuttonId}`)
const transactions = await response.json()
const count = await responseCount.json()
return {
data: await ok.json(),
totalCount: await ok2.json()
data: transactions.transactions,
totalCount: count
}
}
}

export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => {
export default ({ paybuttonId, addressSyncing, tableRefreshCount, timezone = moment.tz.guess() }: IProps): JSX.Element => {
const columns = useMemo(
() => [
{
Expand Down Expand Up @@ -80,17 +82,31 @@ export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess(
}
},
{
Header: 'TX',
Header: () => (<div style={{ textAlign: 'center' }}>TX</div>),
accessor: 'hash',
disableSortBy: true,
Cell: (cellProps) => {
const url = cellProps.cell.row.values['address.networkId'] === 1 ? XEC_TX_EXPLORER_URL : BCH_TX_EXPLORER_URL
return (
<a href={url.concat(cellProps.cell.value)} target="_blank" rel="noopener noreferrer" className="table-eye-ctn">
<div className="table-eye">
<Image src={EyeIcon} alt='View on explorer' />
</div>
</a>
<div className="table-eye-ctn">
<a href={url.concat(cellProps.cell.value)} target="_blank" rel="noopener noreferrer">
<div className="table-eye">
<Image src={EyeIcon} alt='View on explorer' />
</div>
</a>
</div>
)
}
},
{
Header: () => (<div style={{ marginRight: '1px' }}>Address</div>),
accessor: 'address.address',
shrinkable: true,
Cell: (cellProps) => {
return (
<div>
{cellProps.cell.value}
</div>
)
}
}
Expand All @@ -99,19 +115,7 @@ export default ({ addressSyncing, tableRefreshCount, timezone = moment.tz.guess(
)
return (
<>
{Object.keys(addressSyncing).map(transactionAddress => (
<div key={transactionAddress} className='address-transactions-ctn'>
<div className={style.tablelabel}>
<div>{transactionAddress}</div>
<a href={transactionAddress.slice(0, 5) === 'ecash' ? `https://explorer.e.cash/address/${transactionAddress}` : `https://blockchair.com/bitcoin-cash/address/${transactionAddress}`} target="_blank" rel="noopener noreferrer" className="table-eye-ctn">
<div className="table-eye">
<Image src={EyeIcon} alt='View on explorer' />
</div>
</a>
</div>
<TableContainerGetter columns={columns} dataGetter={getGetterForAddress(transactionAddress)} tableRefreshCount={tableRefreshCount} emptyMessage={'No transactions.'}/>
</div>
))}
<TableContainerGetter columns={columns} dataGetter={fetchTransactionsByPaybuttonId(paybuttonId)} tableRefreshCount={tableRefreshCount} emptyMessage={'No transactions.'}/>
</>
)
}
4 changes: 2 additions & 2 deletions components/Transaction/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AddressTransactions from './AddressTransactions'
import PaybuttonTransactions from './PaybuttonTransactions'

export {
AddressTransactions
PaybuttonTransactions
}
17 changes: 14 additions & 3 deletions pages/api/paybutton/transactions/[id].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RESPONSE_MESSAGES } from 'constants/index'
import { fetchTransactionsByPaybuttonId } from 'services/transactionService'
import { RESPONSE_MESSAGES, TX_PAGE_SIZE_LIMIT } from 'constants/index'
import { fetchTransactionsByPaybuttonIdWithPagination } from 'services/transactionService'
import * as paybuttonService from 'services/paybuttonService'
import { setSession } from 'utils/setSession'
import { parseError } from 'utils/validators'
Expand All @@ -9,14 +9,25 @@ export default async (req: any, res: any): Promise<void> => {
await setSession(req, res)
const userId = req.session.userId
const paybuttonId = req.query.id as string
const page = (req.query.page === '' || req.query.page === undefined) ? 0 : Number(req.query.page)
const pageSize = (req.query.pageSize === '' || req.query.pageSize === undefined) ? DEFAULT_TX_PAGE_SIZE : Number(req.query.pageSize)
const orderBy = (req.query.orderBy === '' || req.query.orderBy === undefined) ? undefined : req.query.orderBy as string
const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true')

if (isNaN(page) || isNaN(pageSize)) {
throw new Error(RESPONSE_MESSAGES.PAGE_SIZE_AND_PAGE_SHOULD_BE_NUMBERS_400.message)
}
if (pageSize > TX_PAGE_SIZE_LIMIT) {
throw new Error(RESPONSE_MESSAGES.PAGE_SIZE_LIMIT_EXCEEDED_400.message)
}

try {
const paybutton = await paybuttonService.fetchPaybuttonById(paybuttonId)
if (paybutton.providerUserId !== userId) {
throw new Error(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message)
}

const transactions = await fetchTransactionsByPaybuttonId(paybuttonId)
const transactions = await fetchTransactionsByPaybuttonIdWithPagination(paybuttonId, page, pageSize, orderDesc, orderBy)

res.status(200).json({ transactions })
} catch (err: any) {
Expand Down
6 changes: 3 additions & 3 deletions pages/button/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import Page from 'components/Page'
import { PaybuttonDetail } from 'components/Paybutton'
import { PaybuttonWithAddresses } from 'services/paybuttonService'
import { AddressTransactions } from 'components/Transaction'
import { PaybuttonTransactions } from 'components/Transaction'
import supertokensNode from 'supertokens-node'
import * as SuperTokensConfig from '../../config/backendConfig'
import Session from 'supertokens-node/recipe/session'
Expand Down Expand Up @@ -101,7 +101,7 @@ export default function PayButton (props: PaybuttonProps): React.ReactElement {
})

socket.on(SOCKET_MESSAGES.INCOMING_TXS, (broadcastedData: BroadcastTxData) => {
setTableRefreshCount(tableRefreshCount + 1)
setTableRefreshCount(tableRefreshCountCurrent => tableRefreshCountCurrent + 1)
updateIsSyncing([broadcastedData.address])
})
}
Expand Down Expand Up @@ -207,7 +207,7 @@ export default function PayButton (props: PaybuttonProps): React.ReactElement {
</div>
</div>

<AddressTransactions addressSyncing={isSyncing} tableRefreshCount={tableRefreshCount} timezone={timezone}/>
<PaybuttonTransactions addressSyncing={isSyncing} paybuttonId={paybutton.id} tableRefreshCount={tableRefreshCount} timezone={timezone}/>
<PaybuttonTrigger emailCredits={userProfile.emailCredits} paybuttonId={paybutton.id}/>
</>
)
Expand Down
76 changes: 75 additions & 1 deletion services/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,57 @@ export async function fetchTransactionsByAddressList (
})
}

export async function fetchTransactionsByAddressListWithPagination (
addressIdList: string[],
page: number,
pageSize: number,
orderBy?: string,
orderDesc = true,
networkIdsListFilter?: number[],
): Promise<TransactionsWithPaybuttonsAndPrices[]> {

const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc'

// Get query for orderBy that works with nested properties (e.g. `address.networkId`)
let orderByQuery
if (orderBy !== undefined && orderBy !== '') {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments for what this code block is doing?

if (orderBy.includes('.')) {
const [relation, property] = orderBy.split('.')
orderByQuery = {
[relation]: {
[property]: orderDescString
}
}
} else {
orderByQuery = {
[orderBy]: orderDescString
}
}
} else {
// Default orderBy
orderByQuery = {
timestamp: orderDescString
}
}

return await prisma.transaction.findMany({
where: {
addressId: {
in: addressIdList
},
address: {
networkId: {
in: networkIdsListFilter ?? Object.values(NETWORK_IDS)
}
}
},
include: includePaybuttonsAndPrices,
orderBy: orderByQuery,
skip: page * pageSize,
take: pageSize,
})
}

export async function fetchTxCountByAddressString (addressString: string): Promise<number> {
return await prisma.transaction.count({
where: {
Expand Down Expand Up @@ -513,6 +564,29 @@ export async function fetchTransactionsByPaybuttonId (paybuttonId: string, netwo
return transactions
}

export async function fetchTransactionsByPaybuttonIdWithPagination (
paybuttonId: string,
page: number,
pageSize: number,
orderDesc: boolean,
orderBy?: string,
networkIds?: number[]): Promise<TransactionsWithPaybuttonsAndPrices[]> {
const addressIdList = await fetchAddressesByPaybuttonId(paybuttonId)
const transactions = await fetchTransactionsByAddressListWithPagination(
addressIdList,
page,
pageSize,
orderBy,
orderDesc,
networkIds);

if (transactions.length === 0) {
throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message)
}

return transactions
}

export const getTransactionValueInCurrency = (transaction: TransactionWithAddressAndPrices, currency: SupportedQuotesType): number => {
const {
prices,
Expand Down Expand Up @@ -661,7 +735,7 @@ export async function fetchAllPaymentsByUserIdWithPagination (
userId, page, pageSize, orderDesc, buttonIds
)
}

// Get query for orderBy that works with nested properties (e.g. `address.networkId`)
let orderByQuery
if (orderBy !== undefined && orderBy !== '') {
if (orderBy === 'values') {
Expand Down
19 changes: 17 additions & 2 deletions styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ button:enabled:hover {
background: #f4f4f4;
}

.table-arrow-right {
width: 0;
height: 0;
border-style: solid;
border-width: 4px 5px 4px 0;
border-color: transparent var(--primary-text-color) transparent transparent;
position: absolute;
right: 0;
top: 0;
bottom: 0;
margin: auto;
opacity: 0.5;
}

.table-sort-arrow-down,
.table-sort-arrow-up {
width: 0;
Expand Down Expand Up @@ -219,12 +233,13 @@ button:enabled:hover {
}

.table-eye-ctn {
text-align: right;
text-align: center;
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: center;
}


.table-eye {
width: 20px;
opacity: 0.6;
Expand Down