Skip to content

Commit

Permalink
Simplify the logic for TransactionModal (#408)
Browse files Browse the repository at this point in the history
Ref #394

This PR simplifies the logic of opening modal windows and does a
refactor for the `TransacionModal` component. The component of the modal
type should be global and should not be rendered inside nested
components. The new logic allows to define a modal component only once.
This will make it easier to call up the deposit flow from the dashboard
and landing page. Previously `TransacionModal` used contexts by what
caused a mess in the component. To manage the state of action flow
logic, a special slice was created in the redux store.
  • Loading branch information
r-czajkowski authored May 15, 2024
2 parents e6f4ec3 + f03702b commit 6de1079
Show file tree
Hide file tree
Showing 37 changed files with 341 additions and 232 deletions.
5 changes: 3 additions & 2 deletions dapp/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from "react"
import React, { useState } from "react"
import { AnimatePresence, motion, Variants } from "framer-motion"
import { useState } from "react"
import { useLocation, useOutlet } from "react-router-dom"
import DocsDrawer from "./DocsDrawer"
import Header from "./Header"
import Sidebar from "./Sidebar"
import ModalRoot from "./ModalRoot"

const wrapperVariants: Variants = {
in: { opacity: 0, y: 48 },
Expand Down Expand Up @@ -39,6 +39,7 @@ function Layout() {
</AnimatePresence>
<Sidebar />
<DocsDrawer />
<ModalRoot />
</>
)
}
Expand Down
19 changes: 19 additions & 0 deletions dapp/src/components/ModalRoot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { ElementType } from "react"
import { useModal } from "#/hooks"
import { ModalType } from "#/types"
import TransactionModal from "../TransactionModal"

const MODALS: Record<ModalType, ElementType> = {
STAKE: TransactionModal,
UNSTAKE: TransactionModal,
} as const

export default function ModalRoot() {
const { modalType, modalProps, closeModal } = useModal()

if (!modalType) {
return null
}
const SpecificModal = MODALS[modalType]
return <SpecificModal closeModal={closeModal} {...modalProps} />
}
29 changes: 29 additions & 0 deletions dapp/src/components/ModalRoot/withBaseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { ComponentType } from "react"
import { Modal, ModalContent, ModalOverlay } from "@chakra-ui/react"
import { BaseModalProps } from "#/types"

export const MODAL_BASE_SIZE = "lg"

function withBaseModal<T extends BaseModalProps>(
WrappedModalContent: ComponentType<T>,
) {
return function ModalBase(props: T) {
const { closeModal } = props
return (
<Modal
isOpen
onClose={closeModal}
scrollBehavior="inside"
closeOnOverlayClick={false}
size={MODAL_BASE_SIZE}
>
<ModalOverlay mt="header_height" />
<ModalContent mt="modal_shift">
<WrappedModalContent {...props} />
</ModalContent>
</Modal>
)
}
}

export default withBaseModal
13 changes: 5 additions & 8 deletions dapp/src/components/TransactionModal/ActionFormModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import React, { ReactNode, useCallback, useState } from "react"
import { Box, ModalBody, ModalCloseButton, ModalHeader } from "@chakra-ui/react"
import {
useStakeFlowContext,
useTransactionContext,
useWalletContext,
} from "#/hooks"
import { useAppDispatch, useStakeFlowContext, useWalletContext } from "#/hooks"
import { ACTION_FLOW_TYPES, ActionFlowType, BaseFormProps } from "#/types"
import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import { logPromiseFailure } from "#/utils"
import { setTokenAmount } from "#/store/action-flow"
import StakeFormModal from "./ActiveStakingStep/StakeFormModal"
import UnstakeFormModal from "./ActiveUnstakingStep/UnstakeFormModal"

Expand All @@ -30,8 +27,8 @@ const FORM_DATA: Record<

function ActionFormModal({ type }: { type: ActionFlowType }) {
const { btcAccount, ethAccount } = useWalletContext()
const { setTokenAmount } = useTransactionContext()
const { initStake } = useStakeFlowContext()
const dispatch = useAppDispatch()

const [isLoading, setIsLoading] = useState(false)

Expand All @@ -55,14 +52,14 @@ function ActionFormModal({ type }: { type: ActionFlowType }) {
// TODO: Init unstake flow
if (type === ACTION_FLOW_TYPES.STAKE) await handleInitStake()

setTokenAmount({ amount: values.amount, currency: "bitcoin" })
dispatch(setTokenAmount({ amount: values.amount, currency: "bitcoin" }))
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
},
[handleInitStake, setTokenAmount, type],
[dispatch, handleInitStake, type],
)

const handleSubmitFormWrapper = useCallback(
Expand Down
11 changes: 7 additions & 4 deletions dapp/src/components/TransactionModal/ActiveFlowStep.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactElement, useEffect } from "react"
import { useModalFlowContext } from "#/hooks"
import { useActionFlowActiveStep, useActionFlowType, useModal } from "#/hooks"
import {
ACTION_FLOW_STEPS_TYPES,
ActionFlowType,
Expand All @@ -18,14 +18,17 @@ const FLOW: Record<ActionFlowType, (activeStep: number) => ReactElement> = {
}

export function ActiveFlowStep() {
const { activeStep, type, onClose } = useModalFlowContext()
const { closeModal } = useModal()
const activeStep = useActionFlowActiveStep()
const type = useActionFlowType()

const numberOfSteps = Object.keys(ACTION_FLOW_STEPS_TYPES[type]).length

useEffect(() => {
if (activeStep > numberOfSteps) {
onClose()
closeModal()
}
}, [activeStep, numberOfSteps, onClose])
}, [activeStep, closeModal, numberOfSteps])

return FLOW[type](activeStep)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useCallback } from "react"
import {
useActionFlowTokenAmount,
useAppDispatch,
useDepositBTCTransaction,
useDepositTelemetry,
useExecuteFunction,
useModalFlowContext,
useStakeFlowContext,
useToast,
useTransactionContext,
useWalletContext,
} from "#/hooks"
import { logPromiseFailure } from "#/utils"
Expand All @@ -17,27 +17,28 @@ import Spinner from "#/components/shared/Spinner"
import { TextMd } from "#/components/shared/Typography"
import { CardAlert } from "#/components/shared/alerts"
import { ONE_SEC_IN_MILLISECONDS } from "#/constants"
import { setStatus } from "#/store/action-flow"

const DELAY = ONE_SEC_IN_MILLISECONDS * 2
const DELAY = ONE_SEC_IN_MILLISECONDS
const TOAST_ID = TOAST_IDS.DEPOSIT_TRANSACTION_ERROR
const TOAST = TOASTS[TOAST_ID]

export default function DepositBTCModal() {
const { ethAccount } = useWalletContext()
const { tokenAmount } = useTransactionContext()
const { setStatus } = useModalFlowContext()
const tokenAmount = useActionFlowTokenAmount()
const { btcAddress, depositReceipt, stake } = useStakeFlowContext()
const depositTelemetry = useDepositTelemetry()
const { closeToast, openToast } = useToast()
const dispatch = useAppDispatch()

const onStakeBTCSuccess = useCallback(
() => setStatus(PROCESS_STATUSES.SUCCEEDED),
[setStatus],
() => dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)),
[dispatch],
)

const onStakeBTCError = useCallback(() => {
setStatus(PROCESS_STATUSES.FAILED)
}, [setStatus])
dispatch(setStatus(PROCESS_STATUSES.FAILED))
}, [dispatch])

const handleStake = useExecuteFunction(
stake,
Expand All @@ -47,10 +48,10 @@ export default function DepositBTCModal() {

const onDepositBTCSuccess = useCallback(() => {
closeToast(TOAST_ID)
setStatus(PROCESS_STATUSES.LOADING)
dispatch(setStatus(PROCESS_STATUSES.LOADING))

logPromiseFailure(handleStake())
}, [closeToast, setStatus, handleStake])
}, [closeToast, dispatch, handleStake])

const showError = useCallback(() => {
openToast({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CableWithPlugIcon, Info } from "#/assets/icons"
import { TextMd } from "#/components/shared/Typography"
import { EXTERNAL_HREF } from "#/constants"
import IconWrapper from "#/components/shared/IconWrapper"
import { MODAL_BASE_SIZE } from "#/components/shared/ModalBase"
import { MODAL_BASE_SIZE } from "#/components/ModalRoot/withBaseModal"
import {
IconBrandDiscordFilled,
IconReload,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import React, { useCallback, useState } from "react"
import {
useAppDispatch,
useExecuteFunction,
useModalFlowContext,
useStakeFlowContext,
} from "#/hooks"
import { PROCESS_STATUSES } from "#/types"
import { logPromiseFailure } from "#/utils"
import { setStatus } from "#/store/action-flow"
import ServerErrorModal from "./ServerErrorModal"
import RetryModal from "./RetryModal"
import LoadingModal from "../../LoadingModal"

export default function StakingErrorModal() {
const { setStatus } = useModalFlowContext()
const { stake } = useStakeFlowContext()
const dispatch = useAppDispatch()

const [isLoading, setIsLoading] = useState(false)
const [isServerError, setIsServerError] = useState(false)

const onStakeBTCSuccess = useCallback(
() => setStatus(PROCESS_STATUSES.SUCCEEDED),
[setStatus],
() => dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)),
[dispatch],
)

const onStakeBTCError = useCallback(() => setIsServerError(true), [])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import React, { useCallback } from "react"
import { useExecuteFunction, useModalFlowContext } from "#/hooks"
import { useAppDispatch, useExecuteFunction } from "#/hooks"
import { PROCESS_STATUSES } from "#/types"
import { Button, ModalBody, ModalFooter, ModalHeader } from "@chakra-ui/react"
import { TextMd } from "#/components/shared/Typography"
import { logPromiseFailure } from "#/utils"
import { setStatus } from "#/store/action-flow"

export default function SignMessageModal() {
const { setStatus } = useModalFlowContext()
const dispatch = useAppDispatch()

const onSignMessageSuccess = useCallback(() => {
setStatus(PROCESS_STATUSES.SUCCEEDED)
}, [setStatus])
dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED))
}, [dispatch])

// TODO: After a failed attempt, we should display the message
const onSignMessageError = useCallback(() => {
setStatus(PROCESS_STATUSES.FAILED)
}, [setStatus])
dispatch(setStatus(PROCESS_STATUSES.FAILED))
}, [dispatch])

const handleSignMessage = useExecuteFunction(
// TODO: Use a correct function from the SDK
Expand All @@ -25,13 +26,13 @@ export default function SignMessageModal() {
)

const handleSignMessageWrapper = useCallback(() => {
setStatus(PROCESS_STATUSES.LOADING)
dispatch(setStatus(PROCESS_STATUSES.LOADING))

// TODO: Remove when SDK is ready
setTimeout(() => {
logPromiseFailure(handleSignMessage())
}, 5000)
}, [setStatus, handleSignMessage])
}, [dispatch, handleSignMessage])

return (
<>
Expand Down
10 changes: 6 additions & 4 deletions dapp/src/components/TransactionModal/ModalContentWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react"
import {
useModalFlowContext,
useActionFlowStatus,
useActionFlowTokenAmount,
useActionFlowType,
useRequestBitcoinAccount,
useRequestEthereumAccount,
useTransactionContext,
useWalletContext,
} from "#/hooks"
import { BitcoinIcon, EthereumIcon } from "#/assets/icons"
Expand All @@ -23,8 +24,9 @@ export default function ModalContentWrapper({
const { btcAccount, ethAccount } = useWalletContext()
const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount()
const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount()
const { type, status } = useModalFlowContext()
const { tokenAmount } = useTransactionContext()
const status = useActionFlowStatus()
const type = useActionFlowType()
const tokenAmount = useActionFlowTokenAmount()

if (!btcAccount || !isSupportedBTCAddressType(btcAccount.address))
return (
Expand Down
6 changes: 3 additions & 3 deletions dapp/src/components/TransactionModal/SuccessModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
VStack,
} from "@chakra-ui/react"
import { LoadingSpinnerSuccessIcon } from "#/assets/icons"
import { useModalFlowContext } from "#/hooks"
import { useModal } from "#/hooks"
import { CurrencyBalanceWithConversion } from "#/components/shared/CurrencyBalanceWithConversion"
import { ACTION_FLOW_TYPES, ActionFlowType, TokenAmount } from "#/types"
import { TextMd } from "../shared/Typography"
Expand Down Expand Up @@ -66,7 +66,7 @@ type SuccessModalProps = {
}

export default function SuccessModal({ type, tokenAmount }: SuccessModalProps) {
const { onClose } = useModalFlowContext()
const { closeModal } = useModal()

const { header, footer, renderBody } = CONTENT[type]

Expand All @@ -80,7 +80,7 @@ export default function SuccessModal({ type, tokenAmount }: SuccessModalProps) {
</VStack>
</ModalBody>
<ModalFooter pt={0}>
<Button size="lg" width="100%" variant="outline" onClick={onClose}>
<Button size="lg" width="100%" variant="outline" onClick={closeModal}>
Go to dashboard
</Button>
<HStack spacing={2}>
Expand Down
Loading

0 comments on commit 6de1079

Please sign in to comment.