Skip to content

Commit

Permalink
Feat: Added specified error toasts, and improved button states
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew718PLTS committed Jan 13, 2025
1 parent 6605ed3 commit 0cb5dfa
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 75 deletions.
77 changes: 59 additions & 18 deletions src/components/buttons/3DButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,80 @@ import { PropsWithChildren } from 'react'

type BaseButtonProps = {
onClick?: () => void
error?: boolean
fullWidth?: boolean
type?: 'button' | 'submit' | 'reset'
isError?: boolean
isFullWidth?: boolean
isDisabled?: boolean
isAccountReady?: boolean
}

export const Button3D = ({ children, ...restProps }: PropsWithChildren<BaseButtonProps>) => {
return <_3DButtonLink {...restProps}>{children}</_3DButtonLink>
}

const _3DButtonLink = ({
export const Button3D = ({
children,
error,
fullWidth,
onClick,
type,
type = 'button',
isError,
isFullWidth,
isDisabled,
isAccountReady,
}: PropsWithChildren<BaseButtonProps>) => {
return (
<button className={fullWidth ? 'w-full' : ''} onClick={onClick} type={type ?? 'button'}>
<button
className={isFullWidth ? 'w-full' : ''}
onClick={onClick}
type={type}
disabled={isDisabled}
>
<span
className={`group font-inter outline-offset-4 cursor-pointer ${
error ? 'bg-[#863636]' : 'bg-[#2A326A]'
} ${
fullWidth ? 'w-full ' : ''
className={`group font-inter outline-offset-4 cursor-pointer ${getSubstrateButtonColor({
isDisabled,
isAccountReady,
isError,
})} ${
isFullWidth ? 'w-full ' : ''
} border-b rounded-lg border-primary-dark font-medium select-none inline-block`}
>
<span
className={`${'pr-10'} pl-10 group-active:-translate-y-[2px] block py-[18px] transition-transform delay-[250] hover:-translate-y-[6px] -translate-y-[4px] font-medium text-[15px] border rounded-lg border-primary-dark leading-5 ${
error ? 'bg-[#E14F4F] text-white' : 'bg-[#4D62F0] text-white '
} ${fullWidth ? 'w-full flex items-center justify-center' : ''} `}
className={`pr-10 pl-10 group-active:-translate-y-[2px] block py-[18px] transition-transform delay-[250] hover:translate-y-[${
isDisabled ? '-4px' : '6px'
}] -translate-y-[4px] font-medium text-[15px] border rounded-lg border-primary-dark leading-5 ${getButtonColor(
{
isDisabled,
isAccountReady,
isError,
}
)} ${isFullWidth ? 'w-full flex items-center justify-center' : ''} `}
>
<span className="flex items-center">{children}</span>
</span>
</span>
</button>
)
}

function getSubstrateButtonColor({ isDisabled, isAccountReady, isError }: IGetButtonColorArgs) {
switch (true) {
case isDisabled:
return 'bg-[#666666]'
case isError && isAccountReady:
return 'bg-[#863636]'
default:
return 'bg-[#2A326A]'
}
}

function getButtonColor({ isDisabled, isAccountReady, isError }: IGetButtonColorArgs) {
switch (true) {
case isDisabled:
return 'bg-[#888888] text-white cursor-not-allowed'
case isError && isAccountReady:
return 'bg-[#E14F4F] text-white'
default:
return 'bg-[#4D62F0] text-white '
}
}

interface IGetButtonColorArgs {
isAccountReady: boolean | undefined
isDisabled: boolean | undefined
isError: boolean | undefined
}
10 changes: 10 additions & 0 deletions src/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const Tooltip = ({ text, dataTestId = 'tooltipText' }: TooltipProps) => (
<div className="absolute bottom-[-150%] transform -translate-x-[30%] bg-[rgb(29,29,32)] text-white text-xs rounded-md px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<span datatest-id={dataTestId}>{text}</span>
</div>
)

interface TooltipProps {
text: string
dataTestId?: string
}
4 changes: 2 additions & 2 deletions src/config/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ export function getTokenOptionsByChainId(chainId: ChainId): TokenId[] {
: []
}

export function getTokenById(id: string): Token | null {
return Tokens[id as TokenId] || null
export function getTokenById(id: TokenId | string): Token {
return Tokens[id as TokenId]
}

export function getTokenAddress(id: TokenId, chainId: ChainId): Address {
Expand Down
34 changes: 26 additions & 8 deletions src/features/swap/SwapConfirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import mentoLoaderBlue from 'src/animations/Mentoloader_blue.json'
import mentoLoaderGreen from 'src/animations/Mentoloader_green.json'
import { toastToYourSuccess } from 'src/components/TxSuccessToast'
import { Button3D } from 'src/components/buttons/3DButton'
import { Tooltip } from 'src/components/tooltip/Tooltip'
import { TokenId, Tokens } from 'src/config/tokens'
import { useAppDispatch, useAppSelector } from 'src/features/store/hooks'
import { setConfirmView, setFormValues } from 'src/features/swap/swapSlice'
Expand All @@ -19,6 +20,7 @@ import { FloatingBox } from 'src/layout/FloatingBox'
import { Modal } from 'src/layout/Modal'
import { fromWeiRounded, getAdjustedAmount, toSignificant } from 'src/utils/amount'
import { logger } from 'src/utils/logger'
import { truncateTextByLength } from 'src/utils/string'
import { useAccount, useChainId } from 'wagmi'

interface Props {
Expand Down Expand Up @@ -230,7 +232,7 @@ export function SwapConfirmCard({ formValues }: Props) {
</div>

<div className="flex w-full px-6 pb-6 mt-6">
<Button3D fullWidth onClick={onSubmit}>
<Button3D isFullWidth onClick={onSubmit}>
Swap
</Button3D>
</div>
Expand All @@ -253,9 +255,29 @@ interface SwapConfirmSummaryProps {
}

export function SwapConfirmSummary({ from, to, rate }: SwapConfirmSummaryProps) {
const optimalAmountLength = 8
const fromToken = Tokens[from.token]
const toToken = Tokens[to.token]

const handleAmount = (amount: string) => {
const shouldTruncate = amount.length > optimalAmountLength
const displayedAmount = shouldTruncate
? truncateTextByLength(optimalAmountLength, amount)
: amount

return shouldTruncate ? (
// todo: Replace to module.css. Couldn't been replaced because of incorrect styles while replacing
<div className="relative text-lg font-semibold leading-6 text-center dark:text-white group">
<span data-testid="truncatedAmount"> {displayedAmount}</span>
<Tooltip text={amount}></Tooltip>
</div>
) : (
<div className="relative text-lg font-semibold leading-6 text-center dark:text-white group">
<span data-testid="fullAmount"> {displayedAmount}</span>
</div>
)
}

return (
<div className="dark:bg-[#18181B] bg-[#EFF1F3] rounded-xl mt-6 mx-6 ">
<div className="relative flex items-center gap-3 rounded-xl justify-between bg-white border border-[#E5E7E9] dark:border-transparent dark:bg-[#303033] p-[5px]">
Expand All @@ -265,20 +287,16 @@ export function SwapConfirmSummary({ from, to, rate }: SwapConfirmSummaryProps)
</div>
<div className="flex flex-col items-center flex-1 px-2">
<div className="text-sm text-center dark:text-[#AAB3B6]">{fromToken.symbol}</div>
<div className="text-lg font-semibold leading-6 text-center dark:text-white">
{toSignificant(from.amount)}
</div>
{handleAmount(toSignificant(from.amount))}
</div>
</div>
<div className=" dark:text-[#AAB3B6]">
<div className="dark:text-[#AAB3B6]">
<ChevronRight />
</div>
<div className="flex flex-1 items-center pr-3 h-[70px] bg-[#EFF1F3] dark:bg-[#18181B] rounded-lg">
<div className="flex flex-col items-center flex-1 px-2">
<div className="text-sm text-center dark:text-[#AAB3B6]">{toToken.symbol}</div>
<div className="text-lg font-semibold leading-6 text-center dark:text-white">
{toSignificant(to.amount) || '0'}
</div>
{handleAmount(toSignificant(to.amount) || '0')}
</div>
<div className="my-[15px]">
<TokenIcon size="l" token={toToken} />
Expand Down
112 changes: 80 additions & 32 deletions src/features/swap/SwapForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useConnectModal } from '@rainbow-me/rainbowkit'
import { Form, Formik, useFormikContext } from 'formik'
import { ReactNode, SVGProps, useEffect, useMemo } from 'react'
import { ReactNode, SVGProps, useCallback, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify'
import { Spinner } from 'src/components/animation/Spinner'
import { Button3D } from 'src/components/buttons/3DButton'
Expand All @@ -26,6 +26,7 @@ import { useFormValidator } from 'src/features/swap/useFormValidator'
import { useSwapQuote } from 'src/features/swap/useSwapQuote'
import { FloatingBox } from 'src/layout/FloatingBox'
import { fromWei, fromWeiRounded, toSignificant } from 'src/utils/amount'
import { debounce } from 'src/utils/debounce'
import { logger } from 'src/utils/logger'
import { escapeRegExp, inputRegex } from 'src/utils/string'
import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi'
Expand Down Expand Up @@ -72,11 +73,15 @@ function SwapForm() {
const storedFormValues = useAppSelector((s) => s.swap.formValues) // Get stored form values
const initialFormValues = storedFormValues || initialValues // Use stored values if they exist

const debouncedValidate = debounce(async (values: SwapFormValues) => {
return await validateForm(values)
}, 750)

return (
<Formik<SwapFormValues>
initialValues={initialFormValues}
onSubmit={onSubmit}
validate={validateForm}
validate={debouncedValidate}
validateOnChange={true}
validateOnBlur={false}
>
Expand All @@ -99,7 +104,7 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) {
return chain ? getTokenOptionsByChainId(chain?.id) : getTokenOptionsByChainId(Celo.chainId)
}, [chain])

const { values, setFieldValue } = useFormikContext<SwapFormValues>()
const { values, setFieldValue, setFieldError } = useFormikContext<SwapFormValues>()

const swappableTokenOptions = useMemo(() => {
return getSwappableTokenOptions(values.fromTokenId, chain ? chain?.id : Celo.chainId)
Expand All @@ -110,10 +115,10 @@ function SwapFormInputs({ balances }: { balances: AccountBalances }) {
const { isLoading, quote, rate } = useSwapQuote(amount, direction, fromTokenId, toTokenId)

useEffect(() => {
if (values.direction === 'in' && quote) {
if (values.direction === 'in' && quote && values.quote !== quote) {
setFieldValue('quote', quote)
}
}, [quote, setFieldValue, values.direction])
}, [quote, setFieldError, setFieldValue, values.direction, values.quote])

useEffect(() => {
if (chain && isConnected && !isSwappable(values.fromTokenId, values.toTokenId, chain?.id)) {
Expand Down Expand Up @@ -286,12 +291,12 @@ function SubmitButton() {
const { switchNetworkAsync } = useSwitchNetwork()
const { openConnectModal } = useConnectModal()
const dispatch = useAppDispatch()
const { errors, touched } = useFormikContext<SwapFormValues>()
const { errors, touched, values, isValidating } = useFormikContext<SwapFormValues>()

const isAccountReady = address && isConnected
const isOnCelo = chains.some((chn) => chn.id === chain?.id)

const switchToNetwork = async () => {
const switchToNetwork = useCallback(async () => {
try {
if (!switchNetworkAsync) throw new Error('switchNetworkAsync undefined')
logger.debug('Resetting and switching to Celo')
Expand All @@ -304,40 +309,83 @@ function SubmitButton() {
logger.error('Error updating network', error)
toast.error('Could not switch network, does wallet support switching?')
}
}
}, [switchNetworkAsync, dispatch])

const error =
touched.amount && (errors.amount || errors.fromTokenId || errors.toTokenId || errors.slippage)
let text

if (error) {
text = error
} else if (!isAccountReady) {
text = 'Connect Wallet'
} else if (!isOnCelo) {
text = 'Switch to Celo Network'
} else {
text = 'Continue'
}
const amountWasModified = touched.amount || values.amount
const quoteLikelyStillLoading = useMemo(
() =>
values.amount && values.quote && values.quote === '0' && errors.amount === 'Amount Required',
[values.amount, values.quote, errors.amount]
)

const type = isAccountReady ? 'submit' : 'button'
let onClick
const hasError = useMemo(() => {
if (!amountWasModified) return false
if (quoteLikelyStillLoading) return false

if (!isAccountReady) {
onClick = openConnectModal
} else if (!isOnCelo) {
onClick = switchToNetwork
}
return !!(
errors.amount ||
errors.quote ||
errors.fromTokenId ||
errors.toTokenId ||
errors.slippage
)
}, [amountWasModified, quoteLikelyStillLoading, errors])

const errorText = useMemo(
() =>
errors.amount ||
errors.quote ||
errors.fromTokenId ||
errors.toTokenId ||
errors.slippage ||
'',
[errors]
)

const showLongError = typeof error === 'string' && error?.length > 50
const showLongError = useMemo(() => errorText.length > 50, [errorText])

const buttonText = useMemo(() => {
if (!isAccountReady) return 'Connect Wallet'
if (isValidating && (!errors.amount || !quoteLikelyStillLoading)) return 'Continue'
if (!isOnCelo) return 'Switch to Celo Network'
if (hasError) return showLongError ? 'Error' : errorText
return 'Continue'
}, [
errors.amount,
errorText,
hasError,
isAccountReady,
isOnCelo,
isValidating,
quoteLikelyStillLoading,
showLongError,
])

const onClick = useMemo(() => {
if (!isAccountReady) return openConnectModal
if (!isOnCelo) return switchToNetwork
return undefined
}, [isAccountReady, isOnCelo, openConnectModal, switchToNetwork])

const buttonType = useMemo(
() => (isAccountReady && !hasError ? 'submit' : 'button'),
[isAccountReady, hasError]
)

return (
<div className="flex flex-col items-center justify-center w-full">
{showLongError ? (
<div className="bg-[#E14F4F] rounded-md text-white p-4 mb-6">{error}</div>
<div className="bg-[#E14F4F] rounded-md text-white p-4 mb-6">{errorText}</div>
) : null}
<Button3D fullWidth onClick={onClick} type={type} error={error ? true : false}>
{showLongError ? 'Error' : text}
<Button3D
isDisabled={isValidating}
isError={hasError}
isFullWidth
onClick={onClick}
type={buttonType}
isAccountReady={isAccountReady}
>
{buttonText}
</Button3D>
</div>
)
Expand Down
4 changes: 4 additions & 0 deletions src/features/swap/useFormValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ export function useFormValidator(balances: AccountBalances, lastUpdated: number
const tokenId = values.fromTokenId
const tokenBalance = balances[tokenId]
const weiAmount = toWei(parsedAmount, Tokens[values.fromTokenId].decimals)

if (weiAmount.gt(tokenBalance) && !areAmountsNearlyEqual(weiAmount, tokenBalance)) {
return { amount: 'Amount exceeds balance' }
}

const { exceeds, errorMsg } = await checkTradingLimits(values, chainId)
if (exceeds) return { amount: errorMsg }
if (values.amount && values.quote === '0') return { quote: 'Error' }

return {}
})().catch((error) => {
logger.error(error)
Expand Down
Loading

0 comments on commit 0cb5dfa

Please sign in to comment.