Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { Environment } from '@imtbl/config';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { PurchaseErrorTypes } from '../types';
import { HandoverTarget } from '../../../context/handover-context/HandoverContext';
import { HandoverContent } from '../../../components/Handover/HandoverContent';
import { sendPurchaseCloseEvent } from '../PurchaseWidgetEvents';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import {
ViewActions,
ViewContext,
} from '../../../context/view-context/ViewContext';
import {
useAnalytics,
UserJourney,
} from '../../../context/analytics-provider/SegmentAnalyticsProvider';
import { RiveStateMachineInput } from '../../../types/HandoverTypes';
import { useHandover } from '../../../lib/hooks/useHandover';
import { APPROVE_TXN_ANIMATION } from '../../../lib/squid/config';
import { getRemoteRive } from '../../../lib/utils';
import { PurchaseWidgetViews } from '../../../context/view-context/PurchaseViewContextTypes';

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 { t } = useTranslation();
const { addHandover, closeHandover } = useHandover({
id: HandoverTarget.GLOBAL,
});

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

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

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

viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
view: {
type: PurchaseWidgetViews.PURCHASE,
},
},
});
};

const errorConfig: Record<PurchaseErrorTypes, ErrorConfig> = {
[PurchaseErrorTypes.DEFAULT]: {
headingText: t('views.PURCHASE.error.default.heading'),
secondaryButtonText: t('views.PURCHASE.error.default.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.INVALID_PARAMETERS]: {
headingText: t('views.PURCHASE.error.invalidParameters.heading'),
subHeadingText: t('views.PURCHASE.error.invalidParameters.subHeading'),
secondaryButtonText: t('views.PURCHASE.error.invalidParameters.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.ROUTE_ERROR]: {
headingText: t('views.PURCHASE.error.routeError.heading'),
subHeadingText: t('views.PURCHASE.error.routeError.subHeading'),
secondaryButtonText: t('views.PURCHASE.error.routeError.secondaryButtonText'),
onSecondaryButtonClick: goBackToPurchaseView,
},
[PurchaseErrorTypes.SERVICE_BREAKDOWN]: {
headingText: t('views.PURCHASE.error.serviceBreakdown.heading'),
subHeadingText: t('views.PURCHASE.error.serviceBreakdown.subHeading'),
secondaryButtonText: t('views.PURCHASE.error.serviceBreakdown.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.TRANSACTION_FAILED]: {
headingText: t('views.PURCHASE.error.transactionFailed.heading'),
subHeadingText: t('views.PURCHASE.error.transactionFailed.subHeading'),
primaryButtonText: t('views.PURCHASE.error.transactionFailed.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.transactionFailed.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.UNRECOGNISED_CHAIN]: {
headingText: t('views.PURCHASE.error.unrecognisedChain.heading'),
subHeadingText: t('views.PURCHASE.error.unrecognisedChain.subHeading'),
primaryButtonText: t('views.PURCHASE.error.unrecognisedChain.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.unrecognisedChain.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.PROVIDER_ERROR]: {
headingText: t('views.PURCHASE.error.providerError.heading'),
subHeadingText: t('views.PURCHASE.error.providerError.subHeading'),
primaryButtonText: t('views.PURCHASE.error.providerError.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.providerError.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.WALLET_FAILED]: {
headingText: t('views.PURCHASE.error.walletFailed.heading'),
subHeadingText: t('views.PURCHASE.error.walletFailed.subHeading'),
primaryButtonText: t('views.PURCHASE.error.walletFailed.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.walletFailed.secondaryButtonText'),
onSecondaryButtonClick: goBackToPurchaseView,
},
[PurchaseErrorTypes.WALLET_REJECTED]: {
headingText: t('views.PURCHASE.error.walletRejected.heading'),
subHeadingText: t('views.PURCHASE.error.walletRejected.subHeading'),
primaryButtonText: t('views.PURCHASE.error.walletRejected.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.walletRejected.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.WALLET_REJECTED_NO_FUNDS]: {
headingText: t('views.PURCHASE.error.walletRejectedNoFunds.heading'),
subHeadingText: t('views.PURCHASE.error.walletRejectedNoFunds.subHeading'),
primaryButtonText: t('views.PURCHASE.error.walletRejectedNoFunds.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.walletRejectedNoFunds.secondaryButtonText'),
onSecondaryButtonClick: closeWidget,
},
[PurchaseErrorTypes.WALLET_POPUP_BLOCKED]: {
headingText: t('views.PURCHASE.error.walletPopupBlocked.heading'),
subHeadingText: t('views.PURCHASE.error.walletPopupBlocked.subHeading'),
primaryButtonText: t('views.PURCHASE.error.walletPopupBlocked.primaryButtonText'),
onPrimaryButtonClick: goBackToPurchaseView,
secondaryButtonText: t('views.PURCHASE.error.walletPopupBlocked.secondaryButtonText'),
onSecondaryButtonClick: goBackToPurchaseView,
},
};

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

const showErrorHandover = (
errorType: PurchaseErrorTypes,
data?: Record<string, unknown>,
) => {
page({
userJourney: UserJourney.PURCHASE,
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
@@ -0,0 +1,70 @@
import { useContext } from 'react';
import { PurchaseError, PurchaseErrorTypes } from '../types';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';

import { useError } from './useError';

import { useProvidersContext } from '../../../context/providers-context/ProvidersContext';
import { sendPurchaseFailedEvent } from '../PurchaseWidgetEvents';
import { PurchaseContext } from '../context/PurchaseContext';

export const useErrorHandler = () => {
const {
purchaseState: { id: contextId },
} = useContext(PurchaseContext);

const {
providersState: {
checkout,
},
} = useProvidersContext();

const { showErrorHandover } = useError(checkout.config.environment);

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

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

let errorType = PurchaseErrorTypes.WALLET_FAILED;

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

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

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

if (
reason.includes('status failed')
|| reason.includes('transaction failed')
) {
errorType = PurchaseErrorTypes.TRANSACTION_FAILED;
sendPurchaseFailedEvent(eventTarget, errorType);
}

if (reason.includes('unrecognized chain')) {
errorType = PurchaseErrorTypes.UNRECOGNISED_CHAIN;
}

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

showErrorHandover(errorType, { contextId, error });
};

return { onTransactionError };
};
19 changes: 19 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/purchase/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,22 @@ export type DirectCryptoPayData = {
export enum SquidRouteOptionType {
SQUID_ROUTE = 'squid-route',
}

export type PurchaseError = {
type: PurchaseErrorTypes;
data?: Record<string, unknown>;
};

export enum PurchaseErrorTypes {
DEFAULT = 'DEFAULT_ERROR',
INVALID_PARAMETERS = 'INVALID_PARAMETERS',
SERVICE_BREAKDOWN = 'SERVICE_BREAKDOWN',
TRANSACTION_FAILED = 'TRANSACTION_FAILED',
UNRECOGNISED_CHAIN = 'UNRECOGNISED_CHAIN',
PROVIDER_ERROR = 'PROVIDER_ERROR',
WALLET_FAILED = 'WALLET_FAILED',
WALLET_REJECTED = 'WALLET_REJECTED',
WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS',
WALLET_POPUP_BLOCKED = 'WALLET_POPUP_BLOCKED',
ROUTE_ERROR = 'ROUTE_ERROR',
}
Loading