From 28286ec25bf36a80ff043a61ed5fe3047c7e59b1 Mon Sep 17 00:00:00 2001 From: Piotr Sadlik Date: Fri, 26 Apr 2024 13:26:30 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=B7=20Add=20Place=20Bid=20transaction?= =?UTF-8?q?=20flow=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartlomiej Tarczynski --- .../hooks/useExplorerAddressLink.ts | 13 ---- .../src/blockchain/hooks/useExplorerLinks.ts | 19 +++++ .../transaction/TransactionAction.ts | 11 +++ .../blockchain/transaction/Transactions.ts | 5 ++ .../src/blockchain/transaction/index.ts | 2 + .../components/auction/AuctionTransaction.tsx | 72 +++++++++++++++++ .../TransactionSuccess/TransactionSuccess.tsx | 78 +++++++++++++++++++ .../TransactionSuccessHeader.tsx | 47 +++++++++++ .../auction/TransactionSuccess/index.ts | 1 + .../src/components/bids/BidsListEntry.tsx | 6 +- .../bids/allBids/GoldenTicketWinner.tsx | 2 +- .../src/components/form/ReviewForm.tsx | 60 ++++++++++++++ .../src/components/stepper/Stepper.tsx | 16 ++-- .../components/stepper/TransactionStepper.tsx | 66 ++++++++++++++++ .../src/components/topBar/AccountButton.tsx | 4 +- .../components/topBar/AccountDetailModal.tsx | 6 +- .../userActious/bid/PlaceBid/PlaceBidFlow.tsx | 27 ++----- .../userActious/bid/PlaceBid/PlaceBidForm.tsx | 8 +- .../userActious/bid/PlaceBid/usePlaceBid.ts | 37 +++++++++ .../src/utils/formatters/shortenEthAddress.ts | 4 - .../src/utils/formatters/shortenHexString.ts | 4 + 21 files changed, 429 insertions(+), 59 deletions(-) delete mode 100644 packages/frontend/src/blockchain/hooks/useExplorerAddressLink.ts create mode 100644 packages/frontend/src/blockchain/hooks/useExplorerLinks.ts create mode 100644 packages/frontend/src/blockchain/transaction/TransactionAction.ts create mode 100644 packages/frontend/src/blockchain/transaction/Transactions.ts create mode 100644 packages/frontend/src/blockchain/transaction/index.ts create mode 100644 packages/frontend/src/components/auction/AuctionTransaction.tsx create mode 100644 packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccess.tsx create mode 100644 packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccessHeader.tsx create mode 100644 packages/frontend/src/components/auction/TransactionSuccess/index.ts create mode 100644 packages/frontend/src/components/form/ReviewForm.tsx create mode 100644 packages/frontend/src/components/stepper/TransactionStepper.tsx create mode 100644 packages/frontend/src/components/userActious/bid/PlaceBid/usePlaceBid.ts delete mode 100644 packages/frontend/src/utils/formatters/shortenEthAddress.ts create mode 100644 packages/frontend/src/utils/formatters/shortenHexString.ts diff --git a/packages/frontend/src/blockchain/hooks/useExplorerAddressLink.ts b/packages/frontend/src/blockchain/hooks/useExplorerAddressLink.ts deleted file mode 100644 index 7746971d..00000000 --- a/packages/frontend/src/blockchain/hooks/useExplorerAddressLink.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Hex } from 'viem' -import { useChainId, useChains } from 'wagmi' - -export const useExplorerAddressLink = (address: Hex | undefined): string | undefined => { - const chains = useChains() - const chainId = useChainId() - const currentChain = chains.find((chain) => chain.id === chainId) - - if (!currentChain || !currentChain.blockExplorers?.default || !address) { - return undefined - } - return `${currentChain.blockExplorers.default.url}/address/${address}` -} diff --git a/packages/frontend/src/blockchain/hooks/useExplorerLinks.ts b/packages/frontend/src/blockchain/hooks/useExplorerLinks.ts new file mode 100644 index 00000000..a2f296e4 --- /dev/null +++ b/packages/frontend/src/blockchain/hooks/useExplorerLinks.ts @@ -0,0 +1,19 @@ +import { Hex } from 'viem' +import { useChainId, useChains } from 'wagmi' + +export function useExplorerAddressLink(address: Hex | undefined): string | undefined { + const url = useExplorerUrl() + return url && `${url}/address/${address}` +} + +export function useExplorerTxLink(txHash: string) { + const url = useExplorerUrl() + return `${url}/tx/${txHash}` +} + +function useExplorerUrl() { + const chains = useChains() + const chainId = useChainId() + const currentChain = chains.find((chain) => chain.id === chainId) + return currentChain?.blockExplorers?.default.url +} diff --git a/packages/frontend/src/blockchain/transaction/TransactionAction.ts b/packages/frontend/src/blockchain/transaction/TransactionAction.ts new file mode 100644 index 00000000..b691e635 --- /dev/null +++ b/packages/frontend/src/blockchain/transaction/TransactionAction.ts @@ -0,0 +1,11 @@ +import { MutationStatus } from '@tanstack/react-query' +import { Transactions } from '.' +import { Hex } from 'viem' + +export interface TransactionAction { + type: Transactions + send: () => Promise + status: MutationStatus + resetStatus: () => void + transactionHash: Hex | undefined +} diff --git a/packages/frontend/src/blockchain/transaction/Transactions.ts b/packages/frontend/src/blockchain/transaction/Transactions.ts new file mode 100644 index 00000000..aad7179a --- /dev/null +++ b/packages/frontend/src/blockchain/transaction/Transactions.ts @@ -0,0 +1,5 @@ +export enum Transactions { + Place, + Bump, + Withdraw, +} diff --git a/packages/frontend/src/blockchain/transaction/index.ts b/packages/frontend/src/blockchain/transaction/index.ts new file mode 100644 index 00000000..de296479 --- /dev/null +++ b/packages/frontend/src/blockchain/transaction/index.ts @@ -0,0 +1,2 @@ +export * from './TransactionAction' +export * from './Transactions' diff --git a/packages/frontend/src/components/auction/AuctionTransaction.tsx b/packages/frontend/src/components/auction/AuctionTransaction.tsx new file mode 100644 index 00000000..058aeb3d --- /dev/null +++ b/packages/frontend/src/components/auction/AuctionTransaction.tsx @@ -0,0 +1,72 @@ +import { TransactionAction, Transactions } from '@/blockchain/transaction' +import styled from 'styled-components' +import { TxFlowSteps } from './TxFlowSteps' +import { BackButton } from '../buttons/BackButton' +import { FormSubHeading, FormWrapper } from '../form' +import { TransactionStepper } from '../stepper/TransactionStepper' +import { ReviewForm } from '../form/ReviewForm' +import { TransactionSuccess } from './TransactionSuccess' + +export const heading = { + [Transactions.Place]: 'Place bid', + [Transactions.Bump]: 'Bump your Bid', + [Transactions.Withdraw]: 'Withdraw', +} + +interface AuctionTransactionProps { + action: TransactionAction + amount: bigint + impact?: bigint + view: TxFlowSteps + setView: (state: TxFlowSteps) => void +} + +export const AuctionTransaction = ({ action, amount, impact, view, setView }: AuctionTransactionProps) => { + const isFailed = action.status === 'error' + + return ( + + + + {view !== TxFlowSteps.Confirmation && ( + + )} + {heading[action.type]} + + {view === TxFlowSteps.Review && ( + + )} + {view === TxFlowSteps.Confirmation && ( + + )} + + + + ) +} + +const Transaction = styled.div` + display: flex; + width: 100%; +` + +const TransactionWrapper = styled(FormWrapper)` + flex: 1; + row-gap: 24px; + padding: 82px 54px; + width: fit-content; +` +const TransactionHeading = styled.div` + display: flex; + align-items: center; + column-gap: 16px; +` diff --git a/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccess.tsx b/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccess.tsx new file mode 100644 index 00000000..994dfa9a --- /dev/null +++ b/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccess.tsx @@ -0,0 +1,78 @@ +import { Transactions } from '@/blockchain/transaction' +import styled from 'styled-components' +import { TxFlowSteps } from '../TxFlowSteps' +import { TransactionSuccessHeader } from './TransactionSuccessHeader' +import { useExplorerTxLink } from '@/blockchain/hooks/useExplorerLinks' +import { CopyButton } from '@/components/buttons/CopyButton' +import { RedirectButton } from '@/components/buttons/RedirectButton' +import { Button } from '@/components/buttons' +import { Form, InputLabel } from '@/components/form' +import { Colors } from '@/styles/colors' +import { shortenHexString } from '@/utils/formatters/shortenHexString' +import { Hex } from 'viem' + +interface Props { + txHash: Hex | undefined + action: Transactions + setView: (state: TxFlowSteps) => void + resetStatus: () => void +} + +export const TransactionSuccess = ({ txHash, action, setView, resetStatus }: Props) => { + const transactionLink = useExplorerTxLink(txHash ?? '0x') + + const goHome = () => { + setView(0) + resetStatus() + } + + if (!txHash) { + return null + } + + return ( + + + + Your transaction hash + + {shortenHexString(txHash, 12)} + + + + + + + ) +} + +const Container = styled(Form)` + row-gap: 24px; +` + +const TransactionIdWrapper = styled.div` + display: flex; + width: 100%; + flex-direction: column; +` + +const TransactionIdLabel = styled(InputLabel)` + justify-content: flex-start; + margin-bottom: 8px; +` + +const TransactionIdBox = styled.div` + display: flex; + align-items: center; + width: 100%; + background-color: ${Colors.White}; + padding: 8px 12px; + height: 40px; +` + +const TransactionIdText = styled.span` + flex: 1; + color: ${Colors.Grey}; +` diff --git a/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccessHeader.tsx b/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccessHeader.tsx new file mode 100644 index 00000000..d88dd9ce --- /dev/null +++ b/packages/frontend/src/components/auction/TransactionSuccess/TransactionSuccessHeader.tsx @@ -0,0 +1,47 @@ +import { Transactions } from '@/blockchain/transaction' +import { CheckIcon } from '@/components/icons' +import { Colors } from '@/styles/colors' +import styled from 'styled-components' + +const text = { + [Transactions.Place]: 'placed your bid', + [Transactions.Bump]: 'bumped your bid', + [Transactions.Withdraw]: 'withdrawn your funds', +} + +interface SuccessHeaderProps { + action: Transactions +} + +export function TransactionSuccessHeader({ action }: SuccessHeaderProps) { + return ( + + + + + You've successfully {text[action]}! + + ) +} + +const SuccessHeaderWrap = styled.div` + display: flex; + align-items: center; + column-gap: 8px; +` + +const HeaderRowIconContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 38px; + height: 38px; + border-radius: 50%; + border: 2px solid ${Colors.Black}; +` + +const SuccessText = styled.span` + font-weight: 600; + font-size: 20px; + line-height: 26px; +` diff --git a/packages/frontend/src/components/auction/TransactionSuccess/index.ts b/packages/frontend/src/components/auction/TransactionSuccess/index.ts new file mode 100644 index 00000000..e0230900 --- /dev/null +++ b/packages/frontend/src/components/auction/TransactionSuccess/index.ts @@ -0,0 +1 @@ +export * from './TransactionSuccess' diff --git a/packages/frontend/src/components/bids/BidsListEntry.tsx b/packages/frontend/src/components/bids/BidsListEntry.tsx index 59ed23ef..90ed207d 100644 --- a/packages/frontend/src/components/bids/BidsListEntry.tsx +++ b/packages/frontend/src/components/bids/BidsListEntry.tsx @@ -3,8 +3,8 @@ import styled, { css } from 'styled-components' import { Bid } from '@/types/bid' import { Colors } from '@/styles/colors' import { formatEther } from 'viem' -import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerAddressLink' -import { shortenEthAddress } from '@/utils/formatters/shortenEthAddress' +import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerLinks' +import { shortenHexString } from '@/utils/formatters/shortenHexString' interface Props { bid: Bid @@ -23,7 +23,7 @@ export const BidsListEntry = ({ bid, isUser, view = 'full' }: Props) => { - {view === 'short' ? shortenEthAddress(bid.address) : bid.address} + {view === 'short' ? shortenHexString(bid.address) : bid.address} diff --git a/packages/frontend/src/components/bids/allBids/GoldenTicketWinner.tsx b/packages/frontend/src/components/bids/allBids/GoldenTicketWinner.tsx index 9714bda1..7af9400e 100644 --- a/packages/frontend/src/components/bids/allBids/GoldenTicketWinner.tsx +++ b/packages/frontend/src/components/bids/allBids/GoldenTicketWinner.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components' import { Colors } from '@/styles/colors' -import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerAddressLink' +import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerLinks' import { Hex } from 'viem' interface Props { diff --git a/packages/frontend/src/components/form/ReviewForm.tsx b/packages/frontend/src/components/form/ReviewForm.tsx new file mode 100644 index 00000000..c19fb90d --- /dev/null +++ b/packages/frontend/src/components/form/ReviewForm.tsx @@ -0,0 +1,60 @@ +import { TransactionAction, Transactions } from '@/blockchain/transaction' +import { TxFlowSteps } from '../auction/TxFlowSteps' +import { useAccount, useBalance } from 'wagmi' +import { Form, FormRow } from '.' +import { formatEther } from 'viem' +import { Button } from '../buttons' +import { heading } from '../auction/AuctionTransaction' + +const amountLabel = { + [Transactions.Place]: 'Your Bid', + [Transactions.Bump]: 'Your Bid Bump', + [Transactions.Withdraw]: 'Withdraw amount', +} + +interface ReviewFormProps { + action: TransactionAction + amount: bigint + impact?: bigint + view: TxFlowSteps + setView: (state: TxFlowSteps) => void +} + +export const ReviewForm = ({ + action: { status, resetStatus, ...action }, + amount, + impact, + view, + setView, +}: ReviewFormProps) => { + const { address } = useAccount() + const etherBalance = useBalance({ address }).data?.value + const isPending = status === 'pending' + + const sendTransaction = async () => { + await action.send() + setView(view + 1) + } + + return ( +
+ + {amountLabel[action.type]} + {formatEther(amount)} ETH + + {!!impact && ( + + Your Bid after the bump + {formatEther(impact)} ETH + + )} + + Wallet Balance + {!!etherBalance && formatEther(etherBalance)} ETH + + +
+ ) +} diff --git a/packages/frontend/src/components/stepper/Stepper.tsx b/packages/frontend/src/components/stepper/Stepper.tsx index 64ebdf5c..2a4fdc1d 100644 --- a/packages/frontend/src/components/stepper/Stepper.tsx +++ b/packages/frontend/src/components/stepper/Stepper.tsx @@ -7,9 +7,10 @@ interface StepContent { description?: string } -type StepDescription = 'default' | 'failed' - -type Step = Record +interface Step { + default: StepContent + failed?: StepContent +} type Steps = Step[] @@ -63,13 +64,8 @@ const StepperList = styled.ul` margin: 0; ` -function getItemColor(props: DisplayTypeProps) { - switch (props.status) { - case 'current': - return typeToItemColor[props.type] - default: - return Colors.Black - } +function getItemColor({ status, type }: DisplayTypeProps) { + return status === 'current' ? typeToItemColor[type] : Colors.Black } const typeToItemColor: Record = { diff --git a/packages/frontend/src/components/stepper/TransactionStepper.tsx b/packages/frontend/src/components/stepper/TransactionStepper.tsx new file mode 100644 index 00000000..6cef7853 --- /dev/null +++ b/packages/frontend/src/components/stepper/TransactionStepper.tsx @@ -0,0 +1,66 @@ +import { Transactions } from '@/blockchain/transaction' +import { Stepper } from './Stepper' +import { heading } from '../auction/AuctionTransaction' +import { styled } from 'styled-components' +import { Colors } from '@/styles/colors' + +interface Props { + current: StepName + action: Transactions + isFailed: boolean +} + +export const TransactionStepper = ({ action, current, isFailed }: Props) => { + const steps = getTransactionSteps(action) + const currentStepIndex = steps.findIndex((step) => [step.default.name, step.failed?.name].includes(current)) + return ( + + Finalize {header[action]} + + + ) +} + +const header = { + [Transactions.Place]: 'Bid', + [Transactions.Bump]: 'Bid Bump', + [Transactions.Withdraw]: 'Withdraw', +} + +const description = { + [Transactions.Place]: 'Initiate and confirm bid transaction in your wallet.', + [Transactions.Bump]: 'Initiate and confirm bump transaction in your wallet.', + [Transactions.Withdraw]: 'Initiate and confirm withdraw transaction in your wallet.', +} + +const getTransactionSteps = (action: Transactions) => { + return [ + { + default: { + name: `${heading[action]}`, + description: `${description[action]}`, + }, + }, + { + default: { + name: 'Finalized', + description: 'The transaction has been confirmed on the blockchain.', + }, + failed: { + name: 'Failed', + description: 'Transaction failed.', + }, + }, + ] +} + +const StepperContainer = styled.div` + display: flex; + flex-direction: column; + width: 313px; + padding: 82px 20px 82px 0; + background-color: ${Colors.Pink}; +` +const StepperHeader = styled.h3` + margin: 0 0 24px 24px; +` diff --git a/packages/frontend/src/components/topBar/AccountButton.tsx b/packages/frontend/src/components/topBar/AccountButton.tsx index b70ba408..61b8945e 100644 --- a/packages/frontend/src/components/topBar/AccountButton.tsx +++ b/packages/frontend/src/components/topBar/AccountButton.tsx @@ -1,7 +1,7 @@ import { useAccount } from 'wagmi' import { useState } from 'react' import { Button } from '@/components/buttons/Button' -import { shortenEthAddress } from '@/utils/formatters/shortenEthAddress' +import { shortenHexString } from '@/utils/formatters/shortenHexString' import { ConnectWalletButton } from '@/components/buttons/ConnectWalletButton' import { AccountDetailModal } from '@/components/topBar/AccountDetailModal' @@ -14,7 +14,7 @@ export const AccountButton = () => { {address ? ( <> {isModalOpen && setIsModalOpen(false)} />} diff --git a/packages/frontend/src/components/topBar/AccountDetailModal.tsx b/packages/frontend/src/components/topBar/AccountDetailModal.tsx index 9805a0ae..f1e88a96 100644 --- a/packages/frontend/src/components/topBar/AccountDetailModal.tsx +++ b/packages/frontend/src/components/topBar/AccountDetailModal.tsx @@ -3,10 +3,10 @@ import { useAccount, useDisconnect } from 'wagmi' import { Colors } from '@/styles/colors' import { Button } from '@/components/buttons' import { Modal } from '@/components/topBar/Modal' -import { shortenEthAddress } from '@/utils/formatters/shortenEthAddress' +import { shortenHexString } from '@/utils/formatters/shortenHexString' import { CopyButton } from '@/components/buttons/CopyButton' import { RedirectButton } from '@/components/buttons/RedirectButton' -import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerAddressLink' +import { useExplorerAddressLink } from '@/blockchain/hooks/useExplorerLinks' export interface ModalProps { isShown: boolean | undefined @@ -32,7 +32,7 @@ export const AccountDetailModal = ({ isShown, onRequestClose }: ModalProps) => { {address && ( <> - {shortenEthAddress(address)} + {shortenHexString(address)} { const { address } = useAccount() const [view, setView] = useState(TxFlowSteps.Placing) const minimumBid = useMinimumBid() const [bid, setBid] = useState('0') - const { bidList } = useBids() + const parsedBid = useMemo(() => parseEther(bid || '0'), [bid]) + const bidAction = usePlaceBid({ value: parsedBid, score: BigInt(20), proof: '0x' }) + useEffect(() => setTransactionViewLock(bidAction.status !== 'idle'), [bidAction.status, setTransactionViewLock]) useEffect(() => setView(TxFlowSteps.Placing), [address]) - - useEffect(() => { - setBid(formatEther(minimumBid)) - }, [minimumBid]) - - const parsedBid = useMemo(() => parseEther(bid || '0'), [bid]) + useEffect(() => setBid(formatEther(minimumBid)), [minimumBid]) return ( <> {view === TxFlowSteps.Placing ? ( - + ) : ( - + )} ) } - -const TransactionViewPlaceholder = () =>
diff --git a/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidForm.tsx b/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidForm.tsx index 7a8e6072..954da134 100644 --- a/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidForm.tsx +++ b/packages/frontend/src/components/userActious/bid/PlaceBid/PlaceBidForm.tsx @@ -1,22 +1,22 @@ import { TxFlowSteps } from '@/components/auction/TxFlowSteps' import { Button } from '@/components/buttons' import { Form, FormHeading, FormRow, FormWrapper, Input } from '@/components/form' -import { Bid } from '@/types/bid' import { formatEther } from 'viem' import { useAccount, useBalance } from 'wagmi' import { getPositionAfterBid } from '../getPositionAfterBid' +import { useBids } from '@/providers/BidsProvider' interface PlaceBidFormProps { bid: string parsedBid: bigint setBid: (val: string) => void minimumBid: bigint - bids: Bid[] setView: (state: TxFlowSteps) => void } -export const PlaceBidForm = ({ bid, parsedBid, setBid, minimumBid, bids, setView }: PlaceBidFormProps) => { +export const PlaceBidForm = ({ bid, parsedBid, setBid, minimumBid, setView }: PlaceBidFormProps) => { const { address } = useAccount() + const { bidList } = useBids() const userBalance = useBalance({ address }).data?.value const notEnoughBalance = userBalance !== undefined && parsedBid > userBalance const bidTooLow = parsedBid < minimumBid @@ -32,7 +32,7 @@ export const PlaceBidForm = ({ bid, parsedBid, setBid, minimumBid, bids, setView Your place in the raffle after the bid - No. {getPositionAfterBid(parsedBid, bids)} + No. {getPositionAfterBid(parsedBid, bidList)}