diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalResetStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalResetStep.tsx index 5529105284e..bbf0530fc6d 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalResetStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalResetStep.tsx @@ -9,6 +9,7 @@ import { VStack, } from '@chakra-ui/react' import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' +import { assertUnreachable } from '@shapeshiftoss/utils' import { useCallback, useMemo } from 'react' import { FaInfoCircle } from 'react-icons/fa' import { FaRotateRight } from 'react-icons/fa6' @@ -17,12 +18,12 @@ import { Row } from 'components/Row/Row' import { Text } from 'components/Text' import { AllowanceType } from 'hooks/queries/useApprovalFees' import { selectHopExecutionMetadata } from 'state/slices/tradeQuoteSlice/selectors' -import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' +import { HopExecutionState, TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types' import { useAppSelector } from 'state/store' import { SharedApprovalStep } from './SharedApprovalStep/SharedApprovalStep' import type { RenderAllowanceContentCallbackParams } from './SharedApprovalStep/types' -import { ApprovalStatusIcon } from './StatusIcon' +import { StatusIcon } from './StatusIcon' export type ApprovalResetStepProps = { tradeQuoteStep: TradeQuoteStep @@ -33,7 +34,7 @@ export type ApprovalResetStepProps = { activeTradeId: TradeQuote['id'] } -const initialIcon = +const defaultIcon = export const ApprovalResetStep = ({ tradeQuoteStep, @@ -50,19 +51,30 @@ export const ApprovalResetStep = ({ hopIndex, } }, [activeTradeId, hopIndex]) - const { state, allowanceReset } = useAppSelector(state => + const { state: hopExecutionState, allowanceReset } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter), ) const stepIndicator = useMemo(() => { - return ( - - ) - }, [allowanceReset.state, state]) + const txStatus = (() => { + switch (hopExecutionState) { + case HopExecutionState.Pending: + return TransactionExecutionState.AwaitingConfirmation + case HopExecutionState.AwaitingApprovalReset: + return allowanceReset.state === TransactionExecutionState.Failed + ? TransactionExecutionState.Failed + : TransactionExecutionState.Pending + case HopExecutionState.AwaitingApproval: + case HopExecutionState.AwaitingSwap: + case HopExecutionState.Complete: + return TransactionExecutionState.Complete + default: + assertUnreachable(hopExecutionState) + } + })() + + return + }, [allowanceReset.state, hopExecutionState]) const renderResetAllowanceContent = useCallback( ({ @@ -110,13 +122,23 @@ export const ApprovalResetStep = ({ [translate, isActive], ) + const isComplete = useMemo(() => { + return [ + HopExecutionState.AwaitingApproval, + HopExecutionState.AwaitingSwap, + HopExecutionState.Complete, + ].includes(hopExecutionState) + }, [hopExecutionState]) + return ( ) } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep.tsx index 49a8ebfb5da..305e343b2aa 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep.tsx @@ -9,9 +9,10 @@ import { VStack, } from '@chakra-ui/react' import type { TradeQuote, TradeQuoteStep } from '@shapeshiftoss/swapper' +import { assertUnreachable } from '@shapeshiftoss/utils' import { useCallback, useMemo } from 'react' import { FaInfoCircle } from 'react-icons/fa' -import { FaRotateRight } from 'react-icons/fa6' +import { FaThumbsUp } from 'react-icons/fa6' import { useTranslate } from 'react-polyglot' import { Row } from 'components/Row/Row' import { Text } from 'components/Text' @@ -23,7 +24,7 @@ import { useAppSelector } from 'state/store' import { SharedApprovalStep } from './SharedApprovalStep/SharedApprovalStep' import type { RenderAllowanceContentCallbackParams } from './SharedApprovalStep/types' -import { ApprovalStatusIcon } from './StatusIcon' +import { StatusIcon } from './StatusIcon' export type ApprovalStepProps = { tradeQuoteStep: TradeQuoteStep @@ -34,7 +35,7 @@ export type ApprovalStepProps = { activeTradeId: TradeQuote['id'] } -const initialIcon = +const defaultIcon = export const ApprovalStep = ({ tradeQuoteStep, @@ -53,22 +54,32 @@ export const ApprovalStep = ({ hopIndex, } }, [activeTradeId, hopIndex]) - const { state, approval } = useAppSelector(state => + const { state: hopExecutionState, approval } = useAppSelector(state => selectHopExecutionMetadata(state, hopExecutionMetadataFilter), ) const stepIndicator = useMemo(() => { - return ( - - ) - }, [approval.state, state]) + const txStatus = (() => { + switch (hopExecutionState) { + case HopExecutionState.Pending: + case HopExecutionState.AwaitingApprovalReset: + return TransactionExecutionState.AwaitingConfirmation + case HopExecutionState.AwaitingApproval: + return approval.state === TransactionExecutionState.Failed + ? TransactionExecutionState.Failed + : TransactionExecutionState.Pending + case HopExecutionState.AwaitingSwap: + case HopExecutionState.Complete: + return TransactionExecutionState.Complete + default: + assertUnreachable(hopExecutionState) + } + })() - const renderResetAllowanceContent = useCallback( + return + }, [approval.state, hopExecutionState]) + + const renderApprovalContent = useCallback( ({ hopExecutionState, transactionExecutionState, @@ -136,22 +147,27 @@ export const ApprovalStep = ({ [isActive, translate, isExactAllowance, toggleIsExactAllowance], ) + const isComplete = useMemo(() => { + return [HopExecutionState.AwaitingSwap, HopExecutionState.Complete].includes(hopExecutionState) + }, [hopExecutionState]) + return ( ) } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hop.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hop.tsx index a6504c24c6c..714fba76d98 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hop.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/Hop.tsx @@ -221,7 +221,7 @@ export const Hop = ({ )} - {allowanceReset.isRequired && ( + {allowanceReset.isRequired === true && ( { - const isComplete = useMemo(() => { - return [ - HopExecutionState.AwaitingApproval, - HopExecutionState.AwaitingSwap, - HopExecutionState.Complete, - ].includes(hopExecutionState) - }, [hopExecutionState]) - const completedDescription = useMemo(() => { return ( { + (approvalNetworkFeeCryptoFormatted: string) => { return ( ) : ( ) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/SharedApprovalStep/components/SharedApprovalStepPending.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/SharedApprovalStep/components/SharedApprovalStepPending.tsx index db1f71b20d4..5b9fb15d052 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/SharedApprovalStep/components/SharedApprovalStepPending.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/SharedApprovalStep/components/SharedApprovalStepPending.tsx @@ -1,5 +1,5 @@ import type { TradeQuoteStep } from '@shapeshiftoss/swapper' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' import type { AllowanceType } from 'hooks/queries/useApprovalFees' import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' @@ -14,6 +14,7 @@ import { StepperStep } from '../../StepperStep' import type { RenderAllowanceContentCallback } from '../types' export type SharedApprovalStepPendingProps = { + titleTranslation: string tradeQuoteStep: TradeQuoteStep hopIndex: number isLoading?: boolean @@ -22,12 +23,12 @@ export type SharedApprovalStepPendingProps = { transactionExecutionState: TransactionExecutionState stepIndicator: JSX.Element allowanceType: AllowanceType - feeQueryEnabled: boolean - renderDescription: (approvalNetworkFeeCryptoFormatted?: string) => JSX.Element + renderDescription: (approvalNetworkFeeCryptoFormatted: string) => JSX.Element renderContent: RenderAllowanceContentCallback } export const SharedApprovalStepPending = ({ + titleTranslation, tradeQuoteStep, hopIndex, isLoading, @@ -36,36 +37,25 @@ export const SharedApprovalStepPending = ({ hopExecutionState, transactionExecutionState, allowanceType, - feeQueryEnabled, renderDescription, renderContent, }: SharedApprovalStepPendingProps) => { + const translate = useTranslate() const { number: { toCrypto }, } = useLocaleFormatter() - const [localFeeQueryEnabled, setFeeQueryEnabled] = useState(true) - const { approveMutation, approvalNetworkFeeCryptoBaseUnit, isLoading: isAllowanceApprovalLoading, - } = useAllowanceApproval( - tradeQuoteStep, - hopIndex, - allowanceType, - feeQueryEnabled && localFeeQueryEnabled, - activeTradeId, - ) + } = useAllowanceApproval(tradeQuoteStep, hopIndex, allowanceType, true, activeTradeId) const handleSignAllowanceApproval = useCallback(async () => { try { - setFeeQueryEnabled(false) await approveMutation.mutateAsync() } catch (error) { console.error(error) - } finally { - setFeeQueryEnabled(true) } }, [approveMutation]) @@ -74,19 +64,15 @@ export const SharedApprovalStepPending = ({ ) const approvalNetworkFeeCryptoFormatted = useMemo(() => { - if (!feeAsset) return '' - - if (approvalNetworkFeeCryptoBaseUnit) { + if (feeAsset && approvalNetworkFeeCryptoBaseUnit) { return toCrypto( fromBaseUnit(approvalNetworkFeeCryptoBaseUnit, feeAsset.precision), feeAsset.symbol, ) } - return '' - }, [approvalNetworkFeeCryptoBaseUnit, feeAsset, toCrypto]) - - const translate = useTranslate() + return translate('common.loadingText').toLowerCase() + }, [approvalNetworkFeeCryptoBaseUnit, feeAsset, toCrypto, translate]) const content = useMemo(() => { return renderContent({ @@ -110,7 +96,7 @@ export const SharedApprovalStepPending = ({ return ( { - const txStatus = useMemo(() => { - switch (hopExecutionState) { - case HopExecutionState.Pending: - return TransactionExecutionState.AwaitingConfirmation - case HopExecutionState.AwaitingApprovalReset: - case HopExecutionState.AwaitingApproval: - // override completed state to pending, isApprovalNeeded dictates this - if ( - approvalTxState === TransactionExecutionState.Complete && - overrideCompletedStateToPending - ) { - return TransactionExecutionState.Pending - } - - return approvalTxState - // override approvalTxState if external approval triggered app to proceed to next step - case HopExecutionState.AwaitingSwap: - case HopExecutionState.Complete: - return TransactionExecutionState.Complete - default: - assertUnreachable(hopExecutionState) - } - }, [hopExecutionState, approvalTxState, overrideCompletedStateToPending]) - return -} diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useAllowanceApproval.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useAllowanceApproval.tsx index 552289ada06..ade3992bf52 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useAllowanceApproval.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useAllowanceApproval.tsx @@ -35,7 +35,12 @@ export const useAllowanceApproval = ( const isReset = useMemo(() => allowanceType === AllowanceType.Reset, [allowanceType]) - const { allowanceCryptoBaseUnitResult, evmFeesResult, isApprovalRequired } = useApprovalFees({ + const { + allowanceCryptoBaseUnitResult, + evmFeesResult, + isApprovalRequired, + isAllowanceResetRequired, + } = useApprovalFees({ amountCryptoBaseUnit: tradeQuoteStep.sellAmountIncludingProtocolFeesCryptoBaseUnit, assetId: tradeQuoteStep.sellAsset.assetId, from: sellAssetAccountId ? fromAccountId(sellAssetAccountId).account : undefined, @@ -53,6 +58,15 @@ export const useAllowanceApproval = ( dispatch(tradeQuoteSlice.actions.setApprovalStepComplete({ hopIndex, id: confirmedTradeId })) }, [dispatch, hopIndex, isApprovalRequired, confirmedTradeId]) + useEffect(() => { + if (isAllowanceResetRequired !== false || allowanceType !== AllowanceType.Reset) return + + // Mark the allowance reset step complete as required. + // This is deliberately disjoint to the approval transaction orchestration to allow users to + // complete an approval reset externally and have the app respond to the updated allowance on chain. + dispatch(tradeQuoteSlice.actions.setApprovalResetComplete({ hopIndex, id: confirmedTradeId })) + }, [dispatch, hopIndex, isAllowanceResetRequired, confirmedTradeId, allowanceType]) + const approveMutation = useMutation({ ...reactQueries.mutations.approve({ accountNumber: tradeQuoteStep.accountNumber, diff --git a/src/hooks/queries/useApprovalFees.ts b/src/hooks/queries/useApprovalFees.ts index 1bbb38be6f2..f903844d837 100644 --- a/src/hooks/queries/useApprovalFees.ts +++ b/src/hooks/queries/useApprovalFees.ts @@ -35,12 +35,13 @@ export const useApprovalFees = ({ return fromAssetId(assetId) }, [assetId]) - const { allowanceCryptoBaseUnitResult, isApprovalRequired } = useIsApprovalRequired({ - amountCryptoBaseUnit, - assetId, - from, - spender, - }) + const { allowanceCryptoBaseUnitResult, isApprovalRequired, isAllowanceResetRequired } = + useIsApprovalRequired({ + amountCryptoBaseUnit, + assetId, + from, + spender, + }) const approveContractData = useMemo(() => { if (!amountCryptoBaseUnit || !spender) return @@ -72,6 +73,7 @@ export const useApprovalFees = ({ approveContractData, evmFeesResult, isApprovalRequired, + isAllowanceResetRequired, } } diff --git a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts index d67caf78ac2..5ae3428800e 100644 --- a/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts +++ b/src/state/slices/tradeQuoteSlice/tradeQuoteSlice.ts @@ -92,9 +92,6 @@ export const tradeQuoteSlice = createSlice({ const allowanceKey = isReset ? AllowanceKey.AllowanceReset : AllowanceKey.Approval state.tradeExecution[action.payload.id][hopKey][allowanceKey].state = TransactionExecutionState.Failed - if (allowanceKey === AllowanceKey.AllowanceReset) { - state.tradeExecution[action.payload.id][hopKey].state = HopExecutionState.AwaitingApproval - } }, // marks the approval tx as complete, but the allowance check needs to pass before proceeding to swap step setApprovalTxComplete: ( @@ -106,9 +103,14 @@ export const tradeQuoteSlice = createSlice({ const allowanceKey = isReset ? AllowanceKey.AllowanceReset : AllowanceKey.Approval state.tradeExecution[action.payload.id][hopKey][allowanceKey].state = TransactionExecutionState.Complete - if (allowanceKey === AllowanceKey.AllowanceReset) { - state.tradeExecution[action.payload.id][hopKey].state = HopExecutionState.AwaitingApproval - } + }, + setApprovalResetComplete: ( + state, + action: PayloadAction<{ hopIndex: number; id: TradeQuote['id'] }>, + ) => { + const { hopIndex } = action.payload + const key = hopIndex === 0 ? HopKey.FirstHop : HopKey.SecondHop + state.tradeExecution[action.payload.id][key].state = HopExecutionState.AwaitingApproval }, // progresses the hop to the swap step after the allowance check has passed setApprovalStepComplete: (