From 8b73bbf8b150a1aa842e8ef09add7c47dbc67798 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 27 Jan 2025 22:01:10 -0500 Subject: [PATCH 1/3] add retry and error messages --- src/components/Icon.tsx | 6 ++ src/icons/error-exclamation.svg | 3 + src/icons/index.ts | 2 + src/icons/refresh.svg | 3 + .../dialogs/DepositDialog2/DepositForm.tsx | 25 ++++--- .../dialogs/DepositDialog2/DepositSteps.tsx | 72 ++++++++++++++----- src/views/dialogs/DepositDialog2/utils.ts | 58 ++++++++++++--- tailwind.config.js | 22 +++++- 8 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 src/icons/error-exclamation.svg create mode 100644 src/icons/refresh.svg diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 96d13762b..d966b1646 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -31,6 +31,7 @@ import { DiscordIcon, DownloadIcon, EarthIcon, + ErrorExclamationIcon, EtherscanIcon, ExportKeysIcon, FastForwardIcon, @@ -75,6 +76,7 @@ import { PrivacyIcon, QrIcon, QuestionMarkIcon, + RefreshIcon, RewardStarIcon, RocketIcon, RoundedArrowIcon, @@ -137,6 +139,7 @@ export enum IconName { Download = 'Download', Earth = 'Earth', Etherscan = 'Etherscan', + ErrorExclamation = 'ErrorExclamation', ExportKeys = 'ExportKeys', FastForward = 'FastForward', Feedback = 'Feedback', @@ -182,6 +185,7 @@ export enum IconName { Privacy = 'Privacy', Qr = 'Qr', QuestionMark = 'QuestionMark', + Refresh = 'Refresh', RewardStar = 'RewardStar', Rocket = 'Rocket', RoundedArrow = 'RoundedArrow', @@ -241,6 +245,7 @@ const icons = { [IconName.Discord]: DiscordIcon, [IconName.Download]: DownloadIcon, [IconName.Earth]: EarthIcon, + [IconName.ErrorExclamation]: ErrorExclamationIcon, [IconName.Etherscan]: EtherscanIcon, [IconName.ExportKeys]: ExportKeysIcon, [IconName.FastForward]: FastForwardIcon, @@ -286,6 +291,7 @@ const icons = { [IconName.Privacy]: PrivacyIcon, [IconName.Qr]: QrIcon, [IconName.QuestionMark]: QuestionMarkIcon, + [IconName.Refresh]: RefreshIcon, [IconName.RewardStar]: RewardStarIcon, [IconName.Rocket]: RocketIcon, [IconName.RoundedArrow]: RoundedArrowIcon, diff --git a/src/icons/error-exclamation.svg b/src/icons/error-exclamation.svg new file mode 100644 index 000000000..7ff0f1190 --- /dev/null +++ b/src/icons/error-exclamation.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/index.ts b/src/icons/index.ts index 5a6612890..f63c7ef4f 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -26,6 +26,7 @@ export { default as DepthChartIcon } from './depth-chart.svg'; export { default as DiscordIcon } from './discord.svg'; export { default as DownloadIcon } from './download.svg'; export { default as EarthIcon } from './earth.svg'; +export { default as ErrorExclamationIcon } from './error-exclamation.svg'; export { default as ExportKeysIcon } from './export-keys.svg'; export { default as FastForwardIcon } from './fast-forward.svg'; export { default as FeedbackIcon } from './feedback.svg'; @@ -65,6 +66,7 @@ export { default as PrivacyIcon } from './privacy.svg'; export { default as ProfileIcon } from './profile.svg'; export { default as QrIcon } from './qr.svg'; export { default as QuestionMarkIcon } from './question-mark.svg'; +export { default as RefreshIcon } from './refresh.svg'; export { default as RewardStarIcon } from './reward-star.svg'; export { default as RocketIcon } from './rocket.svg'; export { default as RoundedArrowIcon } from './rounded-arrow.svg'; diff --git a/src/icons/refresh.svg b/src/icons/refresh.svg new file mode 100644 index 000000000..25f00a689 --- /dev/null +++ b/src/icons/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/views/dialogs/DepositDialog2/DepositForm.tsx b/src/views/dialogs/DepositDialog2/DepositForm.tsx index 18d316d7a..d7a2494c6 100644 --- a/src/views/dialogs/DepositDialog2/DepositForm.tsx +++ b/src/views/dialogs/DepositDialog2/DepositForm.tsx @@ -100,7 +100,7 @@ export const DepositForm = ({ const [depositSteps, setDepositSteps] = useState(); const [currentStep, setCurrentStep] = useState(0); - const [showRetryCurrentStep, setShowRetryCurrentStep] = useState(false); + const [currentStepError, setCurrentStepError] = useState(); const [awaitingWalletAction, setAwaitingWalletAction] = useState(false); // Helpers for fetching updated values within the useEffect for autoPromptStep @@ -116,11 +116,16 @@ export const DepositForm = ({ useEffect(() => { async function autoPromptStep() { - if (!depositSteps || !depositSteps.length || !walletClientRef.current) { + if ( + !depositSteps || + !depositSteps.length || + !walletClientRef.current || + !depositSteps[currentStep] + ) { return; } - const success = await depositSteps[currentStep]?.executeStep( + const { success, errorMessage } = await depositSteps[currentStep].executeStep( walletClientRef.current, skipClientRef.current ); @@ -128,7 +133,7 @@ export const DepositForm = ({ setCurrentStep((prev) => prev + 1); } if (!success) { - setShowRetryCurrentStep(true); + setCurrentStepError(errorMessage); } } @@ -140,7 +145,7 @@ export const DepositForm = ({ setDepositSteps(undefined); setCurrentStep(0); setAwaitingWalletAction(false); - setShowRetryCurrentStep(false); + setCurrentStepError(undefined); }, [token, debouncedAmount, selectedRoute]); const onDepositClick = async () => { @@ -148,7 +153,7 @@ export const DepositForm = ({ setAwaitingWalletAction(true); if (steps.length === 1) { - const success = await steps[0]?.executeStep(walletClient, skipClient); + const { success } = await steps[0]!.executeStep(walletClient, skipClient); if (!success) setAwaitingWalletAction(false); } else { setDepositSteps(steps); @@ -159,13 +164,13 @@ export const DepositForm = ({ const step = depositSteps?.[currentStep]; if (!step || !walletClient) return; - setShowRetryCurrentStep(false); + setCurrentStepError(undefined); - const success = await step.executeStep(walletClient, skipClient); + const { success, errorMessage } = await step.executeStep(walletClient, skipClient); if (success) { setCurrentStep((prev) => prev + 1); } else { - setShowRetryCurrentStep(true); + setCurrentStepError(errorMessage); } }; @@ -237,7 +242,7 @@ export const DepositForm = ({ diff --git a/src/views/dialogs/DepositDialog2/DepositSteps.tsx b/src/views/dialogs/DepositDialog2/DepositSteps.tsx index d802341b6..ef696014c 100644 --- a/src/views/dialogs/DepositDialog2/DepositSteps.tsx +++ b/src/views/dialogs/DepositDialog2/DepositSteps.tsx @@ -1,5 +1,8 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; +import tw from 'twin.macro'; + +import { ErrorExclamationIcon } from '@/icons'; import ConnectingLine from '@/icons/connecting-line.svg'; import { Icon, IconName } from '@/components/Icon'; @@ -10,7 +13,7 @@ import { DepositStep } from './utils'; type DepositStepsProps = { steps: DepositStep[]; currentStep: number; - showRetry: boolean; + currentStepError?: string; onRetry: () => void; }; @@ -18,25 +21,42 @@ const STEP_TYPE_TO_INFO: { [type: string]: { title: string; icon: ReactNode } } network: { // TODO(deposit2.0): localization title: 'Switch networks', - icon: , + icon: , }, approve: { title: 'Approve USDC', - icon: , + icon: , }, deposit: { title: 'Confirm deposit', - icon: , + icon: , }, }; -export const DepositSteps = ({ steps, currentStep, showRetry, onRetry }: DepositStepsProps) => { +export const DepositSteps = ({ + steps, + currentStep, + currentStepError, + onRetry, +}: DepositStepsProps) => { + const [isShaking, setIsShaking] = useState(false); + useEffect(() => { + if (!currentStepError) { + setIsShaking(false); + return; + } + + setIsShaking(true); + setTimeout(() => setIsShaking(false), 1000); + }, [currentStepError]); + return (
{steps.map((step, i) => { const stepInfo = STEP_TYPE_TO_INFO[step.type]!; + const error = i === currentStep ? currentStepError : undefined; return ( -
+
-
+
{stepInfo.icon} + {error && ( + + )}
-
-
- {stepInfo.title} +
+
+
+ {stepInfo.title} +
+ {error &&
{error}
}
- {showRetry && i === currentStep && ( + {error && ( )}
- {i !== steps.length - 1 && } + {i !== steps.length - 1 && }
); })} diff --git a/src/views/dialogs/DepositDialog2/utils.ts b/src/views/dialogs/DepositDialog2/utils.ts index dfafb21d6..69a0cc55b 100644 --- a/src/views/dialogs/DepositDialog2/utils.ts +++ b/src/views/dialogs/DepositDialog2/utils.ts @@ -1,7 +1,13 @@ import { OfflineSigner } from '@cosmjs/proto-signing'; import { ERC20Approval, RouteResponse, SkipClient, UserAddress } from '@skip-go/client'; import { useQuery } from '@tanstack/react-query'; -import { Address, maxUint256, WalletClient } from 'viem'; +import { + Address, + ChainMismatchError, + maxUint256, + UserRejectedRequestError, + WalletClient, +} from 'viem'; import { useChainId } from 'wagmi'; import ERC20ABI from '@/abi/erc20.json'; @@ -65,10 +71,14 @@ export function getUserAddressesForRoute( }); } +type StepResult = + | { success: true; errorMessage: undefined } + | { success: false; errorMessage: string }; + export type DepositStep = | { type: 'network' | 'approve'; - executeStep: (signer: WalletClient) => Promise; + executeStep: (signer: WalletClient) => Promise; } | { type: 'deposit'; @@ -76,7 +86,7 @@ export type DepositStep = executeStep: ( signer: WalletClient | OfflineSigner, skipClient: SkipClient - ) => Promise; + ) => Promise; }; // Prepares all the steps the user needs to take in their wallet to complete their deposit @@ -111,9 +121,12 @@ export function useDepositSteps({ executeStep: async (signer: WalletClient) => { try { await signer.switchChain({ id: Number(depositToken.chainId) }); - return true; + return { success: true }; } catch (e) { - return false; + return { + success: false, + errorMessage: parseError(e, 'There was an error changing wallet networks.'), + }; } }, }); @@ -171,9 +184,19 @@ export function useDepositSteps({ }); const receipt = await viemClient.waitForTransactionReceipt({ hash: txHash }); // TODO future improvement: also check to see if approval amount is sufficient here - return receipt.status === 'success'; + // TODO(deposit2.0): localization + const isOnChainSuccess = receipt.status === 'success'; + return { + success: isOnChainSuccess, + errorMessage: isOnChainSuccess + ? undefined + : 'Your approval has failed. Please try again.', + } as StepResult; } catch (e) { - return false; + return { + success: false, + errorMessage: parseError(e, 'There was an error with your approval.'), + }; } }, }); @@ -196,9 +219,12 @@ export function useDepositSteps({ onDeposit({ txHash, chainId: chainID }); }, }); - return true; + return { success: true }; } catch (e) { - return false; + return { + success: false, + errorMessage: parseError(e, 'Your deposit has failed. Please try again.'), + }; } }, }); @@ -213,6 +239,7 @@ export function useDepositSteps({ depositRoute?.amountIn, depositRoute?.sourceAssetChainID, depositRoute?.sourceAssetDenom, + walletChainId, ], queryFn: getStepsQuery, }); @@ -236,3 +263,16 @@ function userAddressHelper(route: RouteResponse, userAddresses: UserAddress[]) { } return addressList; } + +// TODO(deposit2.0): localization +function parseError(e: Error, fallbackMessage: string) { + if ('code' in e && e.code === UserRejectedRequestError.code) { + return 'User rejected request.'; + } + + if ('name' in e && e.name === ChainMismatchError.name) { + return 'Please change your wallet network and try again.'; + } + + return fallbackMessage; +} diff --git a/tailwind.config.js b/tailwind.config.js index 634ea722c..e66449c8b 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -124,7 +124,27 @@ export default { borderRadius: ({ theme }) => ({ ...theme('spacing'), }), - extend: {}, + extend: { + animation:{ + 'shake': 'shake 0.82s cubic-bezier(.36,.07,.19,.97) both', + }, + keyframes: { + 'shake' : { + '10%, 90%': { + transform: 'translate3d(-1px, 0, 0)' + }, + '20%, 80%': { + transform: 'translate3d(2px, 0, 0)' + }, + '30%, 50%, 70%': { + transform: 'translate3d(-4px, 0, 0)' + }, + '40%, 60%': { + transform: 'translate3d(4px, 0, 0)' + } + } + } + }, }, plugins: [ plugin(function ({ addUtilities, addComponents }) { From 03221ff21fa19e214ec4c346b9b44d2a47560df8 Mon Sep 17 00:00:00 2001 From: Tina Zheng Date: Mon, 27 Jan 2025 22:10:04 -0500 Subject: [PATCH 2/3] add hover bg --- src/views/dialogs/DepositDialog2/DepositSteps.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/views/dialogs/DepositDialog2/DepositSteps.tsx b/src/views/dialogs/DepositDialog2/DepositSteps.tsx index ef696014c..e0b60501d 100644 --- a/src/views/dialogs/DepositDialog2/DepositSteps.tsx +++ b/src/views/dialogs/DepositDialog2/DepositSteps.tsx @@ -33,6 +33,8 @@ const STEP_TYPE_TO_INFO: { [type: string]: { title: string; icon: ReactNode } } }, }; +const SHAKE_DURATION = 1000; + export const DepositSteps = ({ steps, currentStep, @@ -47,7 +49,7 @@ export const DepositSteps = ({ } setIsShaking(true); - setTimeout(() => setIsShaking(false), 1000); + setTimeout(() => setIsShaking(false), SHAKE_DURATION); }, [currentStepError]); return ( @@ -65,9 +67,7 @@ export const DepositSteps = ({ strokeWidth="2" stroke="" tw="absolute left-0 top-0 flex h-full w-full items-center justify-center text-color-accent" - style={{ - visibility: i === currentStep && !currentStepError ? undefined : 'hidden', - }} + css={(i !== currentStep || currentStepError) && tw`invisible`} />
{stepInfo.title}
@@ -92,7 +92,7 @@ export const DepositSteps = ({ {error && (