Skip to content

Commit

Permalink
[NO CHANGELOGS] [Add Funds Widget] Add Funds Error Handling (#2301)
Browse files Browse the repository at this point in the history
  • Loading branch information
mimi-imtbl authored Oct 14, 2024
1 parent f74c215 commit 18c8840
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { AddFundsWidgetParams } from '@imtbl/checkout-sdk';

import { Stack, CloudImage } from '@biom3/react';
import { Environment } from '@imtbl/config';
import { sendAddFundsCloseEvent } from './AddFundsWidgetEvents';
import { EventTargetContext } from '../../context/event-target-context/EventTargetContext';
import {
Expand Down Expand Up @@ -37,6 +38,10 @@ import { useProvidersContext } from '../../context/providers-context/ProvidersCo
import { ServiceUnavailableErrorView } from '../../views/error/ServiceUnavailableErrorView';
import { ServiceType } from '../../views/error/serviceTypes';
import { getRemoteImage } from '../../lib/utils';
import { isValidAddress } from '../../lib/validations/widgetValidators';
import { amountInputValidation } from '../../lib/validations/amountInputValidations';
import { useError } from './hooks/useError';
import { AddFundsErrorTypes } from './types';

export type AddFundsWidgetInputs = AddFundsWidgetParams & {
config: StrongCheckoutWidgetsConfig;
Expand Down Expand Up @@ -90,6 +95,16 @@ export default function AddFundsWidget({

const squidSdk = useSquid(checkout);
const tokensResponse = useTokens(checkout);
const { showErrorHandover } = useError(checkout.config.environment ?? Environment.SANDBOX);

useEffect(() => {
const isInvalidToTokenAddress = toTokenAddress && !isValidAddress(toTokenAddress);
const isInvalidToAmount = toAmount && !amountInputValidation(toAmount);

if (isInvalidToTokenAddress || isInvalidToAmount) {
showErrorHandover(AddFundsErrorTypes.INVALID_PARAMETERS);
}
}, [toTokenAddress, toAmount]);

useEffect(() => {
(async () => {
Expand Down
146 changes: 146 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/add-funds/hooks/useError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Environment } from '@imtbl/config';
import { useContext } from 'react';
import { AddFundsErrorTypes, RiveStateMachineInput } from '../types';
import { useHandover } from '../../../lib/hooks/useHandover';
import { HandoverTarget } from '../../../context/handover-context/HandoverContext';
import { getRemoteRive } from '../../../lib/utils';
import { APPROVE_TXN_ANIMATION } from '../utils/config';
import { HandoverContent } from '../../../components/Handover/HandoverContent';
import { sendAddFundsCloseEvent } from '../AddFundsWidgetEvents';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import { ViewActions, ViewContext } from '../../../context/view-context/ViewContext';
import { AddFundsWidgetViews } from '../../../context/view-context/AddFundsViewContextTypes';
import { useAnalytics, UserJourney } from '../../../context/analytics-provider/SegmentAnalyticsProvider';

interface ErrorConfig {
headingText: string;
subHeadingText?: string;
primaryButtonText?: string;
onPrimaryButtonClick?: () => void;
secondaryButtonText?: string;
onSecondaryButtonClick?: () => void;
}

export const useError = (environment: Environment) => {
const { viewDispatch } = useContext(ViewContext);

const { page } = useAnalytics();
const { addHandover, closeHandover } = useHandover({
id: HandoverTarget.GLOBAL,
});

const {
eventTargetState: { eventTarget },
} = useContext(EventTargetContext);

const closeWidget = () => {
sendAddFundsCloseEvent(eventTarget);
};

const goBackToAddFundsView = () => {
closeHandover();

viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
view: {
type: AddFundsWidgetViews.ADD_FUNDS,
},
},
});
};

const errorConfig: Record<AddFundsErrorTypes, ErrorConfig> = {
[AddFundsErrorTypes.DEFAULT]: {
headingText: 'Unknown error',
subHeadingText: 'An unknown error occurred. Please try again later.',
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.INVALID_PARAMETERS]: {
headingText: 'Invalid parameters',
subHeadingText: 'The parameters provided are invalid. Please check again.',
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.SERVICE_BREAKDOWN]: {
headingText: 'Our system is currently down',
subHeadingText: 'We are currently experiencing technical difficulties. Please try again later.',
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.TRANSACTION_FAILED]: {
headingText: 'Transaction failed',
subHeadingText: 'The transaction failed. Please try again.',
primaryButtonText: 'Retry',
onPrimaryButtonClick: goBackToAddFundsView,
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.WALLET_FAILED]: {
headingText: 'Transaction failed',
subHeadingText: 'The transaction failed. Please try again.',
primaryButtonText: 'Retry',
onPrimaryButtonClick: goBackToAddFundsView,
secondaryButtonText: 'Close',
onSecondaryButtonClick: goBackToAddFundsView,
},
[AddFundsErrorTypes.WALLET_REJECTED]: {
headingText: 'Transaction rejected',
subHeadingText: 'The transaction was rejected. Please try again.',
primaryButtonText: 'Retry',
onPrimaryButtonClick: goBackToAddFundsView,
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.WALLET_REJECTED_NO_FUNDS]: {
headingText: 'Insufficient funds',
subHeadingText: 'You do not have enough funds to complete the transaction.',
primaryButtonText: 'Retry',
onPrimaryButtonClick: goBackToAddFundsView,
secondaryButtonText: 'Close',
onSecondaryButtonClick: closeWidget,
},
[AddFundsErrorTypes.WALLET_POPUP_BLOCKED]: {
headingText: "Browser's popup blocked",
subHeadingText: 'Please enable popups in your browser to proceed.',
primaryButtonText: 'Retry',
onPrimaryButtonClick: goBackToAddFundsView,
secondaryButtonText: 'Close',
onSecondaryButtonClick: goBackToAddFundsView,
},
};

const getErrorConfig = (errorType: AddFundsErrorTypes) => errorConfig[errorType];

const showErrorHandover = (errorType: AddFundsErrorTypes, data?: Record<string, unknown>) => {
page({
userJourney: UserJourney.ADD_FUNDS,
screen: 'Error',
extras: {
errorType,
data,
},
});

addHandover({
animationUrl: getRemoteRive(
environment,
APPROVE_TXN_ANIMATION,
),
inputValue: RiveStateMachineInput.ERROR,
children: <HandoverContent
headingText={getErrorConfig(errorType).headingText}
subheadingText={getErrorConfig(errorType).subHeadingText}
primaryButtonText={getErrorConfig(errorType).primaryButtonText}
onPrimaryButtonClick={getErrorConfig(errorType).onPrimaryButtonClick}
secondaryButtonText={getErrorConfig(errorType).secondaryButtonText}
onSecondaryButtonClick={getErrorConfig(errorType).onSecondaryButtonClick}
/>,
});
};

return {
showErrorHandover,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,60 @@ import { Web3Provider } from '@ethersproject/providers';
import { RouteResponse } from '@0xsquid/squid-types';
import { Squid } from '@0xsquid/sdk';
import { ethers } from 'ethers';
import { Environment } from '@imtbl/config';
import { isSquidNativeToken } from '../functions/isSquidNativeToken';
import { useError } from './useError';
import { AddFundsError, AddFundsErrorTypes } from '../types';

export const useExecute = (environment: Environment) => {
const { showErrorHandover } = useError(environment);

const handleTransactionError = (err: unknown) => {
const reason = `${(err as any)?.reason || (err as any)?.message || ''}`.toLowerCase();

let errorType = AddFundsErrorTypes.WALLET_FAILED;

if (reason.includes('failed') && reason.includes('open confirmation')) {
errorType = AddFundsErrorTypes.WALLET_POPUP_BLOCKED;
}

if (reason.includes('rejected') && reason.includes('user')) {
errorType = AddFundsErrorTypes.WALLET_REJECTED;
}

if (reason.includes('failed to submit') && reason.includes('highest gas limit')) {
errorType = AddFundsErrorTypes.WALLET_REJECTED_NO_FUNDS;
}

if (reason.includes('status failed') || reason.includes('transaction failed')) {
errorType = AddFundsErrorTypes.TRANSACTION_FAILED;
}

const error: AddFundsError = {
type: errorType,
data: { error: err },
};

showErrorHandover(errorType, { error });
};

export const useExecute = () => {
const convertToNetworkChangeableProvider = async (
provider: Web3Provider,
): Promise<Web3Provider> => new ethers.providers.Web3Provider(provider.provider, 'any');

const checkProviderChain = async (
provider: Web3Provider,
chainId: string,
): Promise<void> => {
): Promise<boolean> => {
if (!provider.provider.request) {
throw new Error('provider does not have request method');
}

try {
const fromChainHex = `0x${parseInt(chainId, 10).toString(16)}`;
const providerChainId = await provider.provider.request({
method: 'eth_chainId',
});

if (fromChainHex !== providerChainId) {
await provider.provider.request({
method: 'wallet_switchEthereumChain',
Expand All @@ -31,45 +65,53 @@ export const useExecute = () => {
},
],
});
return true;
}
} catch (e) {
throw new Error('Error checking provider');
return true;
} catch (error) {
handleTransactionError(error);
return false;
}
};

const getAllowance = async (
provider: Web3Provider,
routeResponse: RouteResponse,
): Promise<ethers.BigNumber | undefined> => {
if (!isSquidNativeToken(routeResponse?.route?.params.fromToken)) {
const erc20Abi = [
'function allowance(address owner, address spender) public view returns (uint256)',
];
const fromToken = routeResponse?.route.params.fromToken;
const signer = provider.getSigner();
const tokenContract = new ethers.Contract(fromToken, erc20Abi, signer);

const ownerAddress = await signer.getAddress();
const transactionRequestTarget = routeResponse?.route?.transactionRequest?.target;

if (!transactionRequestTarget) {
throw new Error('transactionRequest target is undefined');
try {
if (!isSquidNativeToken(routeResponse?.route?.params.fromToken)) {
const erc20Abi = [
'function allowance(address owner, address spender) public view returns (uint256)',
];
const fromToken = routeResponse?.route.params.fromToken;
const signer = provider.getSigner();
const tokenContract = new ethers.Contract(fromToken, erc20Abi, signer);

const ownerAddress = await signer.getAddress();
const transactionRequestTarget = routeResponse?.route?.transactionRequest?.target;

if (!transactionRequestTarget) {
throw new Error('transactionRequest target is undefined');
}

const allowance = await tokenContract.allowance(
ownerAddress,
transactionRequestTarget,
);
return allowance;
}

const allowance = await tokenContract.allowance(
ownerAddress,
transactionRequestTarget,
);
return allowance;
return ethers.constants.MaxUint256; // no approval is needed for native tokens
} catch (error) {
showErrorHandover(AddFundsErrorTypes.DEFAULT, { error });
return undefined;
}

return ethers.constants.MaxUint256; // no approval is needed for native tokens
};

const approve = async (
provider: Web3Provider,
routeResponse: RouteResponse,
): Promise<void> => {
): Promise<ethers.providers.TransactionReceipt | undefined> => {
try {
if (!isSquidNativeToken(routeResponse?.route?.params.fromToken)) {
const erc20Abi = [
Expand All @@ -93,18 +135,20 @@ export const useExecute = () => {
transactionRequestTarget,
fromAmount,
);
await tx.wait();
return tx.wait();
}
} catch (e) {
throw new Error('Error approving tokens');
return undefined;
} catch (error) {
handleTransactionError(error);
return undefined;
}
};

const execute = async (
squid: Squid,
provider: Web3Provider,
routeResponse: RouteResponse,
): Promise<ethers.providers.TransactionReceipt> => {
): Promise<ethers.providers.TransactionReceipt | undefined> => {
if (!provider.provider.request) {
throw new Error('provider does not have request method');
}
Expand All @@ -115,8 +159,9 @@ export const useExecute = () => {
route: routeResponse.route,
})) as unknown as ethers.providers.TransactionResponse;
return tx.wait();
} catch (e) {
throw new Error('Error executing route');
} catch (error) {
handleTransactionError(error);
return undefined;
}
};

Expand Down
16 changes: 16 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/add-funds/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,19 @@ export enum RiveStateMachineInput {
COMPLETED = 3,
ERROR = 4,
}

export type AddFundsError = {
type: AddFundsErrorTypes;
data?: Record<string, unknown>;
};

export enum AddFundsErrorTypes {
DEFAULT = 'DEFAULT_ERROR',
INVALID_PARAMETERS = 'INVALID_PARAMETERS',
SERVICE_BREAKDOWN = 'SERVICE_BREAKDOWN',
TRANSACTION_FAILED = 'TRANSACTION_FAILED',
WALLET_FAILED = 'WALLET_FAILED',
WALLET_REJECTED = 'WALLET_REJECTED',
WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS',
WALLET_POPUP_BLOCKED = 'WALLET_POPUP_BLOCKED',
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ export const SQUID_SDK_BASE_URL = 'https://apiplus.squidrouter.com';
export const SQUID_API_BASE_URL = 'https://api.squidrouter.com/v1';

export const SQUID_NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';

export const FIXED_HANDOVER_DURATION = 2000;

export const APPROVE_TXN_ANIMATION = '/access_coins.riv';

export const EXECUTE_TXN_ANIMATION = '/swapping_coins.riv';
Loading

0 comments on commit 18c8840

Please sign in to comment.