From 1cc306c236da9095e2a4c1c1e690b1fcf90bd688 Mon Sep 17 00:00:00 2001 From: Alejandro Loaiza Date: Tue, 10 Dec 2024 10:24:57 +1100 Subject: [PATCH 1/2] fix: Add tokens handle get routes errors (#2459) --- .../src/lib/squid/hooks/useRoutes.ts | 59 +++++++++++++------ .../checkout/widgets-lib/src/locales/en.json | 5 ++ .../checkout/widgets-lib/src/locales/ja.json | 5 ++ .../checkout/widgets-lib/src/locales/ko.json | 5 ++ .../checkout/widgets-lib/src/locales/zh.json | 5 ++ .../src/widgets/add-tokens/hooks/useError.tsx | 6 ++ .../src/widgets/add-tokens/types.ts | 1 + .../src/widgets/add-tokens/views/Review.tsx | 13 +++- 8 files changed, 78 insertions(+), 21 deletions(-) diff --git a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts index 29da3b76de..31ead28296 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts @@ -155,25 +155,46 @@ export const useRoutes = () => { fromAmount: string, fromAddress?: string, quoteOnly = true, - ): Promise => await retry( - () => squid.getRoute({ - fromChain: fromToken.chainId, - fromToken: fromToken.address, - fromAmount: convertToFormattedAmount(fromAmount, fromToken.decimals), - toChain: toToken.chainId, - toToken: toToken.address, - fromAddress, - toAddress, - quoteOnly, - enableBoost: true, - receiveGasOnDestination: !isPassportProvider(toProvider), - }), - { - retryIntervalMs: 1000, - retries: 5, - nonRetryable: (err: any) => err.response.status !== 429, - }, - ); + ): Promise => { + try { + return await retry( + () => squid.getRoute({ + fromChain: fromToken.chainId, + fromToken: fromToken.address, + fromAmount: convertToFormattedAmount(fromAmount, fromToken.decimals), + toChain: toToken.chainId, + toToken: toToken.address, + fromAddress, + toAddress, + quoteOnly, + enableBoost: true, + receiveGasOnDestination: !isPassportProvider(toProvider), + }), + { + retryIntervalMs: 1000, + retries: 5, + nonRetryable: (err: any) => err.response?.status !== 429, + }, + ); + } catch (error: any) { + track({ + userJourney: UserJourney.ADD_TOKENS, + screen: 'Routes', + action: 'Failed', + extras: { + contextId: id, + fromToken: fromToken.symbol, + toToken: toToken.symbol, + fromChain: fromToken.chainId, + toChain: toToken.chainId, + errorStatus: error.response?.status, + errorMessage: error.response?.data?.message, + errorStack: error.stack, + }, + }); + throw error; + } + }; const isRouteToAmountGreaterThanToAmount = ( routeResponse: RouteResponse, diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 2888458a28..0e2e77671e 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -769,6 +769,11 @@ "heading": "Oops! It seems your payment wallet has changed", "body": "You'll be ask to re-connect the same wallet you selected to pay with before proceeding", "buttonText": "Re-select payment wallet" + }, + "routeError": { + "heading": "We couldn't find a suitable final quote", + "subHeading": "Please try again", + "secondaryButtonText": "Go back" } }, "review": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 403e3016ac..b8f4ce7e3e 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -752,6 +752,11 @@ "heading": "おっと!支払いウォレットが変更されました", "body": "以前に選択したウォレットと再接続するように求められます", "buttonText": "支払いウォレットを再選択" + }, + "routeError": { + "heading": "適切な最終見積もりが見つかりませんでした", + "subHeading": "もう一度お試しください", + "secondaryButtonText": "戻る" } }, "review": { diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index 981428260e..942ddf2424 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -749,6 +749,11 @@ "heading": "앗! 결제 지갑이 변경되었습니다", "body": "이전에 선택한 지갑과 다시 연결하라는 메시지가 표시됩니다", "buttonText": "결제 지갑 재선택" + }, + "routeError": { + "heading": "적절한 최종 견적을 찾을 수 없습니다", + "subHeading": "다시 시도해 주세요", + "secondaryButtonText": "돌아가기" } }, "review": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 668efdbb45..2bf2e1a903 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -749,6 +749,11 @@ "heading": "抱歉!您的支付钱包似乎已更改", "body": "在继续之前,您需要重新连接之前选择的支付钱包。", "buttonText": "重新选择支付钱包" + }, + "routeError": { + "heading": "我们无法找到合适的最终报价", + "subHeading": "请再试一次", + "secondaryButtonText": "返回" } }, "review": { diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useError.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useError.tsx index 3e02fc8910..46a82e0691 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useError.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/hooks/useError.tsx @@ -70,6 +70,12 @@ export const useError = (environment: Environment) => { secondaryButtonText: t('views.ADD_TOKENS.error.invalidParameters.secondaryButtonText'), onSecondaryButtonClick: closeWidget, }, + [AddTokensErrorTypes.ROUTE_ERROR]: { + headingText: t('views.ADD_TOKENS.error.routeError.heading'), + subHeadingText: t('views.ADD_TOKENS.error.routeError.subHeading'), + secondaryButtonText: t('views.ADD_TOKENS.error.routeError.secondaryButtonText'), + onSecondaryButtonClick: goBackToAddTokensView, + }, [AddTokensErrorTypes.SERVICE_BREAKDOWN]: { headingText: t('views.ADD_TOKENS.error.serviceBreakdown.heading'), subHeadingText: t('views.ADD_TOKENS.error.serviceBreakdown.subHeading'), diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/types.ts b/packages/checkout/widgets-lib/src/widgets/add-tokens/types.ts index 2ad881afe7..1a2287c11c 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/types.ts +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/types.ts @@ -28,6 +28,7 @@ export enum AddTokensErrorTypes { WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS', WALLET_POPUP_BLOCKED = 'WALLET_POPUP_BLOCKED', ENVIRONMENT_ERROR = 'ENVIRONMENT_ERROR', + ROUTE_ERROR = 'ROUTE_ERROR', } export enum AddTokensExperiments { diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx index 8bfb0a7568..d0c4a273e0 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx @@ -122,6 +122,7 @@ export function Review({ const [amountData, setAmountData] = useState(); const [proceedDisabled, setProceedDisabled] = useState(true); const [showFeeBreakdown, setShowFeeBreakdown] = useState(false); + const [showSecuringQuote, setShowSecuringQuote] = useState(false); const [showAddressMissmatchDrawer, setShowAddressMissmatchDrawer] = useState(false); const { getAmountData, getRoute } = useRoutes(); const { addHandover, closeHandover } = useHandover({ @@ -152,6 +153,8 @@ export function Review({ if (!fromAddress || !toAddress) return; + setShowSecuringQuote(true); + const updatedAmountData = getAmountData( tokens, data.balance, @@ -174,10 +177,16 @@ export function Review({ fromAddress, false, ); - setRoute(routeResponse.route); setAmountData(updatedAmountData); setProceedDisabled(false); + setShowSecuringQuote(false); + if (routeResponse?.route === undefined) { + showErrorHandover(AddTokensErrorTypes.ROUTE_ERROR, { + contextId: id, + error: 'Failed to obtain final route', + }); + } }; const { fromChain, toChain } = useMemo( @@ -975,7 +984,7 @@ export function Review({ )} - {!route && !showAddressMissmatchDrawer && ( + {!route && !showAddressMissmatchDrawer && showSecuringQuote && ( Date: Tue, 10 Dec 2024 13:47:59 +1100 Subject: [PATCH 2/2] [NO CHANGELOG] [Add Tokens Widget] Check for sufficient gas (#2456) --- .../src/components/Hero/NoGasHero.tsx | 35 +++++ .../NotEnoughGasDrawer/NotEnoughGasDrawer.tsx | 97 ++++++++++++++ .../functions/sortRoutesByFastestTime.ts | 7 + .../src/lib/squid/hooks/useRoutes.ts | 122 ++++++++++++++---- .../widgets-lib/src/lib/squid/types.ts | 1 + .../checkout/widgets-lib/src/locales/en.json | 9 +- .../checkout/widgets-lib/src/locales/ja.json | 9 +- .../checkout/widgets-lib/src/locales/ko.json | 9 +- .../checkout/widgets-lib/src/locales/zh.json | 9 +- .../add-tokens/components/RouteOption.tsx | 11 ++ .../components/SelectedRouteOption.tsx | 11 ++ .../add-tokens/components/TokenDrawerMenu.tsx | 2 +- .../src/widgets/add-tokens/utils/config.ts | 2 + .../widgets/add-tokens/views/AddTokens.tsx | 29 ++++- .../src/widgets/add-tokens/views/Review.tsx | 5 +- 15 files changed, 326 insertions(+), 32 deletions(-) create mode 100644 packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx create mode 100644 packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx diff --git a/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx b/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx new file mode 100644 index 0000000000..8814e9774a --- /dev/null +++ b/packages/checkout/widgets-lib/src/components/Hero/NoGasHero.tsx @@ -0,0 +1,35 @@ +/* eslint-disable max-len */ +import { Box } from '@biom3/react'; + +export function NoGasHero() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx b/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx new file mode 100644 index 0000000000..1e699aa193 --- /dev/null +++ b/packages/checkout/widgets-lib/src/components/NotEnoughGasDrawer/NotEnoughGasDrawer.tsx @@ -0,0 +1,97 @@ +import { + Body, + Box, + Button, + Drawer, + Heading, +} from '@biom3/react'; +import { useTranslation } from 'react-i18next'; +import { RouteData } from '../../lib/squid/types'; +import { NoGasHero } from '../Hero/NoGasHero'; + +export function NotEnoughGasDrawer({ + visible, + routeData, + onTryAgainClick, + onToolkitClick, +}: { + visible: boolean; + routeData?: RouteData; + onTryAgainClick: () => void; + onToolkitClick: () => void; +}) { + const { t } = useTranslation(); + const tokenName = routeData?.route.route.estimate.gasCosts[0].token.symbol ?? ''; + + return ( + + + + + + + {t('views.ADD_TOKENS.noGasDrawer.heading', { token: tokenName })} + + + + {t('views.ADD_TOKENS.noGasDrawer.body')} + + + + + + + + + + ); +} diff --git a/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts b/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts index 2d425fc91e..e43980a8af 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/functions/sortRoutesByFastestTime.ts @@ -4,8 +4,15 @@ export const sortRoutesByFastestTime = (routes: RouteData[]): RouteData[] => { if (!routes) return []; return routes.slice().sort((a, b) => { + // Prioritize isInsufficientGas = false + if (a.isInsufficientGas !== b.isInsufficientGas) { + return a.isInsufficientGas ? 1 : -1; + } + + // Sort by estimatedRouteDuration if isInsufficientGas is the same const timeA = a.route.route.estimate.estimatedRouteDuration; const timeB = b.route.route.estimate.estimatedRouteDuration; + return timeA - timeB; }); }; diff --git a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts index 31ead28296..713c39ecb9 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/hooks/useRoutes.ts @@ -13,6 +13,7 @@ import { isPassportProvider } from '../../provider'; import { AmountData, RouteData, RouteResponseData, Token, } from '../types'; +import { SQUID_NATIVE_TOKEN } from '../config'; const BASE_SLIPPAGE = 0.02; @@ -293,28 +294,97 @@ export const useRoutes = () => { } }; - const getRoutes = async ( + const getRoutesWithFeesValidation = async ( squid: Squid, - amountDataArray: AmountData[], toTokenAddress: string, + balances: TokenBalance[], + fromAmountArray: AmountData[], ): Promise => { - const routePromises = amountDataArray.map((data) => getRoute( - squid, - data.fromToken, - data.toToken, - toTokenAddress, - data.fromAmount, - data.toAmount, - ).then((route) => ({ - amountData: { ...data, additionalBuffer: route.additionalBuffer }, - route: route.route, - }))); + const getGasCost = ( + route: RouteResponseData, + chainId: string | number, + ) => (route.route?.route.estimate.gasCosts || []) + .filter((gasCost) => gasCost.token.chainId === chainId.toString()) + .reduce( + (sum, gasCost) => sum + parseFloat(utils.formatUnits(gasCost.amount, gasCost.token.decimals)), + 0, + ); + + const getTotalFees = ( + route: RouteResponseData, + chainId: string | number, + ) => (route.route?.route.estimate.feeCosts || []) + .filter((fee) => fee.token.chainId === chainId.toString()) + .reduce( + (sum, fee) => sum + parseFloat(utils.formatUnits(fee.amount, fee.token.decimals)), + 0, + ); + + const findUserGasBalance = (chainId: string | number) => balances.find( + (balance: TokenBalance) => balance.address.toLowerCase() === SQUID_NATIVE_TOKEN.toLowerCase() + && balance.chainId.toString() === chainId.toString(), + ); + + const hasSufficientNativeTokenBalance = ( + userGasBalance: TokenBalance | undefined, + fromAmount: string, + fromToken: Token, + totalGasCost: number, + totalFeeCost: number, + ) => { + if (!userGasBalance) return false; + + const userBalance = parseFloat( + utils.formatUnits(userGasBalance.balance, userGasBalance.decimals), + ); + + // If the fromToken is the native token, validate balance for both fromAmount and gas + fee costs + // Otherwise, only validate balance for gas + fee costs + const requiredAmount = fromToken.address.toLowerCase() === SQUID_NATIVE_TOKEN.toLowerCase() + ? parseFloat(fromAmount) + totalGasCost + totalFeeCost + : totalGasCost + totalFeeCost; + + return userBalance >= requiredAmount; + }; - const routesData = await Promise.all(routePromises); + const routePromises = fromAmountArray.map(async (data: AmountData) => { + try { + const routeResponse = await getRoute( + squid, + data.fromToken, + data.toToken, + toTokenAddress, + data.fromAmount, + data.toAmount, + ); + + if (!routeResponse?.route) return null; + + const gasCost = getGasCost(routeResponse, data.balance.chainId); + const feeCost = getTotalFees(routeResponse, data.balance.chainId); + const userGasBalance = findUserGasBalance(data.balance.chainId); + + return { + amountData: data, + route: routeResponse.route, + isInsufficientGas: !hasSufficientNativeTokenBalance( + userGasBalance, + data.fromAmount, + data.fromToken, + gasCost, + feeCost, + ), + } as RouteData; + } catch (error) { + return null; + } + }); - return routesData.filter( - (route): route is RouteData => route?.route !== undefined, + const routesData = (await Promise.all(routePromises)).filter( + (route): route is RouteData => route !== null, ); + + return routesData; }; const fetchRoutesWithRateLimit = async ( @@ -330,7 +400,7 @@ export const useRoutes = () => { ): Promise => { const currentRequestId = ++latestRequestIdRef.current; - let amountDataArray = getSufficientFromAmounts( + let fromAmountDataArray = getSufficientFromAmounts( tokens, balances, toChanId, @@ -339,23 +409,28 @@ export const useRoutes = () => { ); if (!isSwapAllowed) { - amountDataArray = amountDataArray.filter( + fromAmountDataArray = fromAmountDataArray.filter( (amountData) => amountData.balance.chainId !== toChanId, ); } let allRoutes: RouteData[] = []; await Promise.all( - amountDataArray + fromAmountDataArray .reduce((acc, _, i) => { if (i % bulkNumber === 0) { - acc.push(amountDataArray.slice(i, i + bulkNumber)); + acc.push(fromAmountDataArray.slice(i, i + bulkNumber)); } return acc; - }, [] as (typeof amountDataArray)[]) - .map(async (slicedAmountDataArray) => { + }, [] as (typeof fromAmountDataArray)[]) + .map(async (slicedFromAmountDataArray) => { allRoutes.push( - ...(await getRoutes(squid, slicedAmountDataArray, toTokenAddress)), + ...(await getRoutesWithFeesValidation( + squid, + toTokenAddress, + balances, + slicedFromAmountDataArray, + )), ); await delay(delayMs); }), @@ -370,7 +445,6 @@ export const useRoutes = () => { } const sortedRoutes = sortRoutesByFastestTime(allRoutes); - // Only update routes if the request is the latest one if (currentRequestId === latestRequestIdRef.current) { setRoutes(sortedRoutes); diff --git a/packages/checkout/widgets-lib/src/lib/squid/types.ts b/packages/checkout/widgets-lib/src/lib/squid/types.ts index 29135f5f33..dd1ae23150 100644 --- a/packages/checkout/widgets-lib/src/lib/squid/types.ts +++ b/packages/checkout/widgets-lib/src/lib/squid/types.ts @@ -38,6 +38,7 @@ export type AmountData = { export type RouteData = { amountData: AmountData; route: RouteResponse; + isInsufficientGas: boolean; }; export type RouteResponseData = { diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 0e2e77671e..81fbe2c213 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -866,7 +866,14 @@ "fiatPricePrefix": "≈ USD", "zeroFees": "Zero fees", "includedFees": "Fees" - } + }, + "noGasDrawer": { + "heading": "You'll need more {{token}} for gas", + "body": "Add more gas and return to complete this transaction, or try another option such as lowering your amount, using another wallet, or buying with card.", + "primaryAction": "Try another option", + "secondaryAction": "Add more {{token}}" + }, + "noGasRouteMessage": "Insufficient {{token}} for gas" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index b8f4ce7e3e..37c7a48450 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -849,7 +849,14 @@ "fiatPricePrefix": "≈ USD", "zeroFees": "手数料無料", "includedFees": "手数料" - } + }, + "noGasDrawer": { + "heading": "{{token}} がガスにもっと必要です", + "body": "ガスを追加してこの取引を完了するか、金額を減らす、別のウォレットを使用する、またはカードで購入するなど、他のオプションを試してください。", + "primaryAction": "別のオプションを試す", + "secondaryAction": "{{token}} をさらに追加する" + }, + "noGasRouteMessage": "ガス用の {{token}} が不足しています" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index 942ddf2424..6f65394d6a 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -846,7 +846,14 @@ "fiatPricePrefix": "≈ USD", "zeroFees": "수수료 없음", "includedFees": "수수료" - } + }, + "noGasDrawer": { + "heading": "가스에 {{token}}이(가) 더 필요합니다", + "body": "가스를 추가하고 이 거래를 완료하거나 금액을 줄이기, 다른 지갑 사용, 카드로 구매하기와 같은 다른 옵션을 시도해보세요.", + "primaryAction": "다른 옵션 시도하기", + "secondaryAction": "{{token}} 더 추가하기" + }, + "noGasRouteMessage": "가스에 필요한 {{token}}이(가) 부족합니다" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 2bf2e1a903..775b9b6bb5 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -846,7 +846,14 @@ "fiatPricePrefix": "≈ 美元", "zeroFees": "零手续费", "includedFees": "费用" - } + }, + "noGasDrawer": { + "heading": "您需要更多 {{token}} 用于燃料费", + "body": "添加更多燃料费后返回完成此交易,或者尝试其他选项,例如降低金额、使用其他钱包或使用银行卡购买。", + "primaryAction": "尝试其他选项", + "secondaryAction": "添加更多 {{token}}" + }, + "noGasRouteMessage": "用于燃料费的 {{token}} 不足" } }, "footers": { diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx index 7e7311512a..f9c40f6731 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/RouteOption.tsx @@ -98,6 +98,17 @@ export function RouteOption({ {`${t('views.ADD_TOKENS.fees.balance')} ${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${routeBalanceUsd}`} + {routeData.isInsufficientGas && ( + <> +
+ + {t('views.ADD_TOKENS.noGasRouteMessage', { + token: + routeData.route.route.estimate.gasCosts[0].token.symbol, + })} + + + )}
diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx index d2a92d72f5..c0f003cfcf 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/SelectedRouteOption.tsx @@ -182,6 +182,17 @@ export function SelectedRouteOption({ {`${t('views.ADD_TOKENS.fees.balance')} ${t( 'views.ADD_TOKENS.fees.fiatPricePrefix', )} $${routeBalanceUsd}`} + {routeData?.isInsufficientGas && ( + <> +
+ + {t('views.ADD_TOKENS.noGasRouteMessage', { + token: + routeData.route.route.estimate.gasCosts[0].token.symbol, + })} + + + )} diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx index f9b075dc11..43f73da9cb 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/components/TokenDrawerMenu.tsx @@ -215,7 +215,7 @@ export function TokenDrawerMenu({ pos: 'relative', cursor: 'pointer', // eslint-disable-next-line @typescript-eslint/naming-convention - '&:hover > div:first-child': { + '&:hover > div:first-of-type': { boxShadow: ({ base }) => `0 0 0 ${base.border.size[200]} ${base.color.text.body.primary}`, }, }} diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts index 467e9629b6..ed3c268bd4 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/utils/config.ts @@ -9,3 +9,5 @@ export const BLOCK_TXN_ANIMATION = '/blocked.riv'; export const ERROR_TXN_ANIMATION = '/error.riv'; export const TOKEN_PRIORITY_ORDER = ['IMX', 'USDC', 'ETH']; + +export const TOOLKIT_SQUID_URL = 'https://toolkit.immutable.com/squid-bridge/'; diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx index 584a7a0c5a..0ab31e100f 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/AddTokens.tsx @@ -72,6 +72,8 @@ import { getFormattedAmounts } from '../functions/getFormattedNumber'; import { RouteData } from '../../../lib/squid/types'; import { SQUID_NATIVE_TOKEN } from '../../../lib/squid/config'; import { identifyUser } from '../../../lib/analytics/identifyUser'; +import { NotEnoughGasDrawer } from '../../../components/NotEnoughGasDrawer/NotEnoughGasDrawer'; +import { TOOLKIT_SQUID_URL } from '../utils/config'; interface AddTokensProps { checkout: Checkout; @@ -147,6 +149,7 @@ export function AddTokens({ const [fetchingRoutes, setFetchingRoutes] = useState(false); const [insufficientBalance, setInsufficientBalance] = useState(false); const [isAmountInputSynced, setIsAmountInputSynced] = useState(false); + const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false); const debouncedSetSelectedAmount = useRef( debounce((value: string) => { @@ -197,6 +200,7 @@ export function AddTokens({ hasEmbeddedSwap: !!route.route.route.estimate.actions.find( (action) => action.type === ActionType.SWAP, ), + isInsufficientGas: route.isInsufficientGas, }, }); } @@ -543,7 +547,11 @@ export function AddTokens({ const loading = (routeInputsReady || fetchingRoutes) && !(selectedRouteData || insufficientBalance); - const readyToReview = routeInputsReady && !!toAddress && !!selectedRouteData && !loading; + const readyToReview = routeInputsReady + && !!toAddress + && !!selectedRouteData + && !selectedRouteData.isInsufficientGas + && !loading; const handleWalletConnected = ( providerType: 'from' | 'to', @@ -595,6 +603,19 @@ export function AddTokens({ }); }, [id, experiments]); + useEffect(() => { + if (selectedRouteData?.isInsufficientGas) { + setShowNotEnoughGasDrawer(true); + } else { + setShowNotEnoughGasDrawer(false); + } + }, [selectedRouteData]); + + const handleToolkitClick = () => { + setShowNotEnoughGasDrawer(false); + window.open(TOOLKIT_SQUID_URL, '_blank'); + }; + return ( + setShowNotEnoughGasDrawer(false)} + onToolkitClick={handleToolkitClick} + /> ); } diff --git a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx index d0c4a273e0..738b429f69 100644 --- a/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx +++ b/packages/checkout/widgets-lib/src/widgets/add-tokens/views/Review.tsx @@ -50,6 +50,7 @@ import { APPROVE_TXN_ANIMATION, EXECUTE_TXN_ANIMATION, FIXED_HANDOVER_DURATION, + TOOLKIT_SQUID_URL, } from '../utils/config'; import { useAnalytics, @@ -590,7 +591,7 @@ export function Review({ rc={( )} @@ -604,7 +605,7 @@ export function Review({ ), onPrimaryButtonClick: () => { window.open( - 'https://toolkit.immutable.com/squid-bridge/', + TOOLKIT_SQUID_URL, '_blank', 'noreferrer', );