Skip to content

Commit

Permalink
Implement rate expired view
Browse files Browse the repository at this point in the history
  • Loading branch information
WRadoslaw committed Feb 28, 2024
1 parent 3210b94 commit 83723bf
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 118 deletions.
42 changes: 27 additions & 15 deletions packages/atlas/src/components/ChangeNowModal/ChangeNowModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLayoutEffect, useRef, useState } from 'react'
import { useLayoutEffect, useMemo, useRef, useState } from 'react'

import { SvgAlertsInformative32 } from '@/assets/icons'
import { SwapExpired } from '@/components/ChangeNowModal/steps/SwapExpired'
import { DialogButtonProps } from '@/components/_overlays/Dialog'
import { DialogModal } from '@/components/_overlays/DialogModal'

Expand All @@ -16,7 +17,7 @@ type ChangeNowModalProps = {
}

export const ChangeNowModal = ({ type, onClose }: ChangeNowModalProps) => {
const [step, setStep] = useState(ChangeNowModalStep.INFO)
const [step, setStep] = useState(ChangeNowModalStep.PROGRESS)
const [primaryButtonProps, setPrimaryButtonProps] = useState<DialogButtonProps>({ text: 'Select wallet' }) // start with sensible default so that there are no jumps after first effect runs
const formData = useRef<FormData | null>(null)

Expand All @@ -27,18 +28,28 @@ export const ChangeNowModal = ({ type, onClose }: ChangeNowModalProps) => {
onClick: () => setStep(ChangeNowModalStep.FORM),
})
}

if (step === ChangeNowModalStep.SWAP_EXPIRED) {
setPrimaryButtonProps({
text: 'Try again',
onClick: () => setStep(ChangeNowModalStep.FORM),
})
}
}, [step, type])

const secondaryButton =
step === ChangeNowModalStep.INFO
? {
text: 'Cancel',
onClick: () => onClose(),
}
: {
text: 'Back',
onClick: () => setStep((prev) => prev - 1),
}
const secondaryButton = useMemo(() => {
if (ChangeNowModalStep.INFO || ChangeNowModalStep.SWAP_EXPIRED) {
return {
text: 'Cancel',
onClick: () => onClose(),
}
}

return {
text: 'Back',
onClick: () => setStep((prev) => prev - 1),
}
}, [onClose])

const commonProps = {
setPrimaryButtonProps,
Expand All @@ -49,7 +60,7 @@ export const ChangeNowModal = ({ type, onClose }: ChangeNowModalProps) => {
return (
<DialogModal
title={
type === 'topup' && step === ChangeNowModalStep.INFO ? (
(type === 'topup' && step === ChangeNowModalStep.INFO) || step === ChangeNowModalStep.SWAP_EXPIRED ? (
<SvgAlertsInformative32 />
) : type === 'sell' ? (
'Cashout JOY'
Expand All @@ -58,8 +69,8 @@ export const ChangeNowModal = ({ type, onClose }: ChangeNowModalProps) => {
)
}
show
dividers={step !== ChangeNowModalStep.INFO}
onExitClick={() => undefined}
dividers={![ChangeNowModalStep.INFO, ChangeNowModalStep.SWAP_EXPIRED].includes(step)}
onExitClick={step === ChangeNowModalStep.SWAP_EXPIRED ? undefined : () => undefined}
primaryButton={primaryButtonProps}
secondaryButton={secondaryButton}
>
Expand All @@ -78,6 +89,7 @@ export const ChangeNowModal = ({ type, onClose }: ChangeNowModalProps) => {
<SummaryStep {...commonProps} formData={formData.current} />
)}
{step === ChangeNowModalStep.PROGRESS && <ProgressStep />}
{step === ChangeNowModalStep.SWAP_EXPIRED && <SwapExpired />}
</DialogModal>
)
}
196 changes: 100 additions & 96 deletions packages/atlas/src/components/ChangeNowModal/steps/FormStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,68 @@ type FormStepProps = {
initialValues: FormData | null
} & CommonProps

export const FormStep = ({ goToStep, setPrimaryButtonProps, onSubmit, type, initialValues }: FormStepProps) => {
export const FormStep = ({ setPrimaryButtonProps, onSubmit, type, initialValues }: FormStepProps) => {
const { displaySnackbar } = useSnackbar()
const [isLoadingRate, setIsLoadingRate] = useState<'to' | 'from' | null>(null)
const { data, isLoading } = useQuery('changenow-currency', () => changeNowService.getAvailableCurrencies())
const debouncedExchangeEstimation = useRef<DebouncedFunc<
(amount: number, type: 'from' | 'to') => Promise<void>
> | null>(null)
const { data } = useQuery('changenow-currency', () => changeNowService.getAvailableCurrencies(), {
onSuccess: (data) => {
const currencyOptions = data.map((curr) => ({
...curr,
value: curr.legacyTicker,
label: curr.ticker.toUpperCase(),
caption: curr.name,
nodeStart: curr.image ? <img src={curr.image} alt={curr.ticker} /> : <SvgJoyTokenPrimary16 />,
}))
debouncedExchangeEstimation.current = debounce(async (amount: number, direction: 'from' | 'to') => {
const isDirectionFrom = direction === 'from'
setIsLoadingRate(isDirectionFrom ? 'to' : 'from')
const formLegacyTicker = watch(type === 'sell' ? 'to.currency' : 'from.currency')
const currency = currencyOptions?.filter((currency) => formLegacyTicker === currency.legacyTicker)?.[0]
if (!currency) {
setIsLoadingRate(null)
return
}
try {
const { data } = await changeNowService.getEstimatedExchangeAmount(
amount,
currency,
type === 'sell' ? 'sell' : 'buy',
isDirectionFrom ? 'direct' : 'reverse'
)

const { toAmount, fromAmount } = data
setValue(isDirectionFrom ? 'to.amount' : 'from.amount', isDirectionFrom ? toAmount : fromAmount)
setValue('estimatedArrival', data.transactionSpeedForecast)
setValue('rateId', data.rateId)
setValue('validUntil', data.validUntil)
} catch (e) {
if (isAxiosError(e) && e.response?.data.message && e.response.status === 400) {
setError(`${direction}`, {
message: changeNowService.sanitizeApiErrorMessage(e.response.data.message),
type: 'custom',
})
return
}
SentryLogger.error('Failed to get reate estimation: ', 'ChangeNowModal:FormStep', e)

displaySnackbar({
title: 'Failed to get rate estimation',
iconType: 'error',
})
} finally {
setIsLoadingRate(null)
}
}, 500)

const hasInitialValues = !!initialValues
if (hasInitialValues && debouncedExchangeEstimation.current) {
debouncedExchangeEstimation.current(initialValues?.from.amount, 'from')
}
},
})

const {
control,
Expand Down Expand Up @@ -76,14 +131,6 @@ export const FormStep = ({ goToStep, setPrimaryButtonProps, onSubmit, type, init
})
})

const hasInitialValues = !!initialValues
useEffect(() => {
if (hasInitialValues && debouncedExchangeEstimation.current) {
console.log('calc')
debouncedExchangeEstimation.current(initialValues?.from.amount, 'from')
}
}, [hasInitialValues, initialValues?.from.amount])

const currencyOptions = useMemo(() => {
return data?.map((curr) => ({
...curr,
Expand All @@ -94,51 +141,6 @@ export const FormStep = ({ goToStep, setPrimaryButtonProps, onSubmit, type, init
}))
}, [data])

useEffect(() => {
if (currencyOptions) {
debouncedExchangeEstimation.current = debounce(async (amount: number, direction: 'from' | 'to') => {
const isDirectionFrom = direction === 'from'
setIsLoadingRate(isDirectionFrom ? 'to' : 'from')
const formLegacyTicker = watch(type === 'sell' ? 'to.currency' : 'from.currency')
const currency = currencyOptions?.filter((currency) => formLegacyTicker === currency.legacyTicker)?.[0]
if (!currency) {
setIsLoadingRate(null)
return
}
try {
const { data } = await changeNowService.getEstimatedExchangeAmount(
amount,
currency,
type === 'sell' ? 'sell' : 'buy',
isDirectionFrom ? 'direct' : 'reverse'
)

const { toAmount, fromAmount } = data
setValue(isDirectionFrom ? 'to.amount' : 'from.amount', isDirectionFrom ? toAmount : fromAmount)
setValue('estimatedArrival', data.transactionSpeedForecast)
setValue('rateId', data.rateId)
setValue('validUntil', data.validUntil)
} catch (e) {
if (isAxiosError(e) && e.response?.data.message && e.response.status === 400) {
setError(`${direction}`, {
message: changeNowService.sanitizeApiErrorMessage(e.response.data.message),
type: 'custom',
})
return
}
SentryLogger.error('Failed to get reate estimation: ', 'ChangeNowModal:FormStep', e)

displaySnackbar({
title: 'Failed to get rate estimation',
iconType: 'error',
})
} finally {
setIsLoadingRate(null)
}
}, 500)
}
}, [currencyOptions, displaySnackbar, setError, setValue, type, watch])

useEffect(() => {
if (initialValues?.validUntil && new Date(initialValues.validUntil).getTime() < Date.now() && currencyOptions) {
debouncedExchangeEstimation.current?.(initialValues.from.amount, 'from')
Expand Down Expand Up @@ -187,48 +189,50 @@ export const FormStep = ({ goToStep, setPrimaryButtonProps, onSubmit, type, init
}}
/>

<Controller
name="to"
control={control}
render={({ field: { value, onChange } }) => {
return (
<FormField label="Receiving" error={errors.to?.message}>
<CurrencyInput
placeholder="10"
disabled={isLoadingRate === 'to'}
currencies={currencyOptions}
isLoading={isLoadingRate === 'to'}
initialCurrency={
initialValues?.to.currency && currencyOptions
? currencyOptions.find((curr) => curr.legacyTicker === initialValues.to.currency)
: undefined
}
value={value.amount}
lockedCurrency={type === 'buy' ? JOYSTREAM_CHANGENOW_LEGACY_TICKER : undefined}
onChange={(amount) => {
onChange({ ...value, amount })
clearErrors()
if (amount) {
debouncedExchangeEstimation.current?.(amount, 'to')
}
}}
onCurrencySelect={(currency) => {
onChange({ ...value, currency })
clearErrors()
if (value.amount) {
debouncedExchangeEstimation.current?.(value.amount, 'to')
<FlexBox flow="column" gap={4}>
<Controller
name="to"
control={control}
render={({ field: { value, onChange } }) => {
return (
<FormField label="Receiving" error={errors.to?.message}>
<CurrencyInput
placeholder="10"
disabled={isLoadingRate === 'to'}
currencies={currencyOptions}
isLoading={isLoadingRate === 'to'}
initialCurrency={
initialValues?.to.currency && currencyOptions
? currencyOptions.find((curr) => curr.legacyTicker === initialValues.to.currency)
: undefined
}
}}
/>
</FormField>
)
}}
/>
{from.currency && to.currency && (
<Text variant="t200" as="p" color={isLoadingRate ? 'colorTextMuted' : 'colorText'}>
Estimated rate: 1 {from.currency.toUpperCase()} ~ {to.amount / from.amount} {to.currency.toUpperCase()}
</Text>
)}
value={value.amount}
lockedCurrency={type === 'buy' ? JOYSTREAM_CHANGENOW_LEGACY_TICKER : undefined}
onChange={(amount) => {
onChange({ ...value, amount })
clearErrors()
if (amount) {
debouncedExchangeEstimation.current?.(amount, 'to')
}
}}
onCurrencySelect={(currency) => {
onChange({ ...value, currency })
clearErrors()
if (value.amount) {
debouncedExchangeEstimation.current?.(value.amount, 'to')
}
}}
/>
</FormField>
)
}}
/>
{from.currency && to.currency && (
<Text variant="t200" as="p" color={isLoadingRate ? 'colorTextMuted' : 'colorText'}>
Estimated rate: 1 {from.currency.toUpperCase()} ~ {to.amount / from.amount} {to.currency.toUpperCase()}
</Text>
)}
</FlexBox>
</FlexBox>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const successText = (
)

export const ProgressStep = () => {
const txStep = 3
const txStep = 0
const [, txDescription] = steps[txStep] ?? []
return (
<FlexBox gap={6} flow="column">
Expand Down
18 changes: 12 additions & 6 deletions packages/atlas/src/components/ChangeNowModal/steps/SummaryStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query'

import { SvgActionArrowRight, SvgJoyTokenPrimary24 } from '@/assets/icons'
import { CommonProps } from '@/components/ChangeNowModal/steps/types'
import { ChangeNowModalStep, CommonProps } from '@/components/ChangeNowModal/steps/types'
import { FlexBox } from '@/components/FlexBox'
import { Text } from '@/components/Text'
import { TextButton } from '@/components/_buttons/Button'
Expand All @@ -22,10 +22,10 @@ export type SummaryStepProps = {
formData: FormData
} & CommonProps

export const SummaryStep = ({ formData, setPrimaryButtonProps }: SummaryStepProps) => {
export const SummaryStep = ({ formData, setPrimaryButtonProps, goToStep }: SummaryStepProps) => {
const [termsAccepted, setTermsAccepted] = useState(false)
const [error, setError] = useState('')
const [timeDiff, setTimeDiff] = useState(0)
const [timeDiff, setTimeDiff] = useState<number | undefined>(undefined)
const { activeMembership } = useUser()
const { data: currencies } = useQuery('changenow-currency', () => changeNowService.getAvailableCurrencies())
const { estimatedArrival, to, from, rateId, validUntil } = formData

Check warning on line 31 in packages/atlas/src/components/ChangeNowModal/steps/SummaryStep.tsx

View workflow job for this annotation

GitHub Actions / Tests and Linting (ubuntu-latest, 18.x)

'rateId' is assigned a value but never used. Allowed unused vars must match /^_+$/u
Expand All @@ -35,13 +35,19 @@ export const SummaryStep = ({ formData, setPrimaryButtonProps }: SummaryStepProp
text: 'Next',
onClick: () => {
if (termsAccepted) {
if (validUntil) {
const timeDiff = getTimeDiffInSeconds(new Date(validUntil))
if (timeDiff < 10) {
goToStep(ChangeNowModalStep.SWAP_EXPIRED)
}
}
// transction
} else {
setError('You have to accept terms of service')
}
},
})
}, [setPrimaryButtonProps, termsAccepted])
}, [goToStep, setPrimaryButtonProps, termsAccepted, validUntil])

useMountEffect(() => {
if (!validUntil) {
Expand Down Expand Up @@ -104,8 +110,8 @@ export const SummaryStep = ({ formData, setPrimaryButtonProps }: SummaryStepProp
<Text variant="t200" as="p" color="colorText">
Rate valid for
</Text>
<Text variant="t200" as="p">
{formatDurationShort(timeDiff)}
<Text variant="t200" as="p" color={timeDiff !== undefined && timeDiff < 10 ? 'colorTextError' : undefined}>
{timeDiff !== undefined ? formatDurationShort(timeDiff) : '-'}
</Text>
</FlexBox>

Expand Down
15 changes: 15 additions & 0 deletions packages/atlas/src/components/ChangeNowModal/steps/SwapExpired.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FlexBox } from '@/components/FlexBox'
import { Text } from '@/components/Text'

export const SwapExpired = () => {
return (
<FlexBox flow="column">
<Text variant="h500" as="h3">
Swap time expired
</Text>
<Text variant="t200" as="p" color="colorText">
Your swap could not be completed because the session timed out. You can try again or cancel
</Text>
</FlexBox>
)
}
Loading

0 comments on commit 83723bf

Please sign in to comment.