From 9a961b9489608f6666d398a2789d60dee6cfbda3 Mon Sep 17 00:00:00 2001 From: Alejandro Loaiza Date: Tue, 27 Aug 2024 15:46:02 +1000 Subject: [PATCH] feat: Add autoProceed to swap widget (#2112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Luã de Souza --- packages/checkout/sdk/src/sdk.ts | 27 ++- .../widgets/definitions/parameters/swap.ts | 9 + .../view-context/SwapViewContextTypes.ts | 2 +- .../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/swap/SwapWidget.tsx | 69 +++--- .../src/widgets/swap/SwapWidgetRoot.tsx | 7 + .../widgets/swap/components/SwapButton.tsx | 184 +--------------- .../src/widgets/swap/components/SwapForm.tsx | 202 +++++++++++++----- .../src/widgets/swap/context/SwapContext.ts | 19 +- .../swap/views/ApproveERC20Onboarding.tsx | 3 +- .../src/widgets/swap/views/SwapCoins.tsx | 10 +- .../src/components/ui/swap/swap.tsx | 27 ++- yarn.lock | 4 +- 16 files changed, 308 insertions(+), 275 deletions(-) diff --git a/packages/checkout/sdk/src/sdk.ts b/packages/checkout/sdk/src/sdk.ts index 131bed0081..e1f59c9ec3 100644 --- a/packages/checkout/sdk/src/sdk.ts +++ b/packages/checkout/sdk/src/sdk.ts @@ -77,7 +77,7 @@ import { WidgetConfiguration } from './widgets/definitions/configurations'; import { SemanticVersion } from './widgets/definitions/types'; import { validateAndBuildVersion } from './widgets/version'; import { InjectedProvidersManager } from './provider/injectedProvidersManager'; -import { SwapParams, SwapResult } from './types/swap'; +import { SwapParams, SwapQuoteResult, SwapResult } from './types/swap'; const SANDBOX_CONFIGURATION = { baseConfig: { @@ -726,7 +726,7 @@ export class Checkout { } /** - * Fetches the approval and swap transaction details including the quote for the swap. + * Fetches a quote and then performs the approval and swap transaction. * @param {SwapParams} params - The parameters for the swap. * @returns {Promise} - A promise that resolves to the swap result (swap tx, swap tx receipt, quote used in the swap). */ @@ -747,4 +747,27 @@ export class Checkout { params.deadline, ); } + + /** + * Fetches a quote for the swap. + * @param {SwapParams} params - The parameters for the swap. + * @returns {Promise} - A promise that resolves to the swap quote result. + */ + public async swapQuote(params: SwapParams): Promise { + const web3Provider = await provider.validateProvider( + this.config, + params.provider, + ); + return swap.swapQuote( + this.config, + web3Provider, + params.fromToken, + params.toToken, + params.fromAmount, + params.toAmount, + params.slippagePercent, + params.maxHops, + params.deadline, + ); + } } diff --git a/packages/checkout/sdk/src/widgets/definitions/parameters/swap.ts b/packages/checkout/sdk/src/widgets/definitions/parameters/swap.ts index ec5fe7aa34..2c7cbdb714 100644 --- a/packages/checkout/sdk/src/widgets/definitions/parameters/swap.ts +++ b/packages/checkout/sdk/src/widgets/definitions/parameters/swap.ts @@ -1,6 +1,11 @@ import { WalletProviderName } from '../../../types'; import { WidgetLanguage } from '../configurations'; +export enum SwapDirection { + FROM = 'FROM', + TO = 'TO', +} + /** * Swap Widget parameters * @property {string | undefined} amount @@ -19,4 +24,8 @@ export type SwapWidgetParams = { walletProviderName?: WalletProviderName; /** The language to use for the swap widget */ language?: WidgetLanguage; + /** Whether the swap widget should display the form or automatically proceed with the swap */ + autoProceed?: boolean; + /** The direction of the swap */ + direction?: SwapDirection; }; diff --git a/packages/checkout/widgets-lib/src/context/view-context/SwapViewContextTypes.ts b/packages/checkout/widgets-lib/src/context/view-context/SwapViewContextTypes.ts index e4bf78ac23..fd44773307 100644 --- a/packages/checkout/widgets-lib/src/context/view-context/SwapViewContextTypes.ts +++ b/packages/checkout/widgets-lib/src/context/view-context/SwapViewContextTypes.ts @@ -58,7 +58,7 @@ interface SwapInProgressView extends ViewType { } } export interface ApproveERC20SwapData { - approveTransaction: TransactionRequest; + approveTransaction?: TransactionRequest; transaction: TransactionRequest; info: Quote; swapFormInfo: PrefilledSwapForm; diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 185df5ff16..562bbf67d4 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -172,6 +172,11 @@ "loading": { "text": "Swap in progress" } + }, + "PREPARE_SWAP": { + "loading": { + "text": "Calculating swap" + } } }, "APPROVE_ERC20": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 86f118badf..e7574189c2 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -175,6 +175,11 @@ "loading": { "text": "スワップ進行中" } + }, + "PREPARE_SWAP": { + "loading": { + "text": "スワップ計算" + } } }, "APPROVE_ERC20": { diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index b56ce6890d..2ca5791da0 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -171,6 +171,11 @@ "loading": { "text": "교환 진행 중" } + }, + "PREPARE_SWAP": { + "loading": { + "text": "스왑 계산" + } } }, "APPROVE_ERC20": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 827a7d3294..abcd339ac0 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -171,6 +171,11 @@ "loading": { "text": "交换进行中" } + }, + "PREPARE_SWAP": { + "loading": { + "text": "计算交换" + } } }, "APPROVE_ERC20": { diff --git a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidget.tsx b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidget.tsx index ac2f84a8c7..bbff13773b 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidget.tsx @@ -7,10 +7,9 @@ import { useState, } from 'react'; import { - DexConfig, TokenFilterTypes, IMTBLWidgetEvents, SwapWidgetParams, + TokenFilterTypes, IMTBLWidgetEvents, SwapWidgetParams, + SwapDirection, } from '@imtbl/checkout-sdk'; -import { ImmutableConfiguration } from '@imtbl/config'; -import { Exchange } from '@imtbl/dex-sdk'; import { useTranslation } from 'react-i18next'; import { SwapCoins } from './views/SwapCoins'; import { LoadingView } from '../../views/loading/LoadingView'; @@ -61,6 +60,8 @@ export default function SwapWidget({ fromTokenAddress, toTokenAddress, config, + autoProceed, + direction, }: SwapWidgetInputs) { const { t } = useTranslation(); const { @@ -169,30 +170,6 @@ export default function SwapWidget({ // connect loader handle the switch network functionality if (network.chainId !== getL2ChainId(checkout.config)) return; - let dexConfig: DexConfig | undefined; - try { - dexConfig = ( - (await checkout.config.remote.getConfig('dex')) as DexConfig - ); - } catch (err: any) { - showErrorView(err); - return; - } - - const exchange = new Exchange({ - chainId: network.chainId, - baseConfig: new ImmutableConfiguration({ environment }), - secondaryFees: dexConfig.secondaryFees, - overrides: dexConfig.overrides, - }); - - swapDispatch({ - payload: { - type: SwapActions.SET_EXCHANGE, - exchange, - }, - }); - swapDispatch({ payload: { type: SwapActions.SET_NETWORK, @@ -208,6 +185,31 @@ export default function SwapWidget({ })(); }, [checkout, provider]); + useEffect(() => { + swapDispatch({ + payload: { + type: SwapActions.SET_AUTO_PROCEED, + autoProceed: autoProceed ?? false, + direction: direction ?? SwapDirection.FROM, + }, + }); + }, [autoProceed, direction]); + + const cancelAutoProceed = useCallback(() => { + if (autoProceed) { + swapDispatch({ + payload: { + type: SwapActions.SET_AUTO_PROCEED, + autoProceed: false, + direction: SwapDirection.FROM, + }, + }); + } + }, [autoProceed, swapDispatch]); + + const fromAmount = direction === SwapDirection.FROM || direction == null ? amount : undefined; + const toAmount = direction === SwapDirection.TO ? amount : undefined; + return ( @@ -218,14 +220,11 @@ export default function SwapWidget({ {viewState.view.type === SwapWidgetViews.SWAP && ( )} {viewState.view.type === SwapWidgetViews.IN_PROGRESS && ( diff --git a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx index e61aeda164..f68af157c8 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/SwapWidgetRoot.tsx @@ -2,6 +2,7 @@ import React, { Suspense } from 'react'; import { ChainId, IMTBLWidgetEvents, + SwapDirection, SwapWidgetParams, WalletProviderName, WidgetConfiguration, @@ -71,6 +72,10 @@ export class Swap extends Base { validatedParams.toTokenAddress = ''; } + if (params.autoProceed) { + validatedParams.autoProceed = true; + } + return validatedParams; } @@ -131,6 +136,8 @@ export class Swap extends Base { toTokenAddress={this.parameters.toTokenAddress} amount={this.parameters.amount} config={this.strongConfig()} + autoProceed={this.parameters.autoProceed} + direction={this.parameters.direction ?? SwapDirection.FROM} /> diff --git a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapButton.tsx b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapButton.tsx index b2e26dc937..822542f3dc 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapButton.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapButton.tsx @@ -1,188 +1,27 @@ import { Box, Button } from '@biom3/react'; -import { useContext, useState } from 'react'; -import { TransactionResponse } from '@imtbl/dex-sdk'; -import { CheckoutErrorType } from '@imtbl/checkout-sdk'; import { useTranslation } from 'react-i18next'; -import { BigNumber } from 'ethers'; -import { getL2ChainId } from '../../../lib'; -import { PrefilledSwapForm, SwapWidgetViews } from '../../../context/view-context/SwapViewContextTypes'; -import { - ViewContext, - ViewActions, - SharedViews, -} from '../../../context/view-context/ViewContext'; import { swapButtonBoxStyle, swapButtonIconLoadingStyle, } from './SwapButtonStyles'; -import { SwapFormData } from './swapFormTypes'; -import { TransactionRejected } from '../../../components/TransactionRejected/TransactionRejected'; -import { ConnectLoaderContext } from '../../../context/connect-loader-context/ConnectLoaderContext'; -import { UserJourney, useAnalytics } from '../../../context/analytics-provider/SegmentAnalyticsProvider'; -import { isPassportProvider } from '../../../lib/provider'; export interface SwapButtonProps { loading: boolean - updateLoading: (value: boolean) => void validator: () => boolean - transaction: TransactionResponse | null; - data?: SwapFormData; - insufficientFundsForGas: boolean; - openNotEnoughImxDrawer: () => void; - openNetworkSwitchDrawer: () => void; + sendTransaction: () => Promise; } export function SwapButton({ loading, - updateLoading, validator, - transaction, - data, - insufficientFundsForGas, - openNotEnoughImxDrawer, - openNetworkSwitchDrawer, + sendTransaction, }: SwapButtonProps) { const { t } = useTranslation(); - const [showTxnRejectedState, setShowTxnRejectedState] = useState(false); - const { viewDispatch } = useContext(ViewContext); - const { connectLoaderState } = useContext(ConnectLoaderContext); - const { checkout, provider } = connectLoaderState; - const { track } = useAnalytics(); - const sendTransaction = async () => { - const isValid = validator(); - // Tracking swap from data here and is valid or not to understand behaviour - track({ - userJourney: UserJourney.SWAP, - screen: 'SwapCoins', - control: 'Swap', - controlType: 'Button', - extras: { - swapFromAddress: data?.fromTokenAddress, - swapFromAmount: data?.fromAmount, - swapFromTokenSymbol: data?.fromTokenSymbol, - swapToAddress: data?.toTokenAddress, - swapToAmount: data?.toAmount, - swapToTokenSymbol: data?.toTokenSymbol, - isSwapFormValid: isValid, - hasFundsForGas: !insufficientFundsForGas, - }, - }); - if (!isValid) return; - if (!checkout || !provider || !transaction) return; - if (insufficientFundsForGas) { - openNotEnoughImxDrawer(); - return; - } - - try { - // check for switch network here - const currentChainId = await (provider.provider as any).request({ method: 'eth_chainId', params: [] }); - // eslint-disable-next-line radix - const parsedChainId = parseInt(currentChainId.toString()); - if (parsedChainId !== getL2ChainId(checkout.config)) { - openNetworkSwitchDrawer(); - return; - } - } catch (err) { - // eslint-disable-next-line no-console - console.error('Current network check failed', err); - } - - if (!transaction) return; - try { - updateLoading(true); - const prefilledSwapData:PrefilledSwapForm = { - fromAmount: data?.fromAmount || '', - fromTokenAddress: data?.fromTokenAddress || '', - toTokenAddress: data?.toTokenAddress || '', - toAmount: data?.toAmount || '', - }; - - if (transaction.approval) { - // If we need to approve a spending limit first - // send user to Approve ERC20 Onbaording flow - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SwapWidgetViews.APPROVE_ERC20, - data: { - approveTransaction: transaction.approval.transaction, - transaction: transaction.swap.transaction, - info: transaction.quote, - swapFormInfo: prefilledSwapData, - }, - }, - }, - }); - return; - } - const txn = await checkout.sendTransaction({ - provider, - transaction: { - ...transaction.swap.transaction, - gasPrice: (isPassportProvider(provider) ? BigNumber.from(0) : undefined), - }, - }); - - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SwapWidgetViews.IN_PROGRESS, - data: { - transactionResponse: txn.transactionResponse, - swapForm: prefilledSwapData as PrefilledSwapForm, - }, - }, - }, - }); - } catch (err: any) { - // eslint-disable-next-line no-console - console.error(err); - - updateLoading(false); - if (err.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { - setShowTxnRejectedState(true); - return; - } - if (err.type === CheckoutErrorType.UNPREDICTABLE_GAS_LIMIT) { - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SwapWidgetViews.PRICE_SURGE, - data: data as PrefilledSwapForm, - }, - }, - }); - return; - } - if (err.type === CheckoutErrorType.TRANSACTION_FAILED - || err.type === CheckoutErrorType.INSUFFICIENT_FUNDS - || (err.receipt && err.receipt.status === 0)) { - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SwapWidgetViews.FAIL, - reason: 'Transaction failed', - data: data as PrefilledSwapForm, - }, - }, - }); - return; - } - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SharedViews.ERROR_VIEW, - error: err, - }, - }, - }); + const handleClick = async () => { + const canSwap = validator(); + if (canSwap) { + await sendTransaction(); } }; @@ -192,22 +31,13 @@ export function SwapButton({ testId="swap-button" disabled={loading} variant="primary" - onClick={sendTransaction} + onClick={handleClick} size="large" > {loading ? ( ) : t('views.SWAP.swapForm.buttonText')} - setShowTxnRejectedState(false)} - onRetry={() => { - sendTransaction(); - setShowTxnRejectedState(false); - }} - /> ); } diff --git a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx index d6f359423f..5d095e3e6a 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/components/SwapForm.tsx @@ -7,6 +7,7 @@ import { } from '@biom3/react'; import { BigNumber, utils } from 'ethers'; import { TokenInfo, WidgetTheme } from '@imtbl/checkout-sdk'; + import { TransactionResponse } from '@imtbl/dex-sdk'; import { useTranslation } from 'react-i18next'; import { Environment } from '@imtbl/config'; @@ -26,7 +27,6 @@ import { ESTIMATE_DEBOUNCE, getL2ChainId, } from '../../../lib'; -import { quotesProcessor } from '../functions/FetchQuote'; import { SelectInput } from '../../../components/FormComponents/SelectInput/SelectInput'; import { validateFromAmount, @@ -51,6 +51,8 @@ import { processGasFree } from '../functions/processGasFree'; import { processSecondaryFees } from '../functions/processSecondaryFees'; import { processQuoteToken } from '../functions/processQuoteToken'; import { formatQuoteConversionRate } from '../functions/swapConversionRate'; +import { PrefilledSwapForm, SwapWidgetViews } from '../../../context/view-context/SwapViewContextTypes'; +import { TransactionRejected } from '../../../components/TransactionRejected/TransactionRejected'; enum SwapDirection { FROM = 'FROM', @@ -70,18 +72,20 @@ let quoteRequest: CancellablePromise; export interface SwapFromProps { data?: SwapFormData; theme: WidgetTheme; + cancelAutoProceed: () => void; } -export function SwapForm({ data, theme }: SwapFromProps) { +export function SwapForm({ data, theme, cancelAutoProceed }: SwapFromProps) { const { t } = useTranslation(); const { swapState: { allowedTokens, - exchange, tokenBalances, network, + autoProceed, }, } = useContext(SwapContext); + const { connectLoaderState } = useContext(ConnectLoaderContext); const { checkout, provider } = connectLoaderState; const defaultTokenImage = getDefaultTokenImage(checkout?.config.environment, theme); @@ -111,6 +115,7 @@ export function SwapForm({ data, theme }: SwapFromProps) { const [toToken, setToToken] = useState(); const [toTokenError, setToTokenError] = useState(''); const [fromFiatValue, setFromFiatValue] = useState(''); + const [loadedToAndFromTokens, setLoadedToAndFromTokens] = useState(false); // Quote const [quote, setQuote] = useState(null); @@ -143,6 +148,8 @@ export function SwapForm({ data, theme }: SwapFromProps) { const [showUnableToSwapDrawer, setShowUnableToSwapDrawer] = useState(false); const [showNetworkSwitchDrawer, setShowNetworkSwitchDrawer] = useState(false); + const [showTxnRejectedState, setShowTxnRejectedState] = useState(false); + useEffect(() => { if (tokenBalances.length === 0) return; if (!network) return; @@ -190,6 +197,8 @@ export function SwapForm({ data, theme }: SwapFromProps) { isNativeToken(token.address) && data?.toTokenAddress?.toLowerCase() === NATIVE ) || (token.address?.toLowerCase() === data?.toTokenAddress?.toLowerCase()))); } + + setLoadedToAndFromTokens(true); }, [ tokenBalances, allowedTokens, @@ -246,18 +255,17 @@ export function SwapForm({ data, theme }: SwapFromProps) { const processFetchQuoteFrom = async (silently: boolean = false) => { if (!provider) return; - if (!exchange) return; + if (!checkout) return; if (!fromToken) return; if (!toToken) return; try { - const quoteResultPromise = quotesProcessor.fromAmountIn( - exchange, + const quoteResultPromise = checkout.swapQuote({ provider, fromToken, - fromAmount, toToken, - ); + fromAmount, + }); const currentQuoteRequest = CancellablePromise.all([ quoteResultPromise, @@ -332,18 +340,18 @@ export function SwapForm({ data, theme }: SwapFromProps) { const processFetchQuoteTo = async (silently: boolean = false) => { if (!provider) return; - if (!exchange) return; + if (!checkout) return; if (!fromToken) return; if (!toToken) return; try { - const quoteResultPromise = quotesProcessor.fromAmountOut( - exchange, + const quoteResultPromise = checkout.swapQuote({ provider, + fromToken, toToken, + fromAmount: undefined, toAmount, - fromToken, - ); + }); const currentQuoteRequest = CancellablePromise.all([ quoteResultPromise, @@ -461,7 +469,9 @@ export function SwapForm({ data, theme }: SwapFromProps) { }; // Silently refresh the quote - useInterval(() => fetchQuote(true), DEFAULT_QUOTE_REFRESH_INTERVAL); + useInterval(() => { + fetchQuote(true); + }, DEFAULT_QUOTE_REFRESH_INTERVAL); // Fetch quote triggers useEffect(() => { @@ -661,9 +671,106 @@ export function SwapForm({ data, theme }: SwapFromProps) { return isSwapFormValid; }; + const isFormValidForAutoProceed = useMemo(() => { + if (!autoProceed) return false; + if (!loadedToAndFromTokens) return false; + + return !loading; + }, [autoProceed, loading, loadedToAndFromTokens]); + + const canAutoSwap = useMemo(() => { + if (!autoProceed) return false; + if (!isFormValidForAutoProceed) return false; + + const isFormValid = SwapFormValidator(); + + if (!isFormValid) { + cancelAutoProceed(); + return false; + } + + return true; + }, [isFormValidForAutoProceed]); + + const sendTransaction = async () => { + if (!quote) return; + const transaction = quote; + const isValid = SwapFormValidator(); + // Tracking swap from data here and is valid or not to understand behaviour + track({ + userJourney: UserJourney.SWAP, + screen: 'SwapCoins', + control: 'Swap', + controlType: 'Button', + extras: { + swapFromAddress: data?.fromTokenAddress, + swapFromAmount: data?.fromAmount, + swapFromTokenSymbol: data?.fromTokenSymbol, + swapToAddress: data?.toTokenAddress, + swapToAmount: data?.toAmount, + swapToTokenSymbol: data?.toTokenSymbol, + isSwapFormValid: isValid, + hasFundsForGas: !insufficientFundsForGas, + }, + }); + if (!isValid) return; + if (!checkout || !provider || !transaction) return; + if (insufficientFundsForGas) { + cancelAutoProceed(); + openNotEnoughImxDrawer(); + return; + } + + try { + // check for switch network here + const currentChainId = await (provider.provider as any).request({ method: 'eth_chainId', params: [] }); + // eslint-disable-next-line radix + const parsedChainId = parseInt(currentChainId.toString()); + if (parsedChainId !== getL2ChainId(checkout.config)) { + setShowNetworkSwitchDrawer(true); + return; + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('Current network check failed', err); + } + + if (!transaction) return; + + setLoading(true); + const prefilledSwapData:PrefilledSwapForm = { + fromAmount: data?.fromAmount || '', + fromTokenAddress: data?.fromTokenAddress || '', + toTokenAddress: data?.toTokenAddress || '', + toAmount: data?.toAmount || '', + }; + + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SwapWidgetViews.APPROVE_ERC20, + data: { + approveTransaction: transaction.approval?.transaction, + transaction: transaction.swap.transaction, + info: transaction.quote, + swapFormInfo: prefilledSwapData, + }, + }, + }, + }); + }; + + useEffect(() => { + if (!autoProceed) return; + if (!canAutoSwap) return; + sendTransaction(); + }, [canAutoSwap, autoProceed, sendTransaction]); + return ( <> {!isPassportProvider(provider) && ( - { - track({ - userJourney: UserJourney.SWAP, - screen: 'SwapCoins', - control: 'ViewFees', - controlType: 'Button', - }); - }} - sx={{ - paddingBottom: '0', - }} - loading={loading} - /> + { + track({ + userJourney: UserJourney.SWAP, + screen: 'SwapCoins', + control: 'ViewFees', + controlType: 'Button', + }); + }} + sx={{ + paddingBottom: '0', + }} + loading={loading} + /> )} - { - setLoading(value); - }} - loading={loading} - transaction={quote} - data={{ - fromAmount, - toAmount, - fromTokenSymbol: fromToken?.symbol, - fromTokenAddress: fromToken?.address, - toTokenSymbol: toToken?.symbol, - toTokenAddress: toToken?.address, + {!autoProceed && ( + + )} + setShowTxnRejectedState(false)} + onRetry={() => { + sendTransaction(); + setShowTxnRejectedState(false); }} - insufficientFundsForGas={insufficientFundsForGas} - openNotEnoughImxDrawer={openNotEnoughImxDrawer} - openNetworkSwitchDrawer={() => setShowNetworkSwitchDrawer(true)} /> ({ swapState: initialSwapState, @@ -138,6 +149,12 @@ export const swapReducer: Reducer = ( ...state, allowedTokens: action.payload.allowedTokens, }; + case SwapActions.SET_AUTO_PROCEED: + return { + ...state, + autoProceed: action.payload.autoProceed, + direction: action.payload.direction, + }; default: return state; } diff --git a/packages/checkout/widgets-lib/src/widgets/swap/views/ApproveERC20Onboarding.tsx b/packages/checkout/widgets-lib/src/widgets/swap/views/ApproveERC20Onboarding.tsx index f92a8c9ee9..881604f315 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/views/ApproveERC20Onboarding.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/views/ApproveERC20Onboarding.tsx @@ -37,11 +37,12 @@ export function ApproveERC20Onboarding({ data }: ApproveERC20Props) { const { eventTargetState: { eventTarget } } = useContext(EventTargetContext); const isPassport = isPassportProvider(provider); + const noApprovalTransaction = data.approveTransaction === undefined; // Local state const [actionDisabled, setActionDisabled] = useState(false); const [approvalTxnLoading, setApprovalTxnLoading] = useState(false); - const [showSwapTxnStep, setShowSwapTxnStep] = useState(false); + const [showSwapTxnStep, setShowSwapTxnStep] = useState(noApprovalTransaction); const [loading, setLoading] = useState(false); // reject transaction flags const [rejectedSpending, setRejectedSpending] = useState(false); diff --git a/packages/checkout/widgets-lib/src/widgets/swap/views/SwapCoins.tsx b/packages/checkout/widgets-lib/src/widgets/swap/views/SwapCoins.tsx index d4fae90b9a..f8da12f715 100644 --- a/packages/checkout/widgets-lib/src/widgets/swap/views/SwapCoins.tsx +++ b/packages/checkout/widgets-lib/src/widgets/swap/views/SwapCoins.tsx @@ -21,9 +21,11 @@ import { IMX_TOKEN_SYMBOL } from '../../../lib'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { UserJourney, useAnalytics } from '../../../context/analytics-provider/SegmentAnalyticsProvider'; import { isPassportProvider } from '../../../lib/provider'; +import { LoadingView } from '../../../views/loading/LoadingView'; export interface SwapCoinsProps { theme: WidgetTheme; + cancelAutoProceed: () => void; fromAmount?: string; toAmount?: string; fromTokenAddress?: string; @@ -32,6 +34,7 @@ export interface SwapCoinsProps { export function SwapCoins({ theme, + cancelAutoProceed, fromAmount, toAmount, fromTokenAddress, @@ -44,6 +47,7 @@ export function SwapCoins({ const { swapState: { tokenBalances, + autoProceed, }, } = useContext(SwapContext); @@ -79,12 +83,12 @@ export function SwapCoins({ return ( sendSwapWidgetCloseEvent(eventTarget)} /> - )} + ) : ''} footer={} > + {autoProceed && } ); } diff --git a/packages/checkout/widgets-sample-app/src/components/ui/swap/swap.tsx b/packages/checkout/widgets-sample-app/src/components/ui/swap/swap.tsx index 69ae47e433..345512e83a 100644 --- a/packages/checkout/widgets-sample-app/src/components/ui/swap/swap.tsx +++ b/packages/checkout/widgets-sample-app/src/components/ui/swap/swap.tsx @@ -1,17 +1,25 @@ -import { Checkout, SwapEventType, SwapSuccess, WidgetTheme, WidgetType } from '@imtbl/checkout-sdk'; +import { Checkout, SwapDirection, SwapEventType, SwapSuccess, WidgetTheme, WidgetType } from '@imtbl/checkout-sdk'; import { WidgetsFactory } from '@imtbl/checkout-widgets'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; const SWAP_TARGET_ID = 'swap-target' function SwapUI() { + const urlParams = new URLSearchParams(window.location.search); + const checkout = useMemo(() => new Checkout(), []); const factory = useMemo(() => new WidgetsFactory(checkout, {theme: WidgetTheme.DARK}), [checkout]); const swap = useMemo(() => factory.create(WidgetType.SWAP),[factory]); + const [isAutoProceed, setIsAutoProceed] = useState(urlParams.get('isAutoProceed') == 'true'); const updateTheme = (theme: WidgetTheme) => swap.update({config: {theme}}); - + useEffect(() => { - swap.mount(SWAP_TARGET_ID,{amount: '5', fromTokenAddress: '0x3B2d8A1931736Fc321C24864BceEe981B11c3c57', toTokenAddress: "0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439"}); + swap.mount(SWAP_TARGET_ID,{ + amount: '5', + fromTokenAddress: '0x3B2d8A1931736Fc321C24864BceEe981B11c3c57', + toTokenAddress: "0x1CcCa691501174B4A623CeDA58cC8f1a76dc3439", + autoProceed: isAutoProceed, + }); swap.addListener(SwapEventType.SUCCESS, (data: SwapSuccess) => { }) swap.addListener(SwapEventType.FAILURE, (data: any) => { @@ -20,7 +28,11 @@ function SwapUI() { swap.addListener(SwapEventType.CLOSE_WIDGET, (data: any) => { swap.unmount(); }); - }, [swap]) + }, [swap, isAutoProceed]) + + const handleAutoProceedChange = (event: React.ChangeEvent) => { + setIsAutoProceed(event.target.value === 'true'); + }; return (
@@ -30,6 +42,11 @@ function SwapUI() { + Auto Proceed: +
); } diff --git a/yarn.lock b/yarn.lock index d14eea6f67..8fdfe43f9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35416,7 +35416,7 @@ __metadata: resolution: "seaport-core@https://github.com/immutable/seaport-core.git#commit=0633350ec34f21fcede657ff812f11cf7d19144e" dependencies: seaport-types: ^0.0.1 - checksum: 392bce86bbfc4f7c00b65575b238825f4c696bddf5af08be7aa496862e63879375387fd4400f6e900ffee08d65c1f75cf3adad9c6c41ddcf7a3b0389cd73c3c7 + checksum: d8adba0d54106c6fe9370f0775fadef2198e5eab440b36919d1f917705ce2f0a7028e4da021b6df049aa3ca35d7e673a28b78a731130f0ff9fdf7a8bd32e3b94 languageName: node linkType: hard @@ -35460,7 +35460,7 @@ __metadata: seaport-sol: ^1.5.0 seaport-types: ^0.0.1 solady: ^0.0.84 - checksum: f31a7443a50fa1c35ec03ea031743d1d10896653ae443fa15ab8e6f5b4a2ca43f6743d523ae4e1f14df867451e5b2b2130b0bfa58a1085b0bcae3fceb8dfdc9b + checksum: a77e141e4ab5d2c4bb190a38fbb6cda3011fdf5f350b250fbeb4d82ae81cf917a966a2dcb8d9e4fd1bed29e5510ede9b15941b0ac77aeb4272dab94c9f51e7ff languageName: node linkType: hard