From c3e30575a9fcb5478177e965356032f16806387c Mon Sep 17 00:00:00 2001 From: Fara Woolf Date: Fri, 5 Apr 2024 12:25:46 -0500 Subject: [PATCH 1/2] refactor: swaps routes, closes #4317 --- src/app/features/container/container.tsx | 4 +- .../pages/home/components/account-actions.tsx | 2 +- src/app/pages/swap/alex-swap-container.tsx | 35 +++++----- .../components/swap-asset-item.tsx | 2 +- .../components/swap-asset-list.tsx | 51 ++++++++------- .../swap-asset-dialog-base.tsx | 33 ++++++++++ .../swap-asset-dialog-quote.tsx | 33 ++++++++++ .../select-asset-trigger-button.tsx | 6 +- .../components}/swap-amount-field.tsx | 31 +++++---- .../components/swap-asset-select.layout.tsx} | 16 ++--- .../components/swap-asset-selected.tsx} | 9 ++- .../components}/swap-toggle-button.tsx | 26 ++++---- .../swap-asset-select-base.tsx} | 65 ++++++++++--------- .../swap-asset-select-quote.tsx} | 30 ++++----- .../swap-assets-pair.layout.tsx | 10 +-- .../swap-assets-pair/swap-assets-pair.tsx | 23 +++---- .../swap-choose-asset/swap-choose-asset.tsx | 45 ------------- .../components/swap-details/swap-details.tsx | 12 ++-- .../swap/components/swap-selected-assets.tsx | 28 -------- src/app/pages/swap/generate-swap-routes.tsx | 6 +- src/app/pages/swap/hooks/use-swap-form.tsx | 32 ++++----- src/app/pages/swap/hooks/use-swap-navigate.ts | 13 ++++ src/app/pages/swap/swap.context.ts | 4 +- src/app/pages/swap/swap.tsx | 36 ++++++---- src/shared/route-urls.ts | 7 +- tests/page-object-models/swap.page.ts | 6 +- tests/selectors/swap.selectors.ts | 4 +- tests/specs/swap/swap.spec.ts | 16 ++--- 28 files changed, 310 insertions(+), 275 deletions(-) rename src/app/pages/swap/components/{swap-choose-asset => swap-asset-dialog}/components/swap-asset-item.tsx (94%) rename src/app/pages/swap/components/{swap-choose-asset => swap-asset-dialog}/components/swap-asset-list.tsx (56%) create mode 100644 src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-base.tsx create mode 100644 src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-quote.tsx rename src/app/pages/swap/components/{ => swap-asset-select/components}/select-asset-trigger-button.tsx (94%) rename src/app/pages/swap/components/{ => swap-asset-select/components}/swap-amount-field.tsx (71%) rename src/app/pages/swap/components/{swap-selected-asset.layout.tsx => swap-asset-select/components/swap-asset-select.layout.tsx} (89%) rename src/app/pages/swap/components/{selected-asset.tsx => swap-asset-select/components/swap-asset-selected.tsx} (81%) rename src/app/pages/swap/components/{ => swap-asset-select/components}/swap-toggle-button.tsx (55%) rename src/app/pages/swap/components/{swap-selected-asset-from.tsx => swap-asset-select/swap-asset-select-base.tsx} (55%) rename src/app/pages/swap/components/{swap-selected-asset-to.tsx => swap-asset-select/swap-asset-select-quote.tsx} (57%) delete mode 100644 src/app/pages/swap/components/swap-choose-asset/swap-choose-asset.tsx delete mode 100644 src/app/pages/swap/components/swap-selected-assets.tsx create mode 100644 src/app/pages/swap/hooks/use-swap-navigate.ts diff --git a/src/app/features/container/container.tsx b/src/app/features/container/container.tsx index ab925cf0238..4379f003915 100644 --- a/src/app/features/container/container.tsx +++ b/src/app/features/container/container.tsx @@ -69,9 +69,11 @@ export function Container() { const displayHeader = !isLandingPage(pathname) && !isNoHeaderPopup(pathname); const isSessionLocked = getIsSessionLocked(pathname); + // TODO: Refactor? This is very hard to manage with dynamic routes. Temporarily + // added a fix to catch the swap route: '/swap/:base/:quote?' function getOnGoBackLocation(pathname: RouteUrls) { + if (pathname.includes('/swap')) return navigate(RouteUrls.Home); switch (pathname) { - case RouteUrls.Swap: case RouteUrls.Fund.replace(':currency', 'STX'): case RouteUrls.Fund.replace(':currency', 'BTC'): case RouteUrls.SendCryptoAssetForm.replace(':symbol', 'stx'): diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx index e672502ca1b..9fbc32d8e0c 100644 --- a/src/app/pages/home/components/account-actions.tsx +++ b/src/app/pages/home/components/account-actions.tsx @@ -53,7 +53,7 @@ export function AccountActions() { data-testid={HomePageSelectors.SwapBtn} icon={} label="Swap" - onClick={() => navigate(RouteUrls.Swap)} + onClick={() => navigate(RouteUrls.Swap.replace(':base', 'STX').replace(':quote', ''))} /> ), [ChainID.Testnet]: null, diff --git a/src/app/pages/swap/alex-swap-container.tsx b/src/app/pages/swap/alex-swap-container.tsx index 0107298857c..0b19faf9676 100644 --- a/src/app/pages/swap/alex-swap-container.tsx +++ b/src/app/pages/swap/alex-swap-container.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react'; -import { Outlet, useNavigate } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; import { bytesToHex } from '@stacks/common'; import { ContractCallPayload, TransactionTypes } from '@stacks/connect'; @@ -30,6 +30,7 @@ import { useAlexBroadcastSwap } from './hooks/use-alex-broadcast-swap'; import { oneHundredMillion, useAlexSwap } from './hooks/use-alex-swap'; import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap'; import { SwapAsset, SwapFormValues } from './hooks/use-swap-form'; +import { useSwapNavigate } from './hooks/use-swap-navigate'; import { SwapContext, SwapProvider } from './swap.context'; import { defaultSwapFee, @@ -41,7 +42,7 @@ export const alexSwapRoutes = generateSwapRoutes(); function AlexSwapContainer() { const [isSendingMax, setIsSendingMax] = useState(false); - const navigate = useNavigate(); + const navigate = useSwapNavigate(); const { setIsLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); const currentAccount = useCurrentStacksAccount(); const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx(); @@ -78,14 +79,14 @@ function AlexSwapContainer() { ); async function onSubmitSwapForReview(values: SwapFormValues) { - if (isUndefined(values.swapAssetFrom) || isUndefined(values.swapAssetTo)) { + if (isUndefined(values.swapAssetBase) || isUndefined(values.swapAssetQuote)) { logger.error('Error submitting swap for review'); return; } const [router, lpFee] = await Promise.all([ - alex.getRouter(values.swapAssetFrom.currency, values.swapAssetTo.currency), - alex.getFeeRate(values.swapAssetFrom.currency, values.swapAssetTo.currency), + alex.getRouter(values.swapAssetBase.currency, values.swapAssetQuote.currency), + alex.getFeeRate(values.swapAssetBase.currency, values.swapAssetQuote.currency), ]); onSetSwapSubmissionData({ @@ -100,10 +101,10 @@ function AlexSwapContainer() { .filter(isDefined), slippage, sponsored: isSponsoredByAlex, - swapAmountFrom: values.swapAmountFrom, - swapAmountTo: values.swapAmountTo, - swapAssetFrom: values.swapAssetFrom, - swapAssetTo: values.swapAssetTo, + swapAmountBase: values.swapAmountBase, + swapAmountQuote: values.swapAmountQuote, + swapAssetBase: values.swapAssetBase, + swapAssetQuote: values.swapAssetQuote, timestamp: new Date().toISOString(), }); @@ -117,8 +118,8 @@ function AlexSwapContainer() { } if ( - isUndefined(swapSubmissionData.swapAssetFrom) || - isUndefined(swapSubmissionData.swapAssetTo) + isUndefined(swapSubmissionData.swapAssetBase) || + isUndefined(swapSubmissionData.swapAssetQuote) ) { logger.error('No assets selected to perform swap'); return; @@ -127,14 +128,14 @@ function AlexSwapContainer() { setIsLoading(); const fromAmount = BigInt( - new BigNumber(swapSubmissionData.swapAmountFrom) + new BigNumber(swapSubmissionData.swapAmountBase) .multipliedBy(oneHundredMillion) .dp(0) .toString() ); const minToAmount = BigInt( - new BigNumber(swapSubmissionData.swapAmountTo) + new BigNumber(swapSubmissionData.swapAmountQuote) .multipliedBy(oneHundredMillion) .multipliedBy(new BigNumber(1).minus(slippage)) .dp(0) @@ -143,8 +144,8 @@ function AlexSwapContainer() { const tx = alex.runSwap( currentAccount?.address, - swapSubmissionData.swapAssetFrom.currency, - swapSubmissionData.swapAssetTo.currency, + swapSubmissionData.swapAssetBase.currency, + swapSubmissionData.swapAssetQuote.currency, fromAmount, minToAmount, swapSubmissionData.router.map(x => x.currency) @@ -197,8 +198,8 @@ function AlexSwapContainer() { onSetIsSendingMax: value => setIsSendingMax(value), onSubmitSwapForReview, onSubmitSwap, - swappableAssetsFrom: migratePositiveBalancesToTop(swappableAssets), - swappableAssetsTo: swappableAssets, + swappableAssetsBase: migratePositiveBalancesToTop(swappableAssets), + swappableAssetsQuote: swappableAssets, swapSubmissionData, }; diff --git a/src/app/pages/swap/components/swap-choose-asset/components/swap-asset-item.tsx b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx similarity index 94% rename from src/app/pages/swap/components/swap-choose-asset/components/swap-asset-item.tsx rename to src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx index 8e56372da2b..951e4b7c7ab 100644 --- a/src/app/pages/swap/components/swap-choose-asset/components/swap-asset-item.tsx +++ b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-item.tsx @@ -22,7 +22,7 @@ export function SwapAssetItem({ asset, onClick }: SwapAssetItemProps) { const fallback = getAvatarFallback(asset.name); return ( - + diff --git a/src/app/pages/swap/components/swap-choose-asset/components/swap-asset-list.tsx b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx similarity index 56% rename from src/app/pages/swap/components/swap-choose-asset/components/swap-asset-list.tsx rename to src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx index c7870c93e52..736c2152b17 100644 --- a/src/app/pages/swap/components/swap-choose-asset/components/swap-asset-list.tsx +++ b/src/app/pages/swap/components/swap-asset-dialog/components/swap-asset-list.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { SwapSelectors } from '@tests/selectors/swap.selectors'; import BigNumber from 'bignumber.js'; @@ -6,6 +6,7 @@ import { useFormikContext } from 'formik'; import { Stack } from 'leather-styles/jsx'; import { createMoney } from '@shared/models/money.model'; +import { RouteUrls } from '@shared/route-urls'; import { isUndefined } from '@shared/utils'; import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money'; @@ -13,46 +14,46 @@ import { formatMoneyWithoutSymbol } from '@app/common/money/format-money'; import { useSwapContext } from '@app/pages/swap/swap.context'; import { SwapAsset, SwapFormValues } from '../../../hooks/use-swap-form'; -import { useSwapChooseAssetState } from '../swap-choose-asset'; import { SwapAssetItem } from './swap-asset-item'; interface SwapAssetList { assets: SwapAsset[]; + type: string; } -export function SwapAssetList({ assets }: SwapAssetList) { +export function SwapAssetList({ assets, type }: SwapAssetList) { const { fetchToAmount } = useSwapContext(); - const { swapListType } = useSwapChooseAssetState(); const { setFieldError, setFieldValue, values } = useFormikContext(); const navigate = useNavigate(); - - const isFromList = swapListType === 'from'; - const isToList = swapListType === 'to'; + const { base, quote } = useParams(); + const isBaseList = type === 'base'; + const isQuoteList = type === 'quote'; const selectableAssets = assets.filter( asset => - (isFromList && asset.name !== values.swapAssetTo?.name) || - (isToList && asset.name !== values.swapAssetFrom?.name) + (isBaseList && asset.name !== values.swapAssetQuote?.name) || + (isQuoteList && asset.name !== values.swapAssetBase?.name) ); - async function onChooseAsset(asset: SwapAsset) { + async function onSelectAsset(asset: SwapAsset) { let from: SwapAsset | undefined; let to: SwapAsset | undefined; - if (isFromList) { + if (isBaseList) { from = asset; - to = values.swapAssetTo; - await setFieldValue('swapAssetFrom', asset); - } else if (isToList) { - from = values.swapAssetFrom; + to = values.swapAssetQuote; + await setFieldValue('swapAssetBase', asset); + navigate(RouteUrls.Swap.replace(':base', from.name).replace(':quote', quote ?? '')); + } else if (isQuoteList) { + from = values.swapAssetBase; to = asset; - await setFieldValue('swapAssetTo', asset); - setFieldError('swapAssetTo', undefined); + await setFieldValue('swapAssetQuote', asset); + setFieldError('swapAssetQuote', undefined); + navigate(RouteUrls.Swap.replace(':base', base ?? '').replace(':quote', to.name)); } - navigate(-1); - if (from && to && values.swapAmountFrom) { - const toAmount = await fetchToAmount(from, to, values.swapAmountFrom); + if (from && to && values.swapAmountBase) { + const toAmount = await fetchToAmount(from, to, values.swapAmountBase); if (isUndefined(toAmount)) { - await setFieldValue('swapAmountTo', ''); + await setFieldValue('swapAmountQuote', ''); return; } const toAmountAsMoney = createMoney( @@ -60,18 +61,18 @@ export function SwapAssetList({ assets }: SwapAssetList) { to?.balance.symbol ?? '', to?.balance.decimals ); - await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney)); - setFieldError('swapAmountTo', undefined); + await setFieldValue('swapAmountQuote', formatMoneyWithoutSymbol(toAmountAsMoney)); + setFieldError('swapAmountQuote', undefined); } } return ( - + {selectableAssets.map(asset => ( onChooseAsset(asset)} + onClick={() => onSelectAsset(asset)} /> ))} diff --git a/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-base.tsx b/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-base.tsx new file mode 100644 index 00000000000..2db2e43070e --- /dev/null +++ b/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-base.tsx @@ -0,0 +1,33 @@ +import { RouteUrls } from '@shared/route-urls'; + +import { Dialog } from '@app/ui/components/containers/dialog/dialog'; +import { Header } from '@app/ui/components/containers/headers/header'; + +import { useSwapNavigate } from '../../hooks/use-swap-navigate'; +import { useSwapContext } from '../../swap.context'; +import { SwapAssetList } from './components/swap-asset-list'; + +export function SwapAssetDialogBase() { + const { swappableAssetsBase } = useSwapContext(); + const navigate = useSwapNavigate(); + + return ( + navigate(RouteUrls.Swap)} + header={ +
+ Choose asset
to swap + + } + variant="bigTitle" + onGoBack={() => navigate(RouteUrls.Swap)} + /> + } + > + +
+ ); +} diff --git a/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-quote.tsx b/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-quote.tsx new file mode 100644 index 00000000000..dffce78d4a0 --- /dev/null +++ b/src/app/pages/swap/components/swap-asset-dialog/swap-asset-dialog-quote.tsx @@ -0,0 +1,33 @@ +import { RouteUrls } from '@shared/route-urls'; + +import { Dialog } from '@app/ui/components/containers/dialog/dialog'; +import { Header } from '@app/ui/components/containers/headers/header'; + +import { useSwapNavigate } from '../../hooks/use-swap-navigate'; +import { useSwapContext } from '../../swap.context'; +import { SwapAssetList } from './components/swap-asset-list'; + +export function SwapAssetDialogQuote() { + const { swappableAssetsQuote } = useSwapContext(); + const navigate = useSwapNavigate(); + + return ( + navigate(RouteUrls.Swap)} + header={ +
+ Choose asset
to receive + + } + variant="bigTitle" + onGoBack={() => navigate(RouteUrls.Swap)} + /> + } + > + +
+ ); +} diff --git a/src/app/pages/swap/components/select-asset-trigger-button.tsx b/src/app/pages/swap/components/swap-asset-select/components/select-asset-trigger-button.tsx similarity index 94% rename from src/app/pages/swap/components/select-asset-trigger-button.tsx rename to src/app/pages/swap/components/swap-asset-select/components/select-asset-trigger-button.tsx index d51a9747d3a..c76294f5e38 100644 --- a/src/app/pages/swap/components/select-asset-trigger-button.tsx +++ b/src/app/pages/swap/components/swap-asset-select/components/select-asset-trigger-button.tsx @@ -9,13 +9,13 @@ import { ChevronDownIcon } from '@app/ui/icons/chevron-down-icon'; interface SelectAssetTriggerButtonProps { icon?: string; name: string; - onChooseAsset(): void; + onSelectAsset(): void; symbol: string; } export function SelectAssetTriggerButton({ icon, name, - onChooseAsset, + onSelectAsset, symbol, }: SelectAssetTriggerButtonProps) { const [field] = useField(name); @@ -24,7 +24,7 @@ export function SelectAssetTriggerButton({ return (