From 680a26ede4e6c944c19e81c2c77f51e8a8b9403f Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 19 Nov 2024 12:11:02 +0100 Subject: [PATCH 1/5] FIx the wallet window opening The transaction sign window appears despite the rejection of the deposit. Let's make sure that the user does not close the window when the wallet opening action has already been triggered. --- .../ActiveStakingStep/DepositBTCModal.tsx | 16 +++++++++++++--- .../TransactionModal/WalletInteractionModal.tsx | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index 08926094e..bcb3d074a 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -12,7 +12,7 @@ import { eip1193, logPromiseFailure } from "#/utils" import { PROCESS_STATUSES } from "#/types" import { setStatus, setTxHash } from "#/store/action-flow" import { ONE_SEC_IN_MILLISECONDS, queryKeysFactory } from "#/constants" -import { useTimeout } from "@chakra-ui/react" +import { useBoolean, useTimeout } from "@chakra-ui/react" import { useMutation } from "@tanstack/react-query" import WalletInteractionModal from "../WalletInteractionModal" @@ -28,6 +28,8 @@ export default function DepositBTCModal() { queryKey: userKeys.balance(), }) + const [isTriggeredAction, setIsTriggeredAction] = useBoolean() + const onStakeBTCSuccess = useCallback(() => { handleBitcoinBalanceInvalidation() dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)) @@ -97,13 +99,21 @@ export default function DepositBTCModal() { ]) const handledDepositBTCWrapper = useCallback(() => { + setIsTriggeredAction.on() logPromiseFailure(handledDepositBTC()) - }, [handledDepositBTC]) + }, [handledDepositBTC, setIsTriggeredAction]) useTimeout(handledDepositBTCWrapper, ONE_SEC_IN_MILLISECONDS) if (status === "pending" || status === "success") return - return + return ( + + ) } diff --git a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx index dbbf4c53b..8987dc500 100644 --- a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx +++ b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx @@ -44,8 +44,10 @@ const DATA: Record< export default function WalletInteractionModal({ step, + withCloseButton, }: { step: WalletInteractionStep + withCloseButton?: boolean }) { const actionType = useActionFlowType() const connector = useConnector() @@ -53,7 +55,7 @@ export default function WalletInteractionModal({ return ( <> - {step === "opening-wallet" && } + {withCloseButton && } {header} From 2c56e9b560d526c647d7107d443e3b77e0aebef0 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 19 Nov 2024 12:56:31 +0100 Subject: [PATCH 2/5] Do not close the Stake modal on esc button To be able to cancel the deposit/withdrawal flow when a user closes the modal. --- dapp/src/hooks/useTransactionModal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dapp/src/hooks/useTransactionModal.ts b/dapp/src/hooks/useTransactionModal.ts index e052ed131..7e4b9d0be 100644 --- a/dapp/src/hooks/useTransactionModal.ts +++ b/dapp/src/hooks/useTransactionModal.ts @@ -1,4 +1,4 @@ -import { ACTION_FLOW_TYPES, ActionFlowType, MODAL_TYPES } from "#/types" +import { ActionFlowType, MODAL_TYPES } from "#/types" import { useCallback } from "react" import { useModal } from "./useModal" @@ -8,7 +8,7 @@ export function useTransactionModal(type: ActionFlowType) { return useCallback(() => { openModal(MODAL_TYPES[type], { type, - closeOnEsc: type !== ACTION_FLOW_TYPES.UNSTAKE, + closeOnEsc: false, }) }, [openModal, type]) } From eee034b3e140221a543bc06ef97cd5971144b25f Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Tue, 19 Nov 2024 15:48:58 +0100 Subject: [PATCH 3/5] Cancel withdrawal/deposit Cancel the withdrawal/deposit transaction when the user closes the flow modal. --- .../ActiveStakingStep/DepositBTCModal.tsx | 25 ++++---- .../ActiveUnstakingStep/SignMessageModal.tsx | 51 ++++------------ .../WalletInteractionModal.tsx | 6 +- dapp/src/hooks/index.ts | 1 + dapp/src/hooks/useCancelPromise.ts | 58 +++++++++++++++++++ 5 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 dapp/src/hooks/useCancelPromise.ts diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index bcb3d074a..d55ee8f16 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -3,6 +3,7 @@ import { useActionFlowPause, useActionFlowTokenAmount, useAppDispatch, + useCancelPromise, useDepositBTCTransaction, useInvalidateQueries, useStakeFlowContext, @@ -12,7 +13,7 @@ import { eip1193, logPromiseFailure } from "#/utils" import { PROCESS_STATUSES } from "#/types" import { setStatus, setTxHash } from "#/store/action-flow" import { ONE_SEC_IN_MILLISECONDS, queryKeysFactory } from "#/constants" -import { useBoolean, useTimeout } from "@chakra-ui/react" +import { useTimeout } from "@chakra-ui/react" import { useMutation } from "@tanstack/react-query" import WalletInteractionModal from "../WalletInteractionModal" @@ -28,7 +29,8 @@ export default function DepositBTCModal() { queryKey: userKeys.balance(), }) - const [isTriggeredAction, setIsTriggeredAction] = useBoolean() + const { cancel, resolve, shouldOpenErrorModal } = + useCancelPromise("Deposit cancelled") const onStakeBTCSuccess = useCallback(() => { handleBitcoinBalanceInvalidation() @@ -60,13 +62,15 @@ export default function DepositBTCModal() { const onDepositBTCError = useCallback( (error: unknown) => { + if (!shouldOpenErrorModal) return + if (eip1193.didUserRejectRequest(error)) { handlePause() } else { onError(error) } }, - [onError, handlePause], + [shouldOpenErrorModal, handlePause, onError], ) const { mutate: sendBitcoinTransaction, status } = useDepositBTCTransaction({ @@ -81,6 +85,8 @@ export default function DepositBTCModal() { btcAddress, ) + await resolve() + if (verificationStatus === "valid") { sendBitcoinTransaction({ recipient: btcAddress, @@ -94,26 +100,19 @@ export default function DepositBTCModal() { btcAddress, depositReceipt, verifyDepositAddress, + resolve, sendBitcoinTransaction, onError, ]) const handledDepositBTCWrapper = useCallback(() => { - setIsTriggeredAction.on() logPromiseFailure(handledDepositBTC()) - }, [handledDepositBTC, setIsTriggeredAction]) + }, [handledDepositBTC]) useTimeout(handledDepositBTCWrapper, ONE_SEC_IN_MILLISECONDS) if (status === "pending" || status === "success") return - return ( - - ) + return } diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 915a9436a..1cfaef56f 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -1,8 +1,9 @@ -import React, { useCallback, useEffect, useRef, useState } from "react" +import React, { useCallback, useState } from "react" import { useActionFlowPause, useActionFlowTokenAmount, useAppDispatch, + useCancelPromise, useInvalidateQueries, useModal, useTimeout, @@ -22,15 +23,6 @@ const { userKeys } = queryKeysFactory type WithdrawalStatus = "building-data" | "built-data" | "signature" -const sessionIdToPromise: Record< - number, - { - promise: Promise - cancel: (reason: Error) => void - shouldOpenErrorModal: boolean - } -> = {} - export default function SignMessageModal() { const [status, setWaitingStatus] = useState("building-data") @@ -43,25 +35,14 @@ export default function SignMessageModal() { const handleBitcoinPositionInvalidation = useInvalidateQueries({ queryKey: userKeys.position(), }) - const sessionId = useRef(Math.random()) + const { cancel, resolve, shouldOpenErrorModal } = useCancelPromise( + "Withdrawal cancelled", + ) const { transactionFee } = useTransactionDetails( amount, ACTION_FLOW_TYPES.UNSTAKE, ) - useEffect(() => { - let cancel = (_: Error) => {} - const promise: Promise = new Promise((_, reject) => { - cancel = reject - }) - - sessionIdToPromise[sessionId.current] = { - cancel, - promise, - shouldOpenErrorModal: true, - } - }, []) - const dataBuiltStepCallback = useCallback(() => { setWaitingStatus("built-data") return Promise.resolve() @@ -69,11 +50,8 @@ export default function SignMessageModal() { const onSignMessageCallback = useCallback(async () => { setWaitingStatus("signature") - return Promise.race([ - sessionIdToPromise[sessionId.current].promise, - Promise.resolve(), - ]) - }, []) + return resolve() + }, [resolve]) const onSignMessageSuccess = useCallback(() => { handleBitcoinPositionInvalidation() @@ -90,7 +68,7 @@ export default function SignMessageModal() { const onError = useCallback( (error: unknown) => { - if (!sessionIdToPromise[sessionId.current].shouldOpenErrorModal) return + if (!shouldOpenErrorModal) return if (eip1193.didUserRejectRequest(error)) { handlePause() @@ -98,7 +76,7 @@ export default function SignMessageModal() { onSignMessageError(error) } }, - [onSignMessageError, handlePause], + [shouldOpenErrorModal, handlePause, onSignMessageError], ) const { mutate: handleSignMessage } = useMutation({ @@ -151,16 +129,7 @@ export default function SignMessageModal() { }) const onClose = () => { - const currentSessionId = sessionId.current - const sessionData = sessionIdToPromise[currentSessionId] - sessionIdToPromise[currentSessionId] = { - ...sessionData, - shouldOpenErrorModal: false, - } - - sessionIdToPromise[currentSessionId].cancel( - new Error("Withdrawal cancelled"), - ) + cancel() closeModal() } diff --git a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx index 8987dc500..4eeb6e702 100644 --- a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx +++ b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx @@ -44,10 +44,10 @@ const DATA: Record< export default function WalletInteractionModal({ step, - withCloseButton, + onClose, }: { step: WalletInteractionStep - withCloseButton?: boolean + onClose?: () => void }) { const actionType = useActionFlowType() const connector = useConnector() @@ -55,7 +55,7 @@ export default function WalletInteractionModal({ return ( <> - {withCloseButton && } + {step === "opening-wallet" && } {header} diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 9584fa5eb..0f3213d35 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -39,3 +39,4 @@ export { default as useScrollbarVisibility } from "./useScrollbarVisibility" export { default as useAccessCode } from "./useAccessCode" export { default as useFormField } from "./useFormField" export { default as useDepositBTCTransaction } from "./useDepositBTCTransaction" +export { default as useCancelPromise } from "./useCancelPromise" diff --git a/dapp/src/hooks/useCancelPromise.ts b/dapp/src/hooks/useCancelPromise.ts new file mode 100644 index 000000000..e0cafdcde --- /dev/null +++ b/dapp/src/hooks/useCancelPromise.ts @@ -0,0 +1,58 @@ +import { useCallback, useEffect, useMemo, useRef } from "react" + +const sessionIdToPromise: Record< + number, + { + promise: Promise + cancel: (reason: Error) => void + shouldOpenErrorModal: boolean + } +> = {} + +export default function useCancelPromise(errorMsgText: string) { + const sessionId = useRef(Math.random()) + + useEffect(() => { + let cancel = (_: Error) => {} + const promise: Promise = new Promise((_, reject) => { + cancel = reject + }) + + sessionIdToPromise[sessionId.current] = { + cancel, + promise, + shouldOpenErrorModal: true, + } + }, []) + + const cancel = useCallback(() => { + const currentSessionId = sessionId.current + const sessionData = sessionIdToPromise[currentSessionId] + sessionIdToPromise[currentSessionId] = { + ...sessionData, + shouldOpenErrorModal: false, + } + + sessionIdToPromise[currentSessionId].cancel(new Error(errorMsgText)) + }, [errorMsgText]) + + const resolve = useCallback( + () => + Promise.race([ + sessionIdToPromise[sessionId.current].promise, + Promise.resolve(), + ]), + [], + ) + + const shouldOpenErrorModal = useMemo( + () => sessionIdToPromise[sessionId.current]?.shouldOpenErrorModal, + [], + ) + + return { + cancel, + resolve, + shouldOpenErrorModal, + } +} From fffdb6d548b029ab0783836c0ec93d8b7f10cd20 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Fri, 22 Nov 2024 10:19:18 +0100 Subject: [PATCH 4/5] Make sure the `shouldOpenErrorModal` is updated --- dapp/src/hooks/useCancelPromise.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dapp/src/hooks/useCancelPromise.ts b/dapp/src/hooks/useCancelPromise.ts index e0cafdcde..26a15ad37 100644 --- a/dapp/src/hooks/useCancelPromise.ts +++ b/dapp/src/hooks/useCancelPromise.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from "react" +import { useCallback, useEffect, useRef } from "react" const sessionIdToPromise: Record< number, @@ -44,11 +44,8 @@ export default function useCancelPromise(errorMsgText: string) { ]), [], ) - - const shouldOpenErrorModal = useMemo( - () => sessionIdToPromise[sessionId.current]?.shouldOpenErrorModal, - [], - ) + const shouldOpenErrorModal = + sessionIdToPromise[sessionId.current]?.shouldOpenErrorModal return { cancel, From d1962002b2b3e19299b9047969e4a7c920bc9237 Mon Sep 17 00:00:00 2001 From: Karolina Kosiorowska Date: Mon, 25 Nov 2024 11:39:53 +0100 Subject: [PATCH 5/5] Use current session data to check promise state Previously, `shouldOpenErrorModal` returned a wrong value. Let's check the shouldOpenErrorModal value using the current sessionId to make sure the data is up to date. --- .../ActiveStakingStep/DepositBTCModal.tsx | 13 +++++--- .../ActiveUnstakingStep/SignMessageModal.tsx | 10 +++--- dapp/src/hooks/useCancelPromise.ts | 33 ++++++++----------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index d55ee8f16..6a0bf6e5e 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react" +import React, { useCallback, useRef } from "react" import { useActionFlowPause, useActionFlowTokenAmount, @@ -29,8 +29,11 @@ export default function DepositBTCModal() { queryKey: userKeys.balance(), }) - const { cancel, resolve, shouldOpenErrorModal } = - useCancelPromise("Deposit cancelled") + const sessionId = useRef(Math.random()) + const { cancel, resolve, sessionIdToPromise } = useCancelPromise( + sessionId.current, + "Deposit cancelled", + ) const onStakeBTCSuccess = useCallback(() => { handleBitcoinBalanceInvalidation() @@ -62,7 +65,7 @@ export default function DepositBTCModal() { const onDepositBTCError = useCallback( (error: unknown) => { - if (!shouldOpenErrorModal) return + if (!sessionIdToPromise[sessionId.current].shouldOpenErrorModal) return if (eip1193.didUserRejectRequest(error)) { handlePause() @@ -70,7 +73,7 @@ export default function DepositBTCModal() { onError(error) } }, - [shouldOpenErrorModal, handlePause, onError], + [sessionIdToPromise, handlePause, onError], ) const { mutate: sendBitcoinTransaction, status } = useDepositBTCTransaction({ diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 1cfaef56f..877f83264 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react" +import React, { useCallback, useRef, useState } from "react" import { useActionFlowPause, useActionFlowTokenAmount, @@ -35,7 +35,9 @@ export default function SignMessageModal() { const handleBitcoinPositionInvalidation = useInvalidateQueries({ queryKey: userKeys.position(), }) - const { cancel, resolve, shouldOpenErrorModal } = useCancelPromise( + const sessionId = useRef(Math.random()) + const { cancel, resolve, sessionIdToPromise } = useCancelPromise( + sessionId.current, "Withdrawal cancelled", ) const { transactionFee } = useTransactionDetails( @@ -68,7 +70,7 @@ export default function SignMessageModal() { const onError = useCallback( (error: unknown) => { - if (!shouldOpenErrorModal) return + if (!sessionIdToPromise[sessionId.current].shouldOpenErrorModal) return if (eip1193.didUserRejectRequest(error)) { handlePause() @@ -76,7 +78,7 @@ export default function SignMessageModal() { onSignMessageError(error) } }, - [shouldOpenErrorModal, handlePause, onSignMessageError], + [sessionIdToPromise, handlePause, onSignMessageError], ) const { mutate: handleSignMessage } = useMutation({ diff --git a/dapp/src/hooks/useCancelPromise.ts b/dapp/src/hooks/useCancelPromise.ts index 26a15ad37..a6bb1666b 100644 --- a/dapp/src/hooks/useCancelPromise.ts +++ b/dapp/src/hooks/useCancelPromise.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from "react" +import { useCallback, useEffect } from "react" const sessionIdToPromise: Record< number, @@ -9,47 +9,42 @@ const sessionIdToPromise: Record< } > = {} -export default function useCancelPromise(errorMsgText: string) { - const sessionId = useRef(Math.random()) - +export default function useCancelPromise( + sessionId: number, + errorMsgText: string, +) { useEffect(() => { let cancel = (_: Error) => {} const promise: Promise = new Promise((_, reject) => { cancel = reject }) - sessionIdToPromise[sessionId.current] = { + sessionIdToPromise[sessionId] = { cancel, promise, shouldOpenErrorModal: true, } - }, []) + }, [sessionId]) const cancel = useCallback(() => { - const currentSessionId = sessionId.current - const sessionData = sessionIdToPromise[currentSessionId] - sessionIdToPromise[currentSessionId] = { + const sessionData = sessionIdToPromise[sessionId] + sessionIdToPromise[sessionId] = { ...sessionData, shouldOpenErrorModal: false, } - sessionIdToPromise[currentSessionId].cancel(new Error(errorMsgText)) - }, [errorMsgText]) + sessionIdToPromise[sessionId].cancel(new Error(errorMsgText)) + }, [errorMsgText, sessionId]) const resolve = useCallback( () => - Promise.race([ - sessionIdToPromise[sessionId.current].promise, - Promise.resolve(), - ]), - [], + Promise.race([sessionIdToPromise[sessionId].promise, Promise.resolve()]), + [sessionId], ) - const shouldOpenErrorModal = - sessionIdToPromise[sessionId.current]?.shouldOpenErrorModal return { cancel, resolve, - shouldOpenErrorModal, + sessionIdToPromise, } }