Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(frontend): It is ambiguous on what scale is the withdrawal and deposit input #2817

Merged
merged 8 commits into from
Jul 31, 2024
44 changes: 41 additions & 3 deletions packages/frontend/app/components/LiquidityDialog.tsx

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially, I move the input out of the form, but I notice when the user submits the form it doesn't make the native html validation and it doesn't prevent the user from submitting any amount. The validation is made succesfully until the amount reaches the backend.
I understand that what you mention is also important. What do you think? @JoblersTune

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I was hoping the error message would cover that, even more so with your added minimum specification. Can you give me an example of where it is still failing @Emanuel-Palestino?

Copy link
Contributor Author

@Emanuel-Palestino Emanuel-Palestino Jul 29, 2024

Choose a reason for hiding this comment

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

Sure. When I submit a negative value or a very small value, it submits to the backend.

  • Negative values, the error message doesn't appear and the validation is made in the backend.
    image

  • With very small value, the error message appear (although it doesn't prevent to the user from submit). The validation is made in the backend.
    image

The message displayed at the top, is made by the backend. @JoblersTune these are the examples where it fail.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Emanuel-Palestino good catch on the negative values, thanks. We do most of our validation on the backend already. It creates a simpler frontend with faster load times and less JS complexity. We want to validate our input on the backend since that is where we'll be using it, so while it's nice to have frontend, reactive and immediate feedback it can lead to slower load times and some redundancy since backend validation is required as well.

So it's fine to get a server-side error for incorrect data entries. However, let's mitigate this by just adding a frontend message for the case where the value is negative as well, to give users a clearer error where we can.

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const userInput = e.target.value
    const scaledInput = parseFloat(userInput) * Math.pow(10, asset.scale)
    const integerScaledInput = Math.floor(scaledInput)
    if (scaledInput < 0) {
      const error = 'The amount should be a positive value'
      setErrorMessage(error)
    } else if (scaledInput !== integerScaledInput) {
      const error = 'The asset scale cannot accomodate this value'
      setErrorMessage(error)
    } else {
      setErrorMessage('')
    }
    setActualAmount(integerScaledInput)
  }

And then keep that input value above the form element.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perfect, I'll make the changes c:

Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
import { Dialog } from '@headlessui/react'
import { Form } from '@remix-run/react'
import type { ChangeEvent } from 'react'
import { useState } from 'react'
import { XIcon } from '~/components/icons'
import { Button, Input } from '~/components/ui'

type BasicAsset = {
code: string
scale: number
}

type LiquidityDialogProps = {
title: string
onClose: () => void
type: 'Deposit' | 'Withdraw'
asset: BasicAsset
}

export const LiquidityDialog = ({
title,
onClose,
type
type,
asset
}: LiquidityDialogProps) => {
const [actualAmount, setActualAmount] = useState<number>(0)
const [errorMessage, setErrorMessage] = useState<string>('')

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const userInput = e.target.value
const scaledInput = parseFloat(userInput) * Math.pow(10, asset.scale)
const integerScaledInput = Math.floor(scaledInput)
if (scaledInput < 0) {
const error = 'The amount should be a positive value'
setErrorMessage(error)
} else if (scaledInput !== integerScaledInput) {
const error = 'The asset scale cannot accomodate this value'
setErrorMessage(error)
} else {
setErrorMessage('')
}
setActualAmount(integerScaledInput)
}

return (
<Dialog as='div' className='relative z-10' onClose={onClose} open={true}>
<div className='fixed inset-0 bg-tealish/30 bg-opacity-75 transition-opacity' />
Expand All @@ -38,13 +66,23 @@ export const LiquidityDialog = ({
{title}
</Dialog.Title>
<div className='mt-2'>
<Input
required
type='number'
name='displayAmount'
label='Amount'
onChange={handleChange}
addOn={asset.code}
step='any'
error={errorMessage}
/>
<Form method='post' replace preventScrollReset>
<Input
required
min={1}
type='number'
type='hidden'
name='amount'
label='Amount'
value={actualAmount}
/>
<div className='flex justify-end py-3'>
<Button aria-label={`${type} liquidity`} type='submit'>
Expand Down
27 changes: 22 additions & 5 deletions packages/frontend/app/routes/assets.$assetId.deposit-liquidity.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
import { type ActionFunctionArgs } from '@remix-run/node'
import { useNavigate } from '@remix-run/react'
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { useLoaderData, useNavigate } from '@remix-run/react'
import { v4 } from 'uuid'
import { LiquidityDialog } from '~/components/LiquidityDialog'
import { depositAssetLiquidity } from '~/lib/api/asset.server'
import { depositAssetLiquidity, getAssetInfo } from '~/lib/api/asset.server'
import { messageStorage, setMessageAndRedirect } from '~/lib/message.server'
import { amountSchema } from '~/lib/validate.server'
import { redirectIfUnauthorizedAccess } from '../lib/kratos_checks.server'
import { type LoaderFunctionArgs } from '@remix-run/node'
import { z } from 'zod'

export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const cookies = request.headers.get('cookie')
await redirectIfUnauthorizedAccess(request.url, cookies)
return null

const assetId = params.assetId

const result = z.string().uuid().safeParse(assetId)
if (!result.success) {
throw json(null, { status: 400, statusText: 'Invalid asset ID.' })
}

const asset = await getAssetInfo({ id: result.data })

if (!asset) {
throw json(null, { status: 404, statusText: 'Asset not found.' })
}

return json({ asset })
}

export default function AssetDepositLiquidity() {
const navigate = useNavigate()
const dismissDialog = () => navigate('..', { preventScrollReset: true })
const { asset } = useLoaderData<typeof loader>()

return (
<LiquidityDialog
onClose={dismissDialog}
title='Deposit asset liquidity'
type='Deposit'
asset={{ code: asset.code, scale: asset.scale }}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
import { type ActionFunctionArgs } from '@remix-run/node'
import { useNavigate } from '@remix-run/react'
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { useLoaderData, useNavigate } from '@remix-run/react'
import { v4 } from 'uuid'
import { LiquidityDialog } from '~/components/LiquidityDialog'
import { withdrawAssetLiquidity } from '~/lib/api/asset.server'
import { getAssetInfo, withdrawAssetLiquidity } from '~/lib/api/asset.server'
import { messageStorage, setMessageAndRedirect } from '~/lib/message.server'
import { amountSchema } from '~/lib/validate.server'
import { redirectIfUnauthorizedAccess } from '~/lib/kratos_checks.server'
import { type LoaderFunctionArgs } from '@remix-run/node'
import { z } from 'zod'

export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const cookies = request.headers.get('cookie')
await redirectIfUnauthorizedAccess(request.url, cookies)
return null

const assetId = params.assetId

const result = z.string().uuid().safeParse(assetId)
if (!result.success) {
throw json(null, { status: 400, statusText: 'Invalid asset ID.' })
}

const asset = await getAssetInfo({ id: result.data })

if (!asset) {
throw json(null, { status: 404, statusText: 'Asset not found.' })
}

return json({ asset })
}

export default function AssetWithdrawLiquidity() {
const navigate = useNavigate()
const dismissDialog = () => navigate('..', { preventScrollReset: true })
const { asset } = useLoaderData<typeof loader>()

return (
<LiquidityDialog
onClose={dismissDialog}
title='Withdraw asset liquidity'
type='Withdraw'
asset={{ code: asset.code, scale: asset.scale }}
/>
)
}
Expand Down
27 changes: 22 additions & 5 deletions packages/frontend/app/routes/peers.$peerId.deposit-liquidity.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
import { type ActionFunctionArgs } from '@remix-run/node'
import { useNavigate } from '@remix-run/react'
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { useLoaderData, useNavigate } from '@remix-run/react'
import { v4 } from 'uuid'
import { LiquidityDialog } from '~/components/LiquidityDialog'
import { depositPeerLiquidity } from '~/lib/api/peer.server'
import { depositPeerLiquidity, getPeer } from '~/lib/api/peer.server'
import { messageStorage, setMessageAndRedirect } from '~/lib/message.server'
import { amountSchema } from '~/lib/validate.server'
import { redirectIfUnauthorizedAccess } from '../lib/kratos_checks.server'
import { type LoaderFunctionArgs } from '@remix-run/node'
import { z } from 'zod'

export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const cookies = request.headers.get('cookie')
await redirectIfUnauthorizedAccess(request.url, cookies)
return null

const peerId = params.peerId

const result = z.string().uuid().safeParse(peerId)
if (!result.success) {
throw json(null, { status: 400, statusText: 'Invalid peer ID.' })
}

const peer = await getPeer({ id: result.data })

if (!peer) {
throw json(null, { status: 400, statusText: 'Peer not found.' })
}

return json({ asset: peer.asset })
}

export default function PeerDepositLiquidity() {
const navigate = useNavigate()
const dismissDialog = () => navigate('..', { preventScrollReset: true })
const { asset } = useLoaderData<typeof loader>()

return (
<LiquidityDialog
onClose={dismissDialog}
title='Deposit peer liquidity'
type='Deposit'
asset={{ code: asset.code, scale: asset.scale }}
/>
)
}
Expand Down
27 changes: 22 additions & 5 deletions packages/frontend/app/routes/peers.$peerId.withdraw-liquidity.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
import { type ActionFunctionArgs } from '@remix-run/node'
import { useNavigate } from '@remix-run/react'
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { useLoaderData, useNavigate } from '@remix-run/react'
import { v4 } from 'uuid'
import { LiquidityDialog } from '~/components/LiquidityDialog'
import { withdrawPeerLiquidity } from '~/lib/api/peer.server'
import { getPeer, withdrawPeerLiquidity } from '~/lib/api/peer.server'
import { messageStorage, setMessageAndRedirect } from '~/lib/message.server'
import { amountSchema } from '~/lib/validate.server'
import { redirectIfUnauthorizedAccess } from '../lib/kratos_checks.server'
import { type LoaderFunctionArgs } from '@remix-run/node'
import { z } from 'zod'

export const loader = async ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const cookies = request.headers.get('cookie')
await redirectIfUnauthorizedAccess(request.url, cookies)
return null

const peerId = params.peerId

const result = z.string().uuid().safeParse(peerId)
if (!result.success) {
throw json(null, { status: 400, statusText: 'Invalid peer ID.' })
}

const peer = await getPeer({ id: result.data })

if (!peer) {
throw json(null, { status: 400, statusText: 'Peer not found.' })
}

return json({ asset: peer.asset })
}

export default function PeerWithdrawLiquidity() {
const navigate = useNavigate()
const dismissDialog = () => navigate('..', { preventScrollReset: true })
const { asset } = useLoaderData<typeof loader>()

return (
<LiquidityDialog
onClose={dismissDialog}
title='Withdraw peer liquidity'
type='Withdraw'
asset={{ code: asset.code, scale: asset.scale }}
/>
)
}
Expand Down
Loading