Skip to content

Commit

Permalink
dApp: Handle deposit rejection (#453)
Browse files Browse the repository at this point in the history
Closes: #432 

This pull request reverts `ResumeModal` component removal with updated
logic.
dApp now distinguishes error caused by user interruption and displays a
proper modal window to ensure the user really wants to cancel the
process.


https://github.com/thesis/acre/assets/11503879/a1fbcee1-8edc-4274-857b-1e9c94571a7d

Summary of changes:
- added paused/pending action flow states definitions
- added `useActionFlowPause` hook exposing state change handler
functions
- added `ResumeModal` component
- added `TransactionError` type definition
  • Loading branch information
kkosiorowska authored Jun 11, 2024
2 parents 1a92446 + d03424d commit b1607be
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { useCallback, useEffect } from "react"
import {
useActionFlowPause,
useActionFlowTokenAmount,
useAppDispatch,
useDepositBTCTransaction,
useExecuteFunction,
useStakeFlowContext,
useVerifyDepositAddress,
} from "#/hooks"
import { logPromiseFailure } from "#/utils"
import { PROCESS_STATUSES } from "#/types"
import { isLedgerLiveError, logPromiseFailure } from "#/utils"
import { PROCESS_STATUSES, LedgerLiveError } from "#/types"
import { Highlight } from "@chakra-ui/react"
import { TextMd } from "#/components/shared/Typography"
import { CardAlert } from "#/components/shared/alerts"
Expand All @@ -20,6 +21,7 @@ export default function DepositBTCModal() {
const { btcAddress, depositReceipt, stake } = useStakeFlowContext()
const verifyDepositAddress = useVerifyDepositAddress()
const dispatch = useAppDispatch()
const { handlePause } = useActionFlowPause()

const onStakeBTCSuccess = useCallback(() => {
dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED))
Expand All @@ -42,9 +44,22 @@ export default function DepositBTCModal() {
}, [dispatch, handleStake])

// TODO: Handle when the function fails
const showError = useCallback(() => {}, [])
const showError = useCallback((error?: LedgerLiveError) => {
console.error(error)
}, [])

const onDepositBTCError = useCallback(() => showError(), [showError])
const onDepositBTCError = useCallback(
(error: unknown) => {
if (!isLedgerLiveError(error)) return

const isInterrupted =
error.message && error.message.includes("Signature interrupted by user")
if (isInterrupted) handlePause()

showError(error)
},
[showError, handlePause],
)

const { sendBitcoinTransaction, transactionHash } = useDepositBTCTransaction(
onDepositBTCSuccess,
Expand Down
3 changes: 3 additions & 0 deletions dapp/src/components/TransactionModal/ModalContentWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ErrorModal from "./ErrorModal"
import LoadingModal from "./LoadingModal"
import MissingAccountModal from "./MissingAccountModal"
import SuccessModal from "./SuccessModal"
import ResumeModal from "./ResumeModal"

export default function ModalContentWrapper({
children,
Expand Down Expand Up @@ -43,5 +44,7 @@ export default function ModalContentWrapper({

if (status === PROCESS_STATUSES.FAILED) return <ErrorModal type={type} />

if (status === PROCESS_STATUSES.PAUSED) return <ResumeModal />

return children
}
42 changes: 42 additions & 0 deletions dapp/src/components/TransactionModal/ResumeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react"
import {
ModalHeader,
ModalBody,
ModalFooter,
Button,
HStack,
} from "@chakra-ui/react"

import Spinner from "#/components/shared/Spinner"
import { PauseIcon } from "#/assets/icons"
import { TextMd } from "#/components/shared/Typography"
import { useActionFlowPause, useModal } from "#/hooks"

export default function ResumeModal() {
const { handleResume } = useActionFlowPause()
const { closeModal } = useModal()

return (
<>
<ModalHeader pb={6} textAlign="center">
Paused
</ModalHeader>
<ModalBody textAlign="start" py={6} mx={3} gap={4}>
<HStack position="relative" justifyContent="center">
<Spinner size="2xl" />
<PauseIcon position="absolute" boxSize={6} color="brand.400" />
</HStack>

<TextMd>Are your sure you want to cancel?</TextMd>
</ModalBody>
<ModalFooter flexDirection="column" gap={2}>
<Button size="lg" width="100%" onClick={handleResume}>
Resume deposit
</Button>
<Button size="lg" width="100%" variant="outline" onClick={closeModal}>
Yes, cancel
</Button>
</ModalFooter>
</>
)
}
1 change: 1 addition & 0 deletions dapp/src/hooks/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from "./useActionFlowTxHash"
export * from "./useCompletedActivities"
export * from "./useLatestActivities"
export * from "./useAllActivitiesCount"
export * from "./useActionFlowPause"
29 changes: 29 additions & 0 deletions dapp/src/hooks/store/useActionFlowPause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { setStatus } from "#/store/action-flow"
import { PROCESS_STATUSES } from "#/types"
import { useAppDispatch } from "./useAppDispatch"

/**
* Custom hook that provides functions to pause and resume the action flow process.
* @returns An object containing the `handlePause` and `handleResume` functions.
*/
export function useActionFlowPause() {
const dispatch = useAppDispatch()

/**
* Function to pause the action flow process.
* It dispatches an action to set the status to "PAUSED" if the current status is "PENDING".
*/
const handlePause = () => {
dispatch(setStatus(PROCESS_STATUSES.PAUSED))
}

/**
* Function to resume the action flow process.
* It dispatches an action to set the status to "PENDING".
*/
const handleResume = () => {
dispatch(setStatus(PROCESS_STATUSES.PENDING))
}

return { handlePause, handleResume }
}
6 changes: 6 additions & 0 deletions dapp/src/theme/Spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ const sizeXl = defineStyle({
height: 16,
})

const size2Xl = defineStyle({
width: 20,
height: 20,
})

const sizes = {
xl: sizeXl,
"2xl": size2Xl,
}

export const spinnerTheme = defineStyleConfig({ baseStyle, sizes })
2 changes: 2 additions & 0 deletions dapp/src/types/action-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const PROCESS_STATUSES = {
LOADING: "LOADING",
FAILED: "FAILED",
SUCCEEDED: "SUCCEEDED",
PAUSED: "PAUSED",
PENDING: "PENDING",
} as const

export type ProcessStatus =
Expand Down
6 changes: 6 additions & 0 deletions dapp/src/types/ledger-live-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ export type UseRequestAccountReturn = {
account: Account | null
requestAccount: (...params: RequestAccountParams) => Promise<void>
}

export type LedgerLiveError = {
message?: string
name?: string
stack?: string
}
1 change: 1 addition & 0 deletions dapp/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./exchangeApi"
export * from "./verifyDepositAddress"
export * from "./json"
export * from "./activities"
export * from "./type-check"
15 changes: 15 additions & 0 deletions dapp/src/utils/type-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LedgerLiveError } from "#/types"

export function isObject(
arg: unknown,
): arg is Record<string | number | symbol, unknown> {
return typeof arg === "object" && arg !== null
}

export function isString(arg: unknown): arg is string {
return typeof arg === "string"
}

export function isLedgerLiveError(arg: unknown): arg is LedgerLiveError {
return isObject(arg) && arg.name === "Error" && isString(arg.message)
}

0 comments on commit b1607be

Please sign in to comment.