From 0fc636d05f9b1c57c96b9f2cfcdf4ac1694a2455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Tue, 15 Oct 2024 09:58:36 -0300 Subject: [PATCH 1/7] refactor(request): better view state handling on pay --- src/components/Request/Pay/Pay.tsx | 6 +- .../Request/Pay/Views/Initial.view.tsx | 160 ++++++++++-------- 2 files changed, 95 insertions(+), 71 deletions(-) diff --git a/src/components/Request/Pay/Pay.tsx b/src/components/Request/Pay/Pay.tsx index 2e04f3e80..1cd200df7 100644 --- a/src/components/Request/Pay/Pay.tsx +++ b/src/components/Request/Pay/Pay.tsx @@ -1,12 +1,13 @@ 'use client' -import { createElement, useEffect, useState } from 'react' +import { createElement, useEffect, useState, useContext } from 'react' import * as _consts from './Pay.consts' import * as assets from '@/assets' import { peanut, interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import * as generalViews from './Views/GeneralViews' import * as utils from '@/utils' +import * as context from '@/context' import { useCreateLink } from '@/components/Create/useCreateLink' import { ActionType, estimatePoints } from '@/components/utils/utils' import { type ITokenPriceData } from '@/interfaces' @@ -21,6 +22,7 @@ export const PayRequestLink = () => { const [estimatedGasCost, setEstimatedGasCost] = useState(undefined) const [transactionHash, setTransactionHash] = useState('') const [unsignedTx, setUnsignedTx] = useState(undefined) + const { setLoadingState } = useContext(context.loadingStateContext) const [errorMessage, setErrorMessage] = useState('') const fetchPointsEstimation = async ( @@ -55,6 +57,7 @@ export const PayRequestLink = () => { screen: _consts.PAY_SCREEN_FLOW[newIdx], idx: newIdx, })) + setLoadingState('Idle') } const handleOnPrev = () => { @@ -64,6 +67,7 @@ export const PayRequestLink = () => { screen: _consts.PAY_SCREEN_FLOW[newIdx], idx: newIdx, })) + setLoadingState('Idle') } const checkRequestLink = async (pageUrl: string) => { diff --git a/src/components/Request/Pay/Views/Initial.view.tsx b/src/components/Request/Pay/Views/Initial.view.tsx index 9d3c998ab..341d8755b 100644 --- a/src/components/Request/Pay/Views/Initial.view.tsx +++ b/src/components/Request/Pay/Views/Initial.view.tsx @@ -12,14 +12,40 @@ import { useCreateLink } from '@/components/Create/useCreateLink' import { peanut, interfaces } from '@squirrel-labs/peanut-sdk' import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' import { switchNetwork as switchNetworkUtil } from '@/utils/general.utils' +import { type ITokenPriceData } from '@/interfaces' const ERR_NO_ROUTE = 'No route found to pay in this chain and token' -enum RequestStatus { +enum ViewState { + INITIAL = 'INITIAL', LOADING = 'LOADING', - CLAIM = 'CLAIM', - NOT_CONNECTED = 'NOT_CONNECTED', - NOT_FOUND = 'NOT_FOUND', + READY_TO_PAY = 'READY_TO_PAY', + ERROR = 'ERROR', +} + +async function createXChainUnsignedTx({ + tokenData, + requestLink, + senderAddress, +}: { + tokenData: ITokenPriceData + requestLink: Awaited> + senderAddress: string +}) { + const xchainUnsignedTxs = await peanut.prepareXchainRequestFulfillmentTransaction({ + fromToken: tokenData.address, + fromChainId: tokenData.chainId, + senderAddress, + link: requestLink.link, + squidRouterUrl: 'https://apiplus.squidrouter.com/v2/route', + apiUrl: '/api/proxy/get', + provider: await peanut.getDefaultProvider(tokenData.chainId), + tokenType: utils.isAddressZero(tokenData.address) + ? interfaces.EPeanutLinkType.native + : interfaces.EPeanutLinkType.erc20, + fromTokenDecimals: tokenData.decimals as number, + }) + return xchainUnsignedTxs } export const InitialView = ({ @@ -51,32 +77,22 @@ export const InitialView = ({ }>({ showError: false, errorMessage: '' }) const [txFee, setTxFee] = useState('0') const [isFeeEstimationError, setIsFeeEstimationError] = useState(false) - const [linkState, setLinkState] = useState(RequestStatus.NOT_CONNECTED) + const [viewState, setViewState] = useState(ViewState.INITIAL) const [estimatedFromValue, setEstimatedFromValue] = useState('0') const [tokenRequestedLogoURI, setTokenRequestedLogoURI] = useState(undefined) const [tokenRequestedSymbol, setTokenRequestedSymbol] = useState('') - const createXChainUnsignedTx = async () => { - // This function is only makes sense if selectedTokenData is defined - // Check that it is defined before calling this function - if (!selectedTokenData) { - throw new Error('selectedTokenData must be defined before estimating tx fee') - } - const xchainUnsignedTxs = await peanut.prepareXchainRequestFulfillmentTransaction({ - fromToken: selectedTokenData!.address, - fromChainId: selectedTokenData!.chainId, - senderAddress: address ?? '', - link: requestLinkData.link, - squidRouterUrl: 'https://apiplus.squidrouter.com/v2/route', - apiUrl: '/api/proxy/get', - provider: await peanut.getDefaultProvider(selectedTokenData!.chainId), - tokenType: utils.isAddressZero(selectedTokenData!.address) - ? interfaces.EPeanutLinkType.native - : interfaces.EPeanutLinkType.erc20, - fromTokenDecimals: selectedTokenData!.decimals as number, - }) - return xchainUnsignedTxs - } + const calculatedFee = useMemo(() => { + return isXChain ? txFee : utils.formatTokenAmount(estimatedGasCost, 3) + }, [isXChain, estimatedGasCost, txFee]) + + const isButtonDisabled = useMemo(() => { + return ( + viewState === ViewState.LOADING || + viewState === ViewState.ERROR || + (viewState === ViewState.READY_TO_PAY && !calculatedFee) + ) + }, [viewState, isLoading, calculatedFee]) const fetchTokenSymbol = async (chainId: string, address: string) => { const provider = await peanut.getDefaultProvider(chainId) @@ -87,50 +103,43 @@ export const InitialView = ({ setTokenRequestedSymbol(tokenContract?.symbol ?? '') } - const calculatedFee = useMemo(() => { - return isXChain ? txFee : utils.formatTokenAmount(estimatedGasCost, 3) - }, [isXChain, estimatedGasCost, txFee]) useEffect(() => { const estimateTxFee = async () => { - setLinkState(RequestStatus.LOADING) + setLoadingState('Preparing transaction') if (!isXChain) { - setErrorState({ showError: false, errorMessage: '' }) - setIsFeeEstimationError(false) - setLinkState(RequestStatus.CLAIM) - setLoadingState('Idle') + clearError() + setViewState(ViewState.READY_TO_PAY) return } try { - setErrorState({ showError: false, errorMessage: '' }) - const txData = await createXChainUnsignedTx() + clearError() + const txData = await createXChainUnsignedTx({ + tokenData: selectedTokenData!, + requestLink: requestLinkData, + senderAddress: address!, + }) const { feeEstimation, estimatedFromAmount } = txData setEstimatedFromValue(estimatedFromAmount) if (Number(feeEstimation) > 0) { - setErrorState({ showError: false, errorMessage: '' }) - setIsFeeEstimationError(false) + clearError() setTxFee(Number(feeEstimation).toFixed(2)) - setLinkState(RequestStatus.CLAIM) + setViewState(ViewState.READY_TO_PAY) } else { setErrorState({ showError: true, errorMessage: ERR_NO_ROUTE }) setIsFeeEstimationError(true) setTxFee('0') - setLinkState(RequestStatus.NOT_FOUND) } } catch (error) { setErrorState({ showError: true, errorMessage: ERR_NO_ROUTE }) - setLinkState(RequestStatus.NOT_FOUND) setIsFeeEstimationError(true) setTxFee('0') - } finally { - setLoadingState('Idle') } } - if (!isConnected) return + if (!isConnected || !address) return if (isXChain && !selectedTokenData) { setErrorState({ showError: true, errorMessage: ERR_NO_ROUTE }) - setLinkState(RequestStatus.NOT_FOUND) setIsFeeEstimationError(true) setTxFee('0') return @@ -141,9 +150,7 @@ export const InitialView = ({ useEffect(() => { setLoadingState('Loading') - setErrorState({ showError: false, errorMessage: '' }) - setIsFeeEstimationError(false) - setLinkState(RequestStatus.LOADING) + clearError() const isXChain = selectedChainID !== requestLinkData.chainId || !utils.areTokenAddressesEqual(selectedTokenAddress, requestLinkData.tokenAddress) @@ -172,14 +179,37 @@ export const InitialView = ({ } }, [requestLinkData, tokenPriceData]) + useEffect(() => { + if (isLoading) { + setViewState(ViewState.LOADING) + } + }, [isLoading]) + + useEffect(() => { + if (viewState !== ViewState.LOADING) { + setLoadingState('Idle') + } + }, [viewState]) + + useEffect(() => { + if (errorState.showError) { + setViewState(ViewState.ERROR) + } + }, [errorState]) + useEffect(() => { // Load the token chain pair from the request link data resetTokenAndChain() }, []) + const clearError = () => { + setErrorState({ showError: false, errorMessage: '' }) + setIsFeeEstimationError(false) + } + const handleConnectWallet = async () => { open().finally(() => { - if (isConnected) setLinkState(RequestStatus.LOADING) + if (isConnected) setLoadingState('Loading') }) } @@ -202,7 +232,7 @@ export const InitialView = ({ const handleOnNext = async () => { const amountUsd = (Number(requestLinkData.tokenAmount) * (tokenPriceData?.price ?? 0)).toFixed(2) try { - setErrorState({ showError: false, errorMessage: '' }) + clearError() if (!unsignedTx) return if (!isXChain) { await checkUserHasEnoughBalance({ tokenValue: requestLinkData.tokenAmount }) @@ -244,7 +274,11 @@ export const InitialView = ({ await switchNetwork(selectedTokenData!.chainId) } setLoadingState('Sign in wallet') - const xchainUnsignedTxs = await createXChainUnsignedTx() + const xchainUnsignedTxs = await createXChainUnsignedTx({ + tokenData: selectedTokenData!, + requestLink: requestLinkData, + senderAddress: address ?? '', + }) const { unsignedTxs } = xchainUnsignedTxs const hash = await sendTransactions({ @@ -262,8 +296,6 @@ export const InitialView = ({ errorMessage: errorString, }) console.error('Error while submitting request link fulfillment:', error) - } finally { - setLoadingState('Idle') } } @@ -397,30 +429,18 @@ export const InitialView = ({