|
| 1 | +import { Button, Card, H1, Paragraph, XStack, YStack, SubmitButton, Stack } from '@my/ui' |
| 2 | +import { z } from 'zod' |
| 3 | +import { SchemaForm, formFields } from 'app/utils/SchemaForm' |
| 4 | +import { FormProvider, useForm } from 'react-hook-form' |
| 5 | +import type { PropsWithChildren } from 'react' |
| 6 | +import { coins, coinsDict, sendCoin } from 'app/data/coins' |
| 7 | +import { IconCoin } from 'app/components/icons/IconCoin' |
| 8 | +import { useSendAccountBalances } from 'app/utils/useSendAccountBalances' |
| 9 | +import { formatUnits } from 'viem' |
| 10 | +import formatAmount from 'app/utils/formatAmount' |
| 11 | + |
| 12 | +const removeDuplicateInString = (text: string, substring: string) => { |
| 13 | + const [first, ...after] = text.split(substring) |
| 14 | + return first + (after.length ? `${substring}${after.join('')}` : '') |
| 15 | +} |
| 16 | + |
| 17 | +export function OpenPosition() { |
| 18 | + return ( |
| 19 | + <YStack pt={'$size.3.5'} $gtLg={{ pt: 0 }} f={1} $gtMd={{ ml: '$4' }}> |
| 20 | + <YStack pb={'$size.3.5'} gap="$size.0.9"> |
| 21 | + <H1 size={'$9'} fontWeight={'900'} color="$color12" tt={'uppercase'}> |
| 22 | + Open a new position |
| 23 | + </H1> |
| 24 | + <Paragraph color={'$color10'} size={'$5'} maw={650}> |
| 25 | + Liquidity Providers get to share 1.5% of all swaps (added to your rewards automatically), |
| 26 | + and 5b in $SEND rewards over 12 months. |
| 27 | + </Paragraph> |
| 28 | + </YStack> |
| 29 | + |
| 30 | + <YStack $gtMd={{ fd: 'row' }} gap={'$size.1.5'}> |
| 31 | + <OpenPositionForm /> |
| 32 | + </YStack> |
| 33 | + </YStack> |
| 34 | + ) |
| 35 | +} |
| 36 | + |
| 37 | +const OpenPositionForm = () => { |
| 38 | + const form = useForm<z.infer<typeof OpenPositionSchema>>() |
| 39 | + const { balances } = useSendAccountBalances() |
| 40 | + |
| 41 | + const formToken = form.watch('token') |
| 42 | + const formTokenAmount = form.watch('tokenAmount') |
| 43 | + const formSendAmount = form.watch('sendAmount') |
| 44 | + |
| 45 | + const [tokenSymbol, tokenDecimals] = [ |
| 46 | + coinsDict[formToken]?.symbol, |
| 47 | + coinsDict[formToken]?.decimals, |
| 48 | + ] |
| 49 | + |
| 50 | + const sendBalance = ( |
| 51 | + balances?.SEND ? formatUnits(balances.SEND, sendCoin.decimals) : 0n |
| 52 | + ).toString() |
| 53 | + const tokenBalance = formatUnits(balances?.[tokenSymbol] ?? 0n, tokenDecimals) |
| 54 | + |
| 55 | + const canSubmit = |
| 56 | + formTokenAmount && |
| 57 | + Number(formTokenAmount) > 0 && |
| 58 | + Number(formTokenAmount) <= Number(tokenBalance) && |
| 59 | + formSendAmount && |
| 60 | + Number(formSendAmount) > 0 && |
| 61 | + Number(formSendAmount) <= Number(sendBalance) |
| 62 | + |
| 63 | + return ( |
| 64 | + <FormProvider {...form}> |
| 65 | + <SchemaForm |
| 66 | + form={form} |
| 67 | + schema={OpenPositionSchema} |
| 68 | + onSubmit={() => {}} |
| 69 | + props={{ |
| 70 | + tokenAmount: { |
| 71 | + ...sharedStyles, |
| 72 | + onChangeText: (text) => { |
| 73 | + const formattedText = removeDuplicateInString(text, '.').replace(/[^0-9.]/g, '') //remove duplicate "." then filter out any letters |
| 74 | + form.setValue('tokenAmount', formattedText) |
| 75 | + }, |
| 76 | + }, |
| 77 | + sendAmount: { |
| 78 | + ...sharedStyles, |
| 79 | + inputMode: 'numeric', |
| 80 | + onChangeText: (text) => { |
| 81 | + const formattedText = text.replace(/[^0-9]/g, '') // only accept numbers |
| 82 | + form.setValue('sendAmount', formattedText) |
| 83 | + }, |
| 84 | + }, |
| 85 | + token: { |
| 86 | + supportedCoins: coins.filter(({ symbol }) => symbol !== 'SEND'), |
| 87 | + }, |
| 88 | + }} |
| 89 | + defaultValues={{ |
| 90 | + token: 'eth', |
| 91 | + }} |
| 92 | + formProps={{ |
| 93 | + testID: 'OpenPositionForm', |
| 94 | + $gtSm: { maxWidth: '100%' }, |
| 95 | + jc: 'flex-start', |
| 96 | + als: 'flex-start', |
| 97 | + f: 1, |
| 98 | + height: '100%', |
| 99 | + }} |
| 100 | + renderAfter={({ submit }) => ( |
| 101 | + <XStack mt={'$size.3.5'}> |
| 102 | + <SubmitButton |
| 103 | + variant={!canSubmit ? 'outlined' : undefined} |
| 104 | + theme={canSubmit ? 'green' : 'dim'} |
| 105 | + borderRadius={'$4'} |
| 106 | + disabled={!canSubmit} |
| 107 | + // @TODO: submit |
| 108 | + onPress={() => console.log('press')} |
| 109 | + > |
| 110 | + <Button.Text |
| 111 | + tt={'uppercase'} |
| 112 | + color={canSubmit ? '$color1' : '$color12'} |
| 113 | + fontWeight={500} |
| 114 | + ff="$mono" |
| 115 | + > |
| 116 | + {canSubmit ? 'Provide Liquidity' : 'Enter USDC or SEND Amount'} |
| 117 | + </Button.Text> |
| 118 | + </SubmitButton> |
| 119 | + </XStack> |
| 120 | + )} |
| 121 | + > |
| 122 | + {({ token, tokenAmount, sendAmount }) => ( |
| 123 | + <YStack $gtMd={{ fd: 'row' }} gap={'$size.1.5'}> |
| 124 | + <FormCard |
| 125 | + balance={tokenBalance} |
| 126 | + onMaxPress={() => form.setValue('tokenAmount', tokenBalance)} |
| 127 | + > |
| 128 | + <Stack w={'$size.3.5'}> |
| 129 | + {coinsDict[formToken] && ( |
| 130 | + <IconCoin coin={coinsDict[formToken]} size={'$size.3.5'} /> |
| 131 | + )} |
| 132 | + </Stack> |
| 133 | + {tokenAmount} |
| 134 | + {token} |
| 135 | + </FormCard> |
| 136 | + |
| 137 | + <FormCard |
| 138 | + balance={sendBalance} |
| 139 | + onMaxPress={() => form.setValue('sendAmount', sendBalance)} |
| 140 | + > |
| 141 | + <Stack> |
| 142 | + <IconCoin coin={sendCoin} size={'$size.3.5'} /> |
| 143 | + </Stack> |
| 144 | + {sendAmount} |
| 145 | + <Paragraph fontWeight={600} fontSize={'$8'}> |
| 146 | + SEND |
| 147 | + </Paragraph> |
| 148 | + </FormCard> |
| 149 | + </YStack> |
| 150 | + )} |
| 151 | + </SchemaForm> |
| 152 | + </FormProvider> |
| 153 | + ) |
| 154 | +} |
| 155 | + |
| 156 | +const OpenPositionSchema = z.object({ |
| 157 | + token: formFields.coin, |
| 158 | + tokenAmount: formFields.text, |
| 159 | + sendAmount: formFields.text, |
| 160 | +}) |
| 161 | + |
| 162 | +const FormCard = ({ |
| 163 | + balance, |
| 164 | + onMaxPress, |
| 165 | + children, |
| 166 | +}: { |
| 167 | + balance: string |
| 168 | + onMaxPress: () => void |
| 169 | +} & PropsWithChildren) => { |
| 170 | + return ( |
| 171 | + <Card p={'$size.3.5'} flex={1}> |
| 172 | + <XStack |
| 173 | + pb="$size.0.9" |
| 174 | + borderBottomColor={'$color10'} |
| 175 | + borderBottomWidth={1} |
| 176 | + justifyContent="space-between" |
| 177 | + alignItems="center" |
| 178 | + gap="$size.0.9" |
| 179 | + > |
| 180 | + {children} |
| 181 | + </XStack> |
| 182 | + |
| 183 | + <XStack pt="$size.0.9" justifyContent="space-between"> |
| 184 | + <XStack gap="$size.0.75"> |
| 185 | + <Paragraph color="$color10" size={'$5'}> |
| 186 | + Balance: |
| 187 | + </Paragraph> |
| 188 | + <Paragraph size={'$5'} fontWeight={'600'}> |
| 189 | + {formatAmount(balance, 9, 2)} |
| 190 | + </Paragraph> |
| 191 | + </XStack> |
| 192 | + <Button unstyled onPress={onMaxPress}> |
| 193 | + <Button.Text size={'$5'} color="$color10" textDecorationLine="underline"> |
| 194 | + MAX |
| 195 | + </Button.Text> |
| 196 | + </Button> |
| 197 | + </XStack> |
| 198 | + </Card> |
| 199 | + ) |
| 200 | +} |
| 201 | + |
| 202 | +const sharedStyles = { |
| 203 | + fieldsetProps: { flex: 1 }, |
| 204 | + backgroundColor: '$color1', |
| 205 | + flex: 1, |
| 206 | + inputMode: 'decimal', |
| 207 | + fontSize: '$9', |
| 208 | + ff: '$mono', |
| 209 | + placeholder: '0', |
| 210 | + p: 0, |
| 211 | + lineHeight: '$1', |
| 212 | + borderColor: 'transparent', |
| 213 | + outlineColor: 'transparent', |
| 214 | + fontStyle: 'normal', |
| 215 | + '$theme-dark': { |
| 216 | + placeholderTextColor: '$color10', |
| 217 | + }, |
| 218 | + $sm: { |
| 219 | + w: '100%', |
| 220 | + }, |
| 221 | +} as const |
0 commit comments