Skip to content

Commit

Permalink
[GPR-354] Sale Widget | Handle Default Error and Transaction Error (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jhesgodi authored Jan 18, 2024
1 parent f69d346 commit bd4fa06
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 43 deletions.
18 changes: 18 additions & 0 deletions packages/checkout/sdk/src/blockExplorer/blockExplorer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint @typescript-eslint/naming-convention: off */

import { BLOCKSCOUT_CHAIN_URL_MAP } from '../env';
import { ChainId } from '../types';
import { BlockExplorerService } from './blockExplorer';

describe('BlockExplorerService', () => {
it('Should not return link for unknown chainId', () => {
const url = BlockExplorerService.getTransactionLink('unknown' as unknown as ChainId, '0x123');
expect(url).toBe(undefined);
});

it('Should return link for known chainId', () => {
const expectedUrl = BLOCKSCOUT_CHAIN_URL_MAP[ChainId.IMTBL_ZKEVM_TESTNET].url;
const url = BlockExplorerService.getTransactionLink(ChainId.IMTBL_ZKEVM_TESTNET, '0x123');
expect(url).toBe(`${expectedUrl}/tx/0x123`);
});
});
16 changes: 16 additions & 0 deletions packages/checkout/sdk/src/blockExplorer/blockExplorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BLOCKSCOUT_CHAIN_URL_MAP } from '../env';
import { ChainId } from '../types';

export class BlockExplorerService {
/**
* getTransationLink returns the link to the transaction on blockscout
* @param hash transaction hash
* @returns link to the transaction on blockscout
*/
public static getTransactionLink(chainId: ChainId, hash: string): string | undefined {
const url = BLOCKSCOUT_CHAIN_URL_MAP?.[chainId]?.url;
if (!url || !hash) return undefined;

return `${url}/tx/${hash}`;
}
}
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/blockExplorer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BlockExplorerService } from './blockExplorer';
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,4 @@ export type { ErrorType } from './errors';

export { CheckoutErrorType } from './errors';
export { CheckoutConfiguration } from './config';
export { BlockExplorerService } from './blockExplorer';
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface SaleFailView extends ViewType {
type: SaleWidgetViews.SALE_FAIL;
data?: {
errorType: SaleErrorTypes;
transactionHash?: string;
[key: string]: unknown;
};
}
Expand Down
24 changes: 20 additions & 4 deletions packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
useCallback, useContext, useEffect, useMemo, useReducer, useRef,
} from 'react';

import { SaleWidgetParams } from '@imtbl/checkout-sdk';
import { BlockExplorerService, ChainId, SaleWidgetParams } from '@imtbl/checkout-sdk';
import { Environment } from '@imtbl/config';
import { useTranslation } from 'react-i18next';
import { ConnectLoaderContext } from '../../context/connect-loader-context/ConnectLoaderContext';
Expand Down Expand Up @@ -40,9 +40,9 @@ export function SaleWidget(props: SaleWidgetProps) {
fromTokenAddress,
collectionName,
} = props;

const { connectLoaderState } = useContext(ConnectLoaderContext);
const { checkout, provider } = connectLoaderState;
const chainId = useRef<ChainId>();

const { theme } = config;
const biomeTheme = useMemo(() => widgetTheme(theme), [theme]);
Expand All @@ -53,6 +53,15 @@ export function SaleWidget(props: SaleWidgetProps) {
const loadingText = viewState.view.data?.loadingText
|| t('views.LOADING_VIEW.text');

useEffect(() => {
if (!checkout || !provider) return;

(async () => {
const network = await checkout.getNetworkInfo({ provider });
chainId.current = network.chainId;
})();
}, [checkout, provider]);

const mounted = useRef(false);
const onMount = useCallback(() => {
if (!checkout || !provider) return;
Expand Down Expand Up @@ -93,7 +102,6 @@ export function SaleWidget(props: SaleWidgetProps) {
}}
>
<CryptoFiatProvider environment={config.environment}>

{viewState.view.type === SharedViews.LOADING_VIEW && (
<LoadingView loadingText={loadingText} />
)}
Expand All @@ -107,7 +115,15 @@ export function SaleWidget(props: SaleWidgetProps) {
<PayWithCoins />
)}
{viewState.view.type === SaleWidgetViews.SALE_FAIL && (
<SaleErrorView biomeTheme={biomeTheme} errorType={viewState.view.data?.errorType} />
<SaleErrorView
biomeTheme={biomeTheme}
errorType={viewState.view.data?.errorType}
transactionHash={viewState.view.data?.transactionHash}
blockExplorerLink={BlockExplorerService.getTransactionLink(
chainId.current as ChainId,
viewState.view.data?.transactionHash!,
)}
/>
)}
{viewState.view.type === SaleWidgetViews.SALE_SUCCESS && provider && (
<SaleSuccessView data={viewState.view.data} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export class Sale extends Base<WidgetType.SALE> {
environmentId={this.parameters.environmentId!}
collectionName={this.parameters.collectionName!}
language="en"

/>
</ConnectLoader>
</ThemeProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
useState,
} from 'react';
import { ConnectLoaderState } from '../../../context/connect-loader-context/ConnectLoaderContext';
import { FundWithSmartCheckoutSubViews, SaleWidgetViews } from '../../../context/view-context/SaleViewContextTypes';
import {
FundWithSmartCheckoutSubViews,
SaleWidgetViews,
} from '../../../context/view-context/SaleViewContextTypes';
import {
ViewActions,
ViewContext,
Expand Down Expand Up @@ -67,7 +70,9 @@ type SaleContextValues = SaleContextProps & {
goBackToPaymentMethods: (paymentMethod?: SalePaymentTypes | undefined) => void;
goToErrorView: (type: SaleErrorTypes, data?: Record<string, unknown>) => void;
goToSuccessView: (data?: Record<string, unknown>) => void;
querySmartCheckout: ((callback?: (r?: SmartCheckoutResult) => void) => Promise<SmartCheckoutResult | undefined>);
querySmartCheckout: (
callback?: (r?: SmartCheckoutResult) => void
) => Promise<SmartCheckoutResult | undefined>;
smartCheckoutResult: SmartCheckoutResult | undefined;
fundingRoutes: FundingRoute[];
disabledPaymentTypes: SalePaymentTypes[]
Expand Down Expand Up @@ -231,21 +236,24 @@ export function SaleContextProvider(props: {
[paymentMethod, setPaymentMethod, executeResponse],
);

const goToSuccessView = useCallback((data?: Record<string, unknown>) => {
viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
view: {
type: SaleWidgetViews.SALE_SUCCESS,
data: {
paymentMethod,
transactions: executeResponse.transactions,
...data,
const goToSuccessView = useCallback(
(data?: Record<string, unknown>) => {
viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
view: {
type: SaleWidgetViews.SALE_SUCCESS,
data: {
paymentMethod,
transactions: executeResponse.transactions,
...data,
},
},
},
},
});
}, [[paymentMethod, executeResponse]]);
});
},
[[paymentMethod, executeResponse]],
);

useEffect(() => {
if (!signError) return;
Expand All @@ -270,11 +278,14 @@ export function SaleContextProvider(props: {
goToErrorView(smartCheckoutError.type, smartCheckoutError.data);
}, [smartCheckoutError]);

const querySmartCheckout = useCallback(async (callback?: (r?: SmartCheckoutResult) => void) => {
const result = await smartCheckout();
callback?.(result);
return result;
}, [smartCheckout]);
const querySmartCheckout = useCallback(
async (callback?: (r?: SmartCheckoutResult) => void) => {
const result = await smartCheckout();
callback?.(result);
return result;
},
[smartCheckout],
);

useEffect(() => {
if (!smartCheckoutResult) {
Expand All @@ -294,7 +305,9 @@ export function SaleContextProvider(props: {
if (!smartCheckoutResult.sufficient) {
switch (smartCheckoutResult.router.routingOutcome.type) {
case RoutingOutcomeType.ROUTES_FOUND:
setFundingRoutes(smartCheckoutResult.router.routingOutcome.fundingRoutes);
setFundingRoutes(
smartCheckoutResult.router.routingOutcome.fundingRoutes,
);
viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import {
} from '../types';

const PRIMARY_SALES_API_BASE_URL = {
[Environment.SANDBOX]:
'https://api.sandbox.immutable.com/v1/primary-sales',
[Environment.PRODUCTION]:
'https://api.immutable.com/v1/primary-sales',
[Environment.SANDBOX]: 'https://api.sandbox.immutable.com/v1/primary-sales',
[Environment.PRODUCTION]: 'https://api.immutable.com/v1/primary-sales',
};

type SignApiTransaction = {
Expand Down Expand Up @@ -199,15 +197,13 @@ export const useSignOrder = (input: SignOrderInput) => {
setExecuteTransactions({ method, hash: txnResponse?.hash });
await txnResponse?.wait(1);

transactionHash = txnResponse?.hash;
transactionHash = txnResponse?.hash || '';
return transactionHash;
} catch (e) {
// TODO: check error type to send
// SaleErrorTypes.WALLET_REJECTED or SaleErrorTypes.WALLET_REJECTED_NO_FUNDS

const reason = typeof e === 'string' ? e : (e as any).reason || '';
let errorType = SaleErrorTypes.TRANSACTION_FAILED;
const reason = `${(e as any)?.reason || (e as any)?.message || ''}`.toLowerCase();
transactionHash = (e as any)?.transactionHash;

let errorType = SaleErrorTypes.DEFAULT;
if (reason.includes('rejected') && reason.includes('user')) {
errorType = SaleErrorTypes.WALLET_REJECTED;
}
Expand All @@ -219,6 +215,10 @@ export const useSignOrder = (input: SignOrderInput) => {
errorType = SaleErrorTypes.WALLET_REJECTED_NO_FUNDS;
}

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

setSignError({
type: errorType,
data: { error: e },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ interface ErrorHandlerConfig {
}

type SaleErrorViewProps = {
errorType: SaleErrorTypes | undefined,
biomeTheme: BaseTokens
errorType: SaleErrorTypes | undefined,
transactionHash?: string,
blockExplorerLink?: string,
};

export function SaleErrorView({ errorType = SaleErrorTypes.DEFAULT, biomeTheme }: SaleErrorViewProps) {
export function SaleErrorView({
errorType = SaleErrorTypes.DEFAULT, transactionHash, blockExplorerLink, biomeTheme,
}: SaleErrorViewProps) {
const { t } = useTranslation();
const { goBackToPaymentMethods } = useSaleContext();
const { eventTargetState: { eventTarget } } = useContext(EventTargetContext);
Expand All @@ -33,9 +37,9 @@ export function SaleErrorView({ errorType = SaleErrorTypes.DEFAULT, biomeTheme }
const errorHandlersConfig: Record<SaleErrorTypes, ErrorHandlerConfig> = {
[SaleErrorTypes.TRANSACTION_FAILED]: {
onActionClick: goBackToPaymentMethods,
onSecondaryActionClick: () => {
/* TODO: redirects to Immutascan to check the transaction if has is given */
},
onSecondaryActionClick: transactionHash ? () => {
window.open(blockExplorerLink);
} : closeWidget,
statusType: StatusType.FAILURE,
statusIconStyles: {
fill: biomeTheme.color.status.destructive.dim,
Expand Down Expand Up @@ -94,13 +98,16 @@ export function SaleErrorView({ errorType = SaleErrorTypes.DEFAULT, biomeTheme }

const getErrorViewProps = (): StatusViewProps => {
const handlers = errorHandlersConfig[errorType] || {};
const secondaryActionText = errorType === SaleErrorTypes.TRANSACTION_FAILED && transactionHash
? t(`views.SALE_FAIL.errors.${SaleErrorTypes.DEFAULT}.secondaryAction`)
: t(`views.SALE_FAIL.errors.${errorType}.secondaryAction`);

return {
testId: 'fail-view',
statusText: t(`views.SALE_FAIL.errors.${errorType}.description`),
actionText: t(`views.SALE_FAIL.errors.${errorType}.primaryAction`),
onActionClick: handlers?.onActionClick,
secondaryActionText: t(`views.SALE_FAIL.errors.${errorType}.secondaryAction`),
secondaryActionText,
onSecondaryActionClick: handlers?.onSecondaryActionClick,
onCloseClick: closeWidget,
statusType: handlers.statusType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Passport } from '@imtbl/passport';

const defaultPassportConfig = {
environment: 'sandbox',
clientId: 'XuGsHvMqMJrb73diq1fCswWwn4AYhcM6',
clientId: 'q4gEET7vAKD5jsBWV6j8eoYNKEYpOOw1',
redirectUri: 'http://localhost:3000/sale?login=true',
logoutRedirectUri: 'http://localhost:3000/sale?logout=true',
audience: 'platform_api',
Expand Down

0 comments on commit bd4fa06

Please sign in to comment.