diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json
index b666300b37b..197f071b082 100644
--- a/src/assets/translations/en/main.json
+++ b/src/assets/translations/en/main.json
@@ -1071,6 +1071,8 @@
"noOpenOrders": "No open orders yet.",
"noHistoricalOrders": "No historical orders yet.",
"limitPriceIsPercentLowerThanMarket": "Limit price is %{percent}% lower than market price",
+ "awaitingOrderPlacement": "Awaiting order placement on %{swapperName}",
+ "orderPlacement": "Place order on %{swapperName}",
"expiryOption": {
"oneHour": "1 hour",
"oneDay": "1 day",
diff --git a/src/components/MultiHopTrade/components/LImitOrderShared/InnerSteps.tsx b/src/components/MultiHopTrade/components/LImitOrderShared/InnerSteps.tsx
new file mode 100644
index 00000000000..539612ce6f2
--- /dev/null
+++ b/src/components/MultiHopTrade/components/LImitOrderShared/InnerSteps.tsx
@@ -0,0 +1,256 @@
+import { ArrowUpDownIcon, CheckCircleIcon, WarningIcon } from '@chakra-ui/icons'
+import { Box, Center, Collapse, Flex, HStack, Skeleton, StepStatus } from '@chakra-ui/react'
+import { fromAccountId, fromAssetId } from '@shapeshiftoss/caip'
+import { COW_SWAP_VAULT_RELAYER_ADDRESS, SwapperName } from '@shapeshiftoss/swapper'
+import { bn } from '@shapeshiftoss/utils'
+import type Polyglot from 'node-polyglot'
+import { useEffect, useMemo, useState } from 'react'
+import { useTranslate } from 'react-polyglot'
+import type { Address } from 'viem'
+import { AnimatedCheck } from 'components/AnimatedCheck'
+import { CircularProgress } from 'components/CircularProgress/CircularProgress'
+import { Text } from 'components/Text'
+import { useErrorToast } from 'hooks/useErrorToast/useErrorToast'
+import { getErc20Allowance } from 'lib/utils/evm'
+import { usePlaceLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi'
+import {
+ selectInputSellAmountCryptoBaseUnit,
+ selectSellAccountId,
+} from 'state/slices/limitOrderInputSlice/selectors'
+import { LimitOrderSubmissionState } from 'state/slices/limitOrderSlice/constants'
+import {
+ selectActiveQuote,
+ selectActiveQuoteId,
+ selectActiveQuoteSellAsset,
+ selectLimitOrderSubmissionMetadata,
+} from 'state/slices/limitOrderSlice/selectors'
+import { TransactionExecutionState } from 'state/slices/tradeQuoteSlice/types'
+import { useAppSelector, useSelectorWithArgs } from 'state/store'
+
+import { useAllowanceApproval } from '../LimitOrder/hooks/useAllowanceApproval'
+import { StepperStep } from '../MultiHopTradeConfirm/components/StepperStep'
+import { TxLabel } from '../TradeConfirm/TxLabel'
+
+const collapseStyle = { width: '100%' }
+const stepProps = { py: 0, pr: 2, pl: 0 }
+
+const erroredStepIndicator =
+const completedStepIndicator =
+
+export const InnerSteps = () => {
+ const { showErrorToast } = useErrorToast()
+ const translate = useTranslate()
+
+ const [isExpanded, setIsExpanded] = useState(false)
+ const [isLoadingAllowance, setIsLoadingAllowance] = useState(false)
+ const [isAllowanceApprovalRequired, setIsAllowanceApprovalRequired] = useState(false)
+
+ const sellAsset = useAppSelector(selectActiveQuoteSellAsset)
+ const sellAccountId = useAppSelector(selectSellAccountId)
+ const sellAmountCryptoBaseUnit = useAppSelector(selectInputSellAmountCryptoBaseUnit)
+ // const activeQuote = useAppSelector(selectActiveQuote)
+ const quoteId = useAppSelector(selectActiveQuoteId)
+
+ // useAllowanceApproval({
+ // activeQuote,
+ // setTxHash,
+ // feeQueryEnabled: true,
+ // isInitiallyRequired: true,
+ // onMutate,
+ // onError,
+ // onSuccess,
+ // })
+
+ const [_, { data: approvalData, error: approvalError }] = usePlaceLimitOrderMutation()
+
+ const orderSubmissionMetadataFilter = useMemo(() => {
+ return { quoteId: quoteId ?? 0 }
+ }, [quoteId])
+
+ const {
+ state: orderSubmissionState,
+ allowanceReset,
+ allowanceApproval,
+ } = useSelectorWithArgs(selectLimitOrderSubmissionMetadata, orderSubmissionMetadataFilter)
+
+ useEffect(() => {
+ if (!sellAsset || !sellAccountId) return
+ const { assetReference, chainId } = fromAssetId(sellAsset.assetId)
+ const sellAccountAddress = fromAccountId(sellAccountId).account as Address
+
+ ;(async () => {
+ setIsLoadingAllowance(true)
+
+ try {
+ // Check the ERC20 token allowance
+ const allowanceOnChainCryptoBaseUnit = await getErc20Allowance({
+ address: assetReference,
+ spender: COW_SWAP_VAULT_RELAYER_ADDRESS,
+ from: sellAccountAddress as Address,
+ chainId,
+ })
+
+ // If approval is required
+ if (bn(allowanceOnChainCryptoBaseUnit).lt(sellAmountCryptoBaseUnit)) {
+ setIsAllowanceApprovalRequired(true)
+ }
+
+ return
+ } catch (e) {
+ showErrorToast(e)
+ } finally {
+ setIsLoadingAllowance(false)
+ }
+ })()
+ }, [
+ isAllowanceApprovalRequired,
+ sellAccountId,
+ sellAmountCryptoBaseUnit,
+ sellAsset,
+ showErrorToast,
+ ])
+
+ const summaryStepIndicator = useMemo(() => {
+ switch (true) {
+ case !!approvalData:
+ return (
+
+
+
+ )
+ case !!approvalError:
+ return (
+
+
+
+ )
+ default:
+ return (
+
+
+
+ )
+ }
+ }, [approvalData, approvalError])
+
+ const summaryStepProps = useMemo(
+ () => ({
+ py: 0,
+ onClick: () => setIsExpanded(!isExpanded),
+ cursor: 'pointer',
+ 'data-expanded': isExpanded,
+ }),
+ [isExpanded],
+ )
+
+ const titleTranslation: string | [string, number | Polyglot.InterpolationOptions] | null =
+ useMemo(() => {
+ switch (orderSubmissionState) {
+ case LimitOrderSubmissionState.AwaitingAllowanceReset:
+ return 'limitOrder.awaitingAllowanceReset'
+ case LimitOrderSubmissionState.AwaitingAllowanceApproval:
+ return 'limitOrder.awaitingApproval'
+ case LimitOrderSubmissionState.AwaitingLimitOrderSubmission:
+ return ['limitOrder.awaitingOrderPlacement', { swapperName: SwapperName.CowSwap }]
+ default:
+ return null
+ }
+ }, [orderSubmissionState])
+
+ const titleElement = useMemo(() => {
+ return (
+
+
+
+
+
+
+ )
+ }, [titleTranslation])
+
+ const stepIndicator = useMemo(
+ () => (
+
+ ),
+ [approvalError],
+ )
+
+ const allowanceResetTitle = useMemo(() => {
+ return (
+
+
+ {allowanceReset.txHash && sellAsset && sellAccountId && (
+
+ )}
+
+ )
+ }, [allowanceReset.txHash, sellAccountId, sellAsset])
+
+ const allowanceApprovalTitle = useMemo(() => {
+ return (
+
+
+ {allowanceApproval.txHash && sellAsset && sellAccountId && (
+
+ )}
+
+ )
+ }, [allowanceApproval.txHash, sellAccountId, sellAsset])
+
+ return (
+
+
+
+
+ {allowanceReset.isRequired && (
+
+ )}
+ {isAllowanceApprovalRequired && (
+
+ )}
+
+
+
+
+ )
+}
diff --git a/src/components/MultiHopTrade/components/LImitOrderShared/LimitOrderConfirm.tsx b/src/components/MultiHopTrade/components/LImitOrderShared/LimitOrderConfirm.tsx
index 77e0ccfa365..b54032d855f 100644
--- a/src/components/MultiHopTrade/components/LImitOrderShared/LimitOrderConfirm.tsx
+++ b/src/components/MultiHopTrade/components/LImitOrderShared/LimitOrderConfirm.tsx
@@ -1,43 +1,34 @@
-import { InfoIcon } from '@chakra-ui/icons'
-import { Button, Card, HStack, Stack } from '@chakra-ui/react'
-import { SwapperName } from '@shapeshiftoss/swapper'
+import { Button } from '@chakra-ui/react'
import { useCallback, useMemo } from 'react'
-import { useTranslate } from 'react-polyglot'
import { useHistory } from 'react-router-dom'
-import { Amount } from 'components/Amount/Amount'
-import { Row } from 'components/Row/Row'
-import { RawText, Text } from 'components/Text/Text'
-import { TransactionDate } from 'components/TransactionHistoryRows/TransactionDate'
+import { Text } from 'components/Text/Text'
import { usePlaceLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi'
+import {
+ selectBuyAmountCryptoBaseUnit,
+ selectInputSellAmountCryptoBaseUnit,
+} from 'state/slices/limitOrderInputSlice/selectors'
import {
selectActiveQuote,
selectActiveQuoteBuyAsset,
- selectActiveQuoteExpirationTimestamp,
- selectActiveQuoteFeeAsset,
- selectActiveQuoteLimitPrice,
- selectActiveQuoteNetworkFeeCryptoPrecision,
- selectActiveQuoteNetworkFeeUserCurrency,
selectActiveQuoteSellAsset,
} from 'state/slices/limitOrderSlice/selectors'
import { useAppSelector } from 'state/store'
import { LimitOrderRoutePaths } from '../LimitOrder/types'
import { SharedConfirm } from '../SharedConfirm/SharedConfirm'
+import { SharedConfirmBody } from '../SharedConfirm/SharedConfirmBody'
import { SharedConfirmFooter } from '../SharedConfirm/SharedConfirmFooter'
-import { SwapperIcon } from '../TradeInput/components/SwapperIcon/SwapperIcon'
+import { InnerSteps } from './InnerSteps'
+import { LimitOrderDetail } from './LimitOrderDetail'
export const LimitOrderConfirm = () => {
const history = useHistory()
- const translate = useTranslate()
const activeQuote = useAppSelector(selectActiveQuote)
const sellAsset = useAppSelector(selectActiveQuoteSellAsset)
const buyAsset = useAppSelector(selectActiveQuoteBuyAsset)
- const feeAsset = useAppSelector(selectActiveQuoteFeeAsset)
- const networkFeeCryptoPrecision = useAppSelector(selectActiveQuoteNetworkFeeCryptoPrecision)
- const networkFeeUserCurrency = useAppSelector(selectActiveQuoteNetworkFeeUserCurrency)
- const limitPrice = useAppSelector(selectActiveQuoteLimitPrice)
- const quoteExpirationTimestamp = useAppSelector(selectActiveQuoteExpirationTimestamp)
+ const sellAmountCryptoBaseUnit = useAppSelector(selectInputSellAmountCryptoBaseUnit)
+ const buyAmountCryptoBaseUnit = useAppSelector(selectBuyAmountCryptoBaseUnit)
const handleBack = useCallback(() => {
history.push(LimitOrderRoutePaths.Input)
@@ -50,81 +41,21 @@ export const LimitOrderConfirm = () => {
}, [])
const body = useMemo(() => {
- return LimitOrderConfirm
- }, [])
-
- const detail = useMemo(() => {
+ if (!sellAsset || !buyAsset) return null
return (
-
-
-
-
-
-
-
-
- =
-
-
-
-
-
-
-
-
-
-
-
- {SwapperName.CowSwap}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {translate('limitOrder.confirmInfo')}
-
-
-
+
)
- }, [
- buyAsset?.symbol,
- feeAsset?.symbol,
- limitPrice?.buyAssetDenomination,
- networkFeeCryptoPrecision,
- networkFeeUserCurrency,
- quoteExpirationTimestamp,
- sellAsset?.symbol,
- translate,
- ])
+ }, [buyAmountCryptoBaseUnit, buyAsset, sellAmountCryptoBaseUnit, sellAsset])
+
+ const detail = useMemo(() => {
+ return
+ }, [])
const button = useMemo(() => {
return (
@@ -145,6 +76,7 @@ export const LimitOrderConfirm = () => {
return
}, [detail, button])
+ if (!body) return null
return (
{
+ const translate = useTranslate()
+
+ const sellAsset = useAppSelector(selectActiveQuoteSellAsset)
+ const buyAsset = useAppSelector(selectActiveQuoteBuyAsset)
+ const feeAsset = useAppSelector(selectActiveQuoteFeeAsset)
+ const networkFeeCryptoPrecision = useAppSelector(selectActiveQuoteNetworkFeeCryptoPrecision)
+ const networkFeeUserCurrency = useAppSelector(selectActiveQuoteNetworkFeeUserCurrency)
+ const limitPrice = useAppSelector(selectActiveQuoteLimitPrice)
+ const quoteExpirationTimestamp = useAppSelector(selectActiveQuoteExpirationTimestamp)
+
+ return (
+
+
+
+
+
+
+
+
+ =
+
+
+
+
+
+
+
+
+
+
+
+ {SwapperName.CowSwap}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {translate('limitOrder.confirmInfo')}
+
+
+
+ )
+}
diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
index 5905d237760..7f042140695 100644
--- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
+++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx
@@ -18,6 +18,7 @@ import { useAccountsFetchQuery } from 'context/AppProvider/hooks/useAccountsFetc
import { WalletActions } from 'context/WalletProvider/actions'
import { useActions } from 'hooks/useActions'
import { useErrorToast } from 'hooks/useErrorToast/useErrorToast'
+import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag'
import { useWallet } from 'hooks/useWallet/useWallet'
import { getErc20Allowance } from 'lib/utils/evm'
import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi'
@@ -123,6 +124,7 @@ export const LimitOrderInput = ({
} = useActions(limitOrderInput.actions)
const { setActiveQuote } = useActions(limitOrderSlice.actions)
const { isFetching: isAccountsMetadataLoading } = useAccountsFetchQuery()
+ const isNewLimitFlowEnabled = useFeatureFlag('NewLimitFlow')
const feeParams = useMemo(
() => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }),
@@ -277,6 +279,12 @@ export const LimitOrderInput = ({
const { assetReference, chainId } = fromAssetId(limitOrderQuoteParams.sellAssetId)
+ // If the new limit flow is enabled, we don't need to check the allowance at this step
+ if (isNewLimitFlowEnabled) {
+ history.push(LimitOrderRoutePaths.Confirm)
+ return
+ }
+
// Trigger loading state while we check the allowance
setIsCheckingAllowance(true)
@@ -312,6 +320,7 @@ export const LimitOrderInput = ({
setActiveQuote,
expiry,
buyAmountCryptoBaseUnit,
+ isNewLimitFlowEnabled,
handleConnect,
history,
showErrorToast,
diff --git a/src/state/apis/limit-orders/limitOrderApi.ts b/src/state/apis/limit-orders/limitOrderApi.ts
index 72b827e4518..766b481b288 100644
--- a/src/state/apis/limit-orders/limitOrderApi.ts
+++ b/src/state/apis/limit-orders/limitOrderApi.ts
@@ -134,7 +134,7 @@ export const limitOrderApi = createApi({
const {
unsignedOrderCreation,
params: { sellAssetId, accountId },
- } = selectConfirmedLimitOrder(state, quoteId)
+ } = selectConfirmedLimitOrder(state, { quoteId })
const { chainId } = fromAssetId(sellAssetId)
const accountMetadata = selectPortfolioAccountMetadataByAccountId(state, { accountId })
diff --git a/src/state/selectors.ts b/src/state/selectors.ts
index 557b6f81562..a1a73d74795 100644
--- a/src/state/selectors.ts
+++ b/src/state/selectors.ts
@@ -4,7 +4,7 @@ import type { QueryStatus } from '@reduxjs/toolkit/dist/query'
import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip'
import type { TxMetadata } from '@shapeshiftoss/chain-adapters'
import type { TradeQuote } from '@shapeshiftoss/swapper'
-import type { HistoryTimeframe } from '@shapeshiftoss/types'
+import type { HistoryTimeframe, QuoteId } from '@shapeshiftoss/types'
import type { TxStatus } from '@shapeshiftoss/unchained-client'
import createCachedSelector from 're-reselect'
import type { FiatRampAction } from 'components/Modals/FiatRamps/FiatRampsCommon'
@@ -57,6 +57,7 @@ type ParamFilter = Partial<{
parser: TxMetadata['parser']
hopIndex: number
tradeId: TradeQuote['id']
+ quoteId: QuoteId
}>
type ParamFilterKey = keyof ParamFilter
@@ -103,3 +104,4 @@ export const selectParserParamFromFilter = selectParamFromFilter('parser')
export const selectHopIndexParamFromRequiredFilter = selectRequiredParamFromFilter('hopIndex')
export const selectTradeIdParamFromRequiredFilter = selectRequiredParamFromFilter('tradeId')
+export const selectQuoteIdParamFromRequiredFilter = selectRequiredParamFromFilter('quoteId')
diff --git a/src/state/slices/limitOrderSlice/selectors.ts b/src/state/slices/limitOrderSlice/selectors.ts
index bba4dbb99ac..20ec72432fd 100644
--- a/src/state/slices/limitOrderSlice/selectors.ts
+++ b/src/state/slices/limitOrderSlice/selectors.ts
@@ -2,6 +2,8 @@ import type { QuoteId } from '@shapeshiftoss/types'
import { bn, bnOrZero, fromBaseUnit } from '@shapeshiftoss/utils'
import { createSelector } from 'reselect'
import type { ReduxState } from 'state/reducer'
+import { createDeepEqualOutputSelector } from 'state/selector-utils'
+import { selectQuoteIdParamFromRequiredFilter } from 'state/selectors'
import { PriceDirection } from '../limitOrderInputSlice/constants'
import {
@@ -20,6 +22,11 @@ export const selectActiveQuote = createSelector(
limitOrderSlice => limitOrderSlice.activeQuote,
)
+export const selectActiveQuoteId = createSelector(
+ selectActiveQuote,
+ activeQuote => activeQuote?.response.id,
+)
+
export const selectActiveQuoteExpirationTimestamp = createSelector(
selectActiveQuote,
activeQuote => {
@@ -167,7 +174,15 @@ export const selectActiveQuoteLimitPrice = createSelector(
export const selectConfirmedLimitOrder = createSelector(
selectLimitOrderSlice,
- (_state: ReduxState, quoteId: QuoteId) => quoteId,
+ selectQuoteIdParamFromRequiredFilter,
(limitOrderSlice: LimitOrderState, quoteId: QuoteId) =>
limitOrderSlice.confirmedLimitOrder[quoteId],
)
+
+export const selectLimitOrderSubmissionMetadata = createDeepEqualOutputSelector(
+ selectLimitOrderSlice,
+ selectQuoteIdParamFromRequiredFilter,
+ (limitOrders, quoteId) => {
+ return limitOrders.orderSubmission[quoteId]
+ },
+)