Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

Commit

Permalink
[PAY-1792] Add formik, zod, and proper inputs to USDC withdrawal modal (
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondjacobson authored Sep 7, 2023
1 parent 06b4b42 commit 25eb54d
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 63 deletions.
13 changes: 12 additions & 1 deletion packages/common/src/store/ui/modals/createModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const createModal = <T>({
open: (_, action: PayloadAction<T>) => {
return { ...action.payload, isOpen: true }
},
set: (state, action: PayloadAction<T>) => {
return { ...state, ...action.payload }
},
close: (state) => {
state.isOpen = 'closing'
},
Expand Down Expand Up @@ -59,7 +62,7 @@ export const createModal = <T>({
state: T & BaseModalState
) => PayloadAction<T & BaseModalState>

const { close, closed } = slice.actions
const { close, closed, set } = slice.actions
/**
* A hook that returns the state of the modal,
* an open callback that opens the modal,
Expand Down Expand Up @@ -88,9 +91,17 @@ export const createModal = <T>({
dispatch(closed())
}, [dispatch])

const setData = useCallback(
(state?: Partial<T>) => {
dispatch(set({ ...state }))
},
[dispatch]
)

return {
isOpen: isOpen === true,
data,
setData,
onOpen,
onClose,
onClosed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export enum WithdrawUSDCModalPages {

export type WithdrawUSDCModalState = {
page: WithdrawUSDCModalPages
// Completed transaction signature
signature?: string
}

const withdrawUSDCModal = createModal<WithdrawUSDCModalState>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { useWithdrawUSDCModal, WithdrawUSDCModalPages } from '@audius/common'
import {
SolanaWalletAddress,
useUSDCBalance,
useWithdrawUSDCModal,
WithdrawUSDCModalPages
} from '@audius/common'
import { Modal, ModalContent, ModalHeader } from '@audius/stems'
import { Formik } from 'formik'
import { z } from 'zod'
import { toFormikValidationSchema } from 'zod-formik-adapter'

import { ReactComponent as IconTransaction } from 'assets/img/iconTransaction.svg'
import { Icon } from 'components/Icon'
import { Text } from 'components/typography'
import { isValidSolAddress } from 'services/solana/solana'

import styles from './WithdrawUSDCModal.module.css'
import { ConfirmTransferDetails } from './components/ConfirmTransferDetails'
Expand All @@ -12,12 +21,53 @@ import { TransferInProgress } from './components/TransferInProgress'
import { TransferSuccessful } from './components/TransferSuccessful'

const messages = {
title: 'Withdraw Funds'
title: 'Withdraw Funds',
errors: {
insufficientBalance:
'Your USDC wallet does not have enough funds to cover this transaction.',
invalidAddress: 'A valid Solana USDC wallet address is required.',
pleaseConfirm:
'Please confirm you have reviewed the details and accept responsibility for any errors resulting in lost funds.'
}
}

export const AMOUNT = 'amount'
export const ADDRESS = 'address'
export const CONFIRM = 'confirm'

const WithdrawUSDCFormSchema = (userBalance: number) => {
return z.object({
[AMOUNT]: z.number().lte(userBalance, messages.errors.insufficientBalance),
[ADDRESS]: z
.string()
.refine(
(value) => isValidSolAddress(value as SolanaWalletAddress),
messages.errors.invalidAddress
),
[CONFIRM]: z.literal(true)
})
}

export const WithdrawUSDCModal = () => {
const { isOpen, onClose, onClosed, data } = useWithdrawUSDCModal()
const { page } = data
const { data: balance } = useUSDCBalance()

let formPage
switch (page) {
case WithdrawUSDCModalPages.ENTER_TRANSFER_DETAILS:
formPage = <EnterTransferDetails />
break
case WithdrawUSDCModalPages.CONFIRM_TRANSFER_DETAILS:
formPage = <ConfirmTransferDetails />
break
case WithdrawUSDCModalPages.TRANSFER_IN_PROGRESS:
formPage = <TransferInProgress />
break
case WithdrawUSDCModalPages.TRANSFER_SUCCESSFUL:
formPage = <TransferSuccessful />
break
}

return (
<Modal
Expand All @@ -39,18 +89,19 @@ export const WithdrawUSDCModal = () => {
</Text>
</ModalHeader>
<ModalContent>
{page === WithdrawUSDCModalPages.ENTER_TRANSFER_DETAILS ? (
<EnterTransferDetails />
) : null}
{page === WithdrawUSDCModalPages.CONFIRM_TRANSFER_DETAILS ? (
<ConfirmTransferDetails />
) : null}
{page === WithdrawUSDCModalPages.TRANSFER_IN_PROGRESS ? (
<TransferInProgress />
) : null}
{page === WithdrawUSDCModalPages.TRANSFER_SUCCESSFUL ? (
<TransferSuccessful />
) : null}
<Formik
initialValues={{ [AMOUNT]: balance, [ADDRESS]: '', [CONFIRM]: false }}
validationSchema={toFormikValidationSchema(
WithdrawUSDCFormSchema(balance?.toNumber() ?? 0)
)}
// TODO -- call sagas to withdraw
// Saga in turn should update modal state to advance page
onSubmit={(values) => {
console.info(values)
}}
>
{formPage}
</Formik>
</ModalContent>
</Modal>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { useCallback } from 'react'

import { WithdrawUSDCModalPages, useWithdrawUSDCModal } from '@audius/common'
import {
HarmonyButton,
HarmonyButtonSize,
HarmonyButtonType,
IconQuestionCircle,
Switch
} from '@audius/stems'
import { useField, useFormikContext } from 'formik'

import { ReactComponent as IconCaretLeft } from 'assets/img/iconCaretLeft.svg'
import { Divider } from 'components/divider'
import { Text } from 'components/typography'
import {
ADDRESS,
AMOUNT,
CONFIRM
} from 'components/withdraw-usdc-modal/WithdrawUSDCModal'

import styles from './ConfirmTransferDetails.module.css'
import { Hint } from './Hint'
Expand All @@ -29,18 +38,31 @@ const messages = {
}

export const ConfirmTransferDetails = () => {
const wallet = '72pepj'
const amount = '200'
const { submitForm } = useFormikContext()
const { setData } = useWithdrawUSDCModal()
const [{ value: amountValue }] = useField(AMOUNT)
const [{ value: addressValue }] = useField(ADDRESS)
const [confirmField, { error: confirmError }] = useField(CONFIRM)

const handleGoBack = useCallback(() => {
setData({ page: WithdrawUSDCModalPages.ENTER_TRANSFER_DETAILS })
}, [setData])

const handleContinue = useCallback(() => {
setData({ page: WithdrawUSDCModalPages.TRANSFER_IN_PROGRESS })
submitForm()
}, [setData, submitForm])

return (
<div className={styles.root}>
<div className={styles.amount}>
<TextRow left={messages.amountToWithdraw} right={`-$${amount}`} />
<TextRow left={messages.amountToWithdraw} right={`-$${amountValue}`} />
</div>
<Divider style={{ margin: 0 }} />
<div className={styles.destination}>
<TextRow left={messages.destinationAddress} />
<Text variant='body' size='medium' strength='default'>
{wallet}
{addressValue}
</Text>
</div>
<div className={styles.details}>
Expand All @@ -51,7 +73,7 @@ export const ConfirmTransferDetails = () => {
{messages.byProceeding}
</Text>
<div className={styles.acknowledge}>
<Switch checked={true} onChange={() => {}} />
<Switch {...confirmField} />
<Text variant='body' size='small' strength='default'>
{messages.haveCarefully}
</Text>
Expand All @@ -63,11 +85,14 @@ export const ConfirmTransferDetails = () => {
variant={HarmonyButtonType.SECONDARY}
size={HarmonyButtonSize.DEFAULT}
text={messages.goBack}
onClick={handleGoBack}
/>
<HarmonyButton
variant={HarmonyButtonType.SECONDARY}
size={HarmonyButtonSize.DEFAULT}
text={messages.confirm}
onClick={handleContinue}
disabled={confirmError}
/>
</div>
<Hint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import {
ChangeEventHandler,
FocusEventHandler,
useCallback,
useState
} from 'react'

import {
useUSDCBalance,
formatUSDCWeiToNumber,
formatCurrencyBalance,
BNUSDC
BNUSDC,
useWithdrawUSDCModal,
WithdrawUSDCModalPages
} from '@audius/common'
import {
HarmonyButton,
HarmonyButtonSize,
HarmonyButtonType,
IconQuestionCircle,
TokenAmountInput
IconQuestionCircle
} from '@audius/stems'
import BN from 'bn.js'
import { useField } from 'formik'

import { InputV2, InputV2Variant } from 'components/data-entry/InputV2'
import { Divider } from 'components/divider'
import { TextField } from 'components/form-fields'
import { Text } from 'components/typography'
import {
ADDRESS,
AMOUNT
} from 'components/withdraw-usdc-modal/WithdrawUSDCModal'
import {
PRECISION,
onTokenInputBlur,
onTokenInputChange
} from 'utils/tokenInput'

import styles from './EnterTransferDetails.module.css'
import { Hint } from './Hint'
Expand All @@ -27,17 +45,52 @@ const messages = {
destinationAddress: 'Destination Address',
specify: `Specify how much USDC you’d like to withdraw from your Audius Account.`,
destinationDetails: 'Provide a Solana Wallet address to transfer funds to.',
solanaWallet: 'Solana Wallet',
solanaWallet: 'USDC Wallet (Solana)',
amountInputLabel: 'Amount of USDC to withdraw',
continue: 'Continue',
notSure: `Not sure what you’re doing? Visit the help center for guides & more info.`,
guide: 'Guide to USDC Transfers on Audius'
guide: 'Guide to USDC Transfers on Audius',
dollars: '$',
usdc: '(USDC)'
}

export const EnterTransferDetails = () => {
const { data: balance } = useUSDCBalance()
const { setData } = useWithdrawUSDCModal()

const balanceNumber = formatUSDCWeiToNumber((balance ?? new BN(0)) as BNUSDC)
const balanceFormatted = formatCurrencyBalance(balanceNumber)

const [
{ value },
{ error: amountError },
{ setValue: setAmount, setTouched: setAmountTouched }
] = useField(AMOUNT)
const [humanizedValue, setHumanizedValue] = useState(
((value || balanceNumber) / 100).toFixed(PRECISION)
)
const handleAmountChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const { human, value } = onTokenInputChange(e)
setHumanizedValue(human)
setAmount(value)
},
[setAmount, setHumanizedValue]
)
const handleAmountBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
setHumanizedValue(onTokenInputBlur(e))
setAmountTouched(true)
},
[setHumanizedValue, setAmountTouched]
)

const [, { error: addressError }] = useField(ADDRESS)

const handleContinue = useCallback(() => {
setData({ page: WithdrawUSDCModalPages.CONFIRM_TRANSFER_DETAILS })
}, [setData])

return (
<div className={styles.root}>
<TextRow left={messages.currentBalance} right={`$${balanceFormatted}`} />
Expand All @@ -49,7 +102,17 @@ export const EnterTransferDetails = () => {
{messages.specify}
</Text>
</div>
<TokenAmountInput aria-label={messages.amountInputLabel} />
<TextField
title={messages.amountToWithdraw}
label={messages.amountToWithdraw}
aria-label={messages.amountToWithdraw}
name={AMOUNT}
value={humanizedValue}
onChange={handleAmountChange}
onBlur={handleAmountBlur}
startAdornment={messages.dollars}
endAdornment={messages.usdc}
/>
</div>
<Divider style={{ margin: 0 }} />
<div className={styles.destination}>
Expand All @@ -59,16 +122,21 @@ export const EnterTransferDetails = () => {
{messages.destinationDetails}
</Text>
</div>
<InputV2
variant={InputV2Variant.ELEVATED_PLACEHOLDER}
placeholder={messages.solanaWallet}
<TextField
title={messages.destinationAddress}
label={messages.solanaWallet}
aria-label={messages.destinationAddress}
name={ADDRESS}
placeholder=''
/>
</div>
<HarmonyButton
variant={HarmonyButtonType.SECONDARY}
size={HarmonyButtonSize.DEFAULT}
fullWidth
text={messages.continue}
disabled={amountError || addressError}
onClick={handleContinue}
/>
<Hint
text={messages.notSure}
Expand Down
Loading

0 comments on commit 25eb54d

Please sign in to comment.