diff --git a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx index e7db49a646..917ceb754b 100644 --- a/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx +++ b/packages/atlas/src/components/_crt/BuyMarketTokenModal/BuyMarketTokenModal.tsx @@ -1,20 +1,23 @@ import BN from 'bn.js' -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' import { useGetFullCreatorTokenQuery } from '@/api/queries/__generated__/creatorTokens.generated' +import { NumberFormat } from '@/components/NumberFormat' +import { AmmModalFormTemplate } from '@/components/_crt/AmmModalTemplates' +import { AmmModalSummaryTemplate } from '@/components/_crt/AmmModalTemplates/AmmModalSummaryTemplate' import { BuyMarketTokenSuccess } from '@/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenSuccess' import { DialogProps } from '@/components/_overlays/Dialog' import { DialogModal } from '@/components/_overlays/DialogModal' import { useMediaMatch } from '@/hooks/useMediaMatch' -import { hapiBnToTokenNumber } from '@/joystream-lib/utils' -import { useJoystream } from '@/providers/joystream' +import { hapiBnToTokenNumber, tokenNumberToHapiBn } from '@/joystream-lib/utils' +import { useFee, useJoystream, useSubscribeAccountBalance } from '@/providers/joystream' import { useSnackbar } from '@/providers/snackbars' import { useTransaction } from '@/providers/transactions/transactions.hooks' import { useUser } from '@/providers/user/user.hooks' import { calcBuyMarketPricePerToken } from '@/utils/crts' import { BuyMarketTokenConditions } from './steps/BuyMarketTokenConditions' -import { BuyMarketTokenForm } from './steps/BuyMarketTokenForm' export type BuySaleTokenModalProps = { tokenId: string @@ -25,6 +28,7 @@ export type BuySaleTokenModalProps = { enum BUY_MARKET_TOKEN_STEPS { form, conditions, + summary, success, } @@ -32,11 +36,17 @@ export const BuyMarketTokenModal = ({ tokenId, onClose, show }: BuySaleTokenModa const [activeStep, setActiveStep] = useState(BUY_MARKET_TOKEN_STEPS.form) const [primaryButtonProps, setPrimaryButtonProps] = useState() const amountRef = useRef(null) + const { control, watch, handleSubmit, reset, formState } = useForm<{ tokens: number }>() + const tokens = watch('tokens') || 0 + const { fullFee } = useFee('purchaseTokenOnMarketTx', ['1', '1', String(tokens ?? 0), '1000000']) const { data } = useGetFullCreatorTokenQuery({ variables: { id: tokenId, }, }) + const { accountBalance } = useSubscribeAccountBalance() + const title = data?.creatorTokenById?.symbol ?? 'N/A' + const currentAmm = data?.creatorTokenById?.ammCurves.find((amm) => !amm.finalized) const { memberId } = useUser() @@ -58,6 +68,13 @@ export const BuyMarketTokenModal = ({ tokenId, onClose, show }: BuySaleTokenModa text: 'Cancel', onClick: onClose, } + case BUY_MARKET_TOKEN_STEPS.summary: + return { + text: 'Back', + onClick: () => { + setActiveStep(BUY_MARKET_TOKEN_STEPS.conditions) + }, + } default: return undefined } @@ -81,11 +98,18 @@ export const BuyMarketTokenModal = ({ tokenId, onClose, show }: BuySaleTokenModa [data?.creatorTokenById] ) + const priceForAllToken = useMemo(() => { + return hapiBnToTokenNumber(calculateRequiredHapi(Math.max(tokens, 1)) ?? new BN(0)) + }, [tokens, calculateRequiredHapi]) + const pricePerUnit = priceForAllToken / (tokens || 1) + const commonProps = { setPrimaryButtonProps, } - const onSubmitConditions = useCallback(async () => { + const onTermsSubmit = useCallback(() => setActiveStep(BUY_MARKET_TOKEN_STEPS.summary), []) + + const onTransactionSubmit = useCallback(() => { const slippageAmount = calculateRequiredHapi(amountRef.current ?? 0) if (!joystream || !memberId || !amountRef.current || !slippageAmount) { return @@ -112,6 +136,121 @@ export const BuyMarketTokenModal = ({ tokenId, onClose, show }: BuySaleTokenModa }) }, [calculateRequiredHapi, displaySnackbar, handleTransaction, joystream, memberId, proxyCallback, tokenId]) + const formDetails = useMemo( + () => [ + { + title: 'Available balance', + content: , + tooltipText: 'Lorem ipsum', + }, + { + title: 'You will pay', + content: ( + + ), + tooltipText: 'Lorem ipsum', + }, + ], + [accountBalance, calculateRequiredHapi, tokens] + ) + + const summaryDetails = useMemo( + () => [ + { + title: 'Purchase', + content: ( + + ), + tooltipText: 'Lorem ipsum', + }, + { + title: 'Price per unit', + content: ( + + ), + tooltipText: 'Lorem ipsum', + }, + { + title: 'Fee', + content: , + tooltipText: 'Lorem ipsum', + }, + { + title: 'Total', + content: ( + + ), + tooltipText: 'Lorem ipsum', + }, + { + title: 'You will get', + content: ( + + ), + tooltipText: 'Lorem ipsum', + }, + ], + [calculateRequiredHapi, fullFee, pricePerUnit, title, tokens] + ) + + useEffect(() => { + if (!show) { + reset({ tokens: 0 }) + } + }, [reset, show]) + + useEffect(() => { + if (activeStep === BUY_MARKET_TOKEN_STEPS.form) { + setPrimaryButtonProps({ + text: 'Continue', + onClick: () => + handleSubmit((data) => { + amountRef.current = data.tokens + setActiveStep(BUY_MARKET_TOKEN_STEPS.conditions) + })(), + }) + } + + if (activeStep === BUY_MARKET_TOKEN_STEPS.summary) { + setPrimaryButtonProps({ + text: 'Buy token', + onClick: onTransactionSubmit, + }) + } + }, [activeStep, handleSubmit, onTransactionSubmit]) + if (!currentAmm && show) { throw new Error('BuyAmmModal invoked on token without active amm') } @@ -130,20 +269,23 @@ export const BuyMarketTokenModal = ({ tokenId, onClose, show }: BuySaleTokenModa noContentPadding={activeStep === BUY_MARKET_TOKEN_STEPS.conditions} > {activeStep === BUY_MARKET_TOKEN_STEPS.form && ( - { - setActiveStep(BUY_MARKET_TOKEN_STEPS.conditions) - amountRef.current = tokens + { + if (!value || value < 1) return 'You need to buy at least one token' + const requiredHapi = calculateRequiredHapi(value ?? 0) + if (!accountBalance || !requiredHapi) return true + return accountBalance.gte(requiredHapi) ? true : "You don't have enough balance to buy this many tokens" }} - token={data?.creatorTokenById} /> )} {activeStep === BUY_MARKET_TOKEN_STEPS.conditions && ( - + )} + {activeStep === BUY_MARKET_TOKEN_STEPS.summary && } {activeStep === BUY_MARKET_TOKEN_STEPS.success && ( { setPrimaryButtonProps({ - text: 'Buy token', + text: 'Continue', onClick: () => { if (isChecked) { onSubmit() diff --git a/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenForm.tsx b/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenForm.tsx deleted file mode 100644 index 7fc6621e0a..0000000000 --- a/packages/atlas/src/components/_crt/BuyMarketTokenModal/steps/BuyMarketTokenForm.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import BN from 'bn.js' -import { useMemo } from 'react' -import { Controller, useForm } from 'react-hook-form' - -import { FullCreatorTokenFragment } from '@/api/queries/__generated__/fragments.generated' -import { FlexBox } from '@/components/FlexBox/FlexBox' -import { Information } from '@/components/Information' -import { JoyTokenIcon } from '@/components/JoyTokenIcon' -import { NumberFormat } from '@/components/NumberFormat' -import { Text } from '@/components/Text' -import { FormField } from '@/components/_inputs/FormField' -import { TokenInput } from '@/components/_inputs/TokenInput' -import { DetailsContent } from '@/components/_nft/NftTile' -import { atlasConfig } from '@/config' -import { useMediaMatch } from '@/hooks/useMediaMatch' -import { useMountEffect } from '@/hooks/useMountEffect' -import { hapiBnToTokenNumber, tokenNumberToHapiBn } from '@/joystream-lib/utils' -import { useFee, useJoystream, useSubscribeAccountBalance } from '@/providers/joystream' - -import { CommonProps } from './types' - -type BuyMarketTokenFormProps = { - token?: FullCreatorTokenFragment | null - onSubmit: (tokens: number | null) => void - pricePerUnit: number - calculateRequiredHapi: (amount: number) => BN | undefined -} & CommonProps - -export const BuyMarketTokenForm = ({ - token, - setPrimaryButtonProps, - onSubmit, - calculateRequiredHapi, -}: BuyMarketTokenFormProps) => { - const { control, watch, handleSubmit, formState } = useForm<{ tokens: number | null }>() - const { accountBalance } = useSubscribeAccountBalance() - const tokens = watch('tokens') || 0 - const { fullFee } = useFee('purchaseTokenOnMarketTx', ['1', '1', String(tokens ?? 0), '1000000']) - const { tokenPrice } = useJoystream() - - const priceForAllToken = useMemo(() => { - return hapiBnToTokenNumber(calculateRequiredHapi(Math.max(tokens, 1)) ?? new BN(0)) - }, [tokens, calculateRequiredHapi]) - const pricePerUnit = priceForAllToken / (tokens || 1) - - const tokenInUsd = tokens * pricePerUnit * (tokenPrice ?? 0) - const smMatch = useMediaMatch('sm') - const details = useMemo( - () => [ - { - title: 'You will get', - content: ( - - ), - tooltipText: 'Lorem ipsum', - }, - { - title: 'Fee', - content: , - tooltipText: 'Lorem ipsum', - }, - { - title: 'You will spend', - content: ( - - ), - tooltipText: 'Lorem ipsum', - }, - ], - [calculateRequiredHapi, fullFee, pricePerUnit, token?.symbol, tokens] - ) - - useMountEffect(() => { - setPrimaryButtonProps({ - text: 'Continue', - onClick: () => handleSubmit((data) => onSubmit(data.tokens))(), - }) - }) - - return ( - <> - - - } - tooltipText="Lorem ipsum" - withDenomination - /> - } - tooltipText="Lorem ipsum" - withDenomination - /> - - { - if (!value || value < 1) return 'You need to buy at least one token' - if (!accountBalance) return true - const requiredHapi = tokenNumberToHapiBn((value ?? 0) * pricePerUnit) - - return accountBalance.gte(requiredHapi) ? true : "You don't have enough balance to buy this many tokens" - }, - }, - }} - render={({ field }) => ( - - } /> - - )} - /> - - - {details.map((row, i) => ( - - - - {row.title} - - - - {row.content} - - ))} - - - - ) -}