From 4f05d8b8db8c29b22da46fa44998705a59ab9b9c Mon Sep 17 00:00:00 2001 From: Jhonatan Gonzalez Date: Tue, 24 Sep 2024 11:35:11 +1000 Subject: [PATCH] [NO CHANGELOG][Checkout Widget] Validate checkout widget inputs (#2209) --- .../definitions/parameters/checkout.ts | 4 +- .../src/widgets/checkout/CheckoutWidget.tsx | 31 ++- .../widgets/checkout/CheckoutWidgetRoot.tsx | 201 +++++++++++++++++- .../functions/deduplicateSaleItemsArray.ts | 20 ++ .../checkout/functions/isValidCheckoutFlow.ts | 19 ++ .../src/components/ui/checkout/checkout.tsx | 7 +- 6 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 packages/checkout/widgets-lib/src/widgets/checkout/functions/deduplicateSaleItemsArray.ts create mode 100644 packages/checkout/widgets-lib/src/widgets/checkout/functions/isValidCheckoutFlow.ts diff --git a/packages/checkout/sdk/src/widgets/definitions/parameters/checkout.ts b/packages/checkout/sdk/src/widgets/definitions/parameters/checkout.ts index a7bae39c0e..aada60035e 100644 --- a/packages/checkout/sdk/src/widgets/definitions/parameters/checkout.ts +++ b/packages/checkout/sdk/src/widgets/definitions/parameters/checkout.ts @@ -42,7 +42,7 @@ export type CheckouWidgetSaleFlowParams = { flow: CheckoutFlowType.SALE; } & SaleWidgetParams; -export type CheckouWidgetAddFundsFlowParams = { +export type CheckoutWidgetAddFundsFlowParams = { flow: CheckoutFlowType.ADD_FUNDS; } & AddFundsWidgetParams; @@ -53,7 +53,7 @@ export type CheckoutWidgetFlowParams = | CheckouWidgetBridgeFlowParams | CheckouWidgetOnRampFlowParams | CheckouWidgetSaleFlowParams - | CheckouWidgetAddFundsFlowParams; + | CheckoutWidgetAddFundsFlowParams; export type CheckoutWidgetParams = { /** The language to use for the checkout widget */ diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx index 3b6ba89b1a..a020928b99 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidget.tsx @@ -31,6 +31,7 @@ import AddFundsWidget from '../add-funds/AddFundsWidget'; import { getViewShouldConnect } from './functions/getViewShouldConnect'; import { useWidgetEvents } from './hooks/useWidgetEvents'; import { getConnectLoaderParams } from './functions/getConnectLoaderParams'; +import { checkoutFlows } from './functions/isValidCheckoutFlow'; export type CheckoutWidgetInputs = { checkout: Checkout; @@ -82,13 +83,39 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) { }); }, [flowParams]); - const showBackButton = !!view.data?.showBackButton; + /** + * If invalid flow set error view + */ + useEffect(() => { + if (checkoutFlows.includes(flowParams.flow)) return; + + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SharedViews.ERROR_VIEW, + error: { + name: 'InvalidViewType', + message: `Invalid view type "${flowParams}"`, + }, + }, + }, + }); + }, [flowParams.flow]); + /** + * Validate if the view requires connect loader + */ const shouldConnectView = useMemo( () => getViewShouldConnect(view.type), [view.type], ); + /* + * Show back button + */ + const showBackButton = !!view.data?.showBackButton; + return ( @@ -96,7 +123,7 @@ export default function CheckoutWidget(props: CheckoutWidgetInputs) { {view.type === SharedViews.LOADING_VIEW && ( )} - {view.type === SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW && ( + {view.type === SharedViews.ERROR_VIEW && ( { sendCheckoutEvent(eventTarget, { diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx index 1ba1ada713..d9f7cb34eb 100644 --- a/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx +++ b/packages/checkout/widgets-lib/src/widgets/checkout/CheckoutWidgetRoot.tsx @@ -5,6 +5,14 @@ import { WidgetProperties, WidgetTheme, WidgetType, + CheckoutWidgetConnectFlowParams, + CheckoutWidgetWalletFlowParams, + CheckoutWidgetAddFundsFlowParams, + CheckouWidgetSwapFlowParams, + CheckouWidgetBridgeFlowParams, + CheckouWidgetOnRampFlowParams, + CheckouWidgetSaleFlowParams, + CheckoutFlowType, } from '@imtbl/checkout-sdk'; import React, { Suspense } from 'react'; import { ThemeProvider } from '../../components/ThemeProvider/ThemeProvider'; @@ -13,6 +21,13 @@ import { HandoverProvider } from '../../context/handover-context/HandoverProvide import { LoadingView } from '../../views/loading/LoadingView'; import { Base } from '../BaseWidgetRoot'; import i18n from '../../i18n'; +import { + isValidAddress, + isValidAmount, + isValidWalletProvider, +} from '../../lib/validations/widgetValidators'; +import { deduplicateSaleItemsArray } from './functions/deduplicateSaleItemsArray'; +import { checkoutFlows } from './functions/isValidCheckoutFlow'; const CheckoutWidget = React.lazy(() => import('./CheckoutWidget')); @@ -37,11 +52,193 @@ export class CheckoutWidgetRoot extends Base { }; } + protected getValidConnectFlowParams(params: CheckoutWidgetConnectFlowParams) { + const validatedParams = { ...params }; + + if (!Array.isArray(validatedParams.blocklistWalletRdns)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "blocklistWalletRdns" widget input'); + validatedParams.blocklistWalletRdns = []; + } + + return validatedParams; + } + + protected getValidWalletFlowParams(params: CheckoutWidgetWalletFlowParams) { + return params; + } + + protected getValidSaleFlowParams(params: CheckouWidgetSaleFlowParams) { + const validatedParams = { ...params }; + + if (!isValidWalletProvider(params.walletProviderName)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "walletProviderName" widget input'); + validatedParams.walletProviderName = undefined; + } + + if (!Array.isArray(validatedParams.items)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "items" widget input.'); + validatedParams.items = []; + } + + if (!params.environmentId) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "environmentId" widget input'); + validatedParams.environmentId = ''; + } + + if (!params.collectionName) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "collectionName" widget input'); + validatedParams.collectionName = ''; + } + + if ( + params.excludePaymentTypes !== undefined + && !Array.isArray(params.excludePaymentTypes) + ) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "excludePaymentTypes" widget input'); + validatedParams.excludePaymentTypes = []; + } + + return { + ...validatedParams, + items: deduplicateSaleItemsArray(params.items), + }; + } + + protected getValidAddFundsFlowParams( + params: CheckoutWidgetAddFundsFlowParams, + ) { + const validatedParams = { ...params }; + + if (validatedParams.showBridgeOption) { + validatedParams.showBridgeOption = true; + } + + if (validatedParams.showOnrampOption) { + validatedParams.showOnrampOption = true; + } + + if (validatedParams.showSwapOption) { + validatedParams.showSwapOption = true; + } + + if (!isValidAmount(validatedParams.toAmount)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "toAmount" widget input'); + validatedParams.toAmount = ''; + } + + if (!isValidAddress(params.toTokenAddress)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "toTokenAddress" widget input'); + validatedParams.toTokenAddress = ''; + } + + return validatedParams; + } + + protected getValidSwapFlowParams(params: CheckouWidgetSwapFlowParams) { + const validatedParams = { ...params }; + + if (!isValidAmount(params.amount)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "amount" widget input'); + validatedParams.amount = ''; + } + + if (!isValidAddress(params.fromTokenAddress)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "fromTokenAddress" widget input'); + validatedParams.fromTokenAddress = ''; + } + + if (!isValidAddress(params.toTokenAddress)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "toTokenAddress" widget input'); + validatedParams.toTokenAddress = ''; + } + + if (params.autoProceed) { + validatedParams.autoProceed = true; + } + + return validatedParams; + } + + protected getValidBridgeFlowParams(params: CheckouWidgetBridgeFlowParams) { + const validatedParams = { ...params }; + + if (!isValidAmount(params.amount)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "amount" widget input'); + validatedParams.amount = ''; + } + + if (!isValidAddress(params.tokenAddress)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "tokenAddress" widget input'); + validatedParams.tokenAddress = ''; + } + + return validatedParams; + } + + protected getValidOnRampFlowParams(params: CheckouWidgetOnRampFlowParams) { + const validatedParams = { ...params }; + + if (!isValidAmount(params.amount)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "amount" widget input'); + validatedParams.amount = ''; + } + + if (!isValidAddress(params.tokenAddress)) { + // eslint-disable-next-line no-console + console.warn('[IMTBL]: invalid "tokenAddress" widget input'); + validatedParams.tokenAddress = ''; + } + + return validatedParams; + } + protected getValidatedParameters( params: CheckoutWidgetParams, ): CheckoutWidgetParams { - // TODO: Validate params for each widget - return params; + // if empty do nothing + if (Object.keys(params).length === 0) { + return params; + } + + const flowType = params.flow; + const supportedFlows = checkoutFlows.join(', '); + + switch (flowType) { + case CheckoutFlowType.CONNECT: + return this.getValidConnectFlowParams(params); + case CheckoutFlowType.WALLET: + return this.getValidWalletFlowParams(params); + case CheckoutFlowType.SALE: + return this.getValidSaleFlowParams(params); + case CheckoutFlowType.SWAP: + return this.getValidSwapFlowParams(params); + case CheckoutFlowType.BRIDGE: + return this.getValidBridgeFlowParams(params); + case CheckoutFlowType.ONRAMP: + return this.getValidOnRampFlowParams(params); + case CheckoutFlowType.ADD_FUNDS: + return this.getValidAddFundsFlowParams(params); + default: + // eslint-disable-next-line no-console + console.warn( + `[IMTBL]: invalid "flow: ${flowType}" widget input, must be one of the following: ${supportedFlows}`, + ); + return params; + } } protected render() { diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/functions/deduplicateSaleItemsArray.ts b/packages/checkout/widgets-lib/src/widgets/checkout/functions/deduplicateSaleItemsArray.ts new file mode 100644 index 0000000000..886e43ab48 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/checkout/functions/deduplicateSaleItemsArray.ts @@ -0,0 +1,20 @@ +import { SaleItem } from '@imtbl/checkout-sdk'; + +export function deduplicateSaleItemsArray(items: SaleItem[] | undefined): SaleItem[] { + if (!items || !Array.isArray(items)) return []; + + const uniqueItems = items.reduce((acc, item) => { + const itemIndex = acc.findIndex( + ({ productId }) => productId === item.productId, + ); + + if (itemIndex !== -1) { + acc[itemIndex] = { ...item, qty: acc[itemIndex].qty + item.qty }; + return acc; + } + + return [...acc, { ...item }]; + }, [] as SaleItem[]); + + return uniqueItems; +} diff --git a/packages/checkout/widgets-lib/src/widgets/checkout/functions/isValidCheckoutFlow.ts b/packages/checkout/widgets-lib/src/widgets/checkout/functions/isValidCheckoutFlow.ts new file mode 100644 index 0000000000..cf785fcd46 --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/checkout/functions/isValidCheckoutFlow.ts @@ -0,0 +1,19 @@ +import { CheckoutFlowType } from '@imtbl/checkout-sdk'; + +/** Orchestration Events List */ +export const checkoutFlows = [ + CheckoutFlowType.CONNECT, + CheckoutFlowType.WALLET, + CheckoutFlowType.SALE, + CheckoutFlowType.SWAP, + CheckoutFlowType.BRIDGE, + CheckoutFlowType.ONRAMP, + CheckoutFlowType.ADD_FUNDS, +]; + +/** + * Check if event is orchestration event + */ +export function isValidCheckoutFlow(flow: string): boolean { + return checkoutFlows.includes(flow as CheckoutFlowType); +} diff --git a/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx b/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx index ea5058db35..4a7df9d57a 100644 --- a/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx +++ b/packages/checkout/widgets-sample-app/src/components/ui/checkout/checkout.tsx @@ -340,7 +340,7 @@ function CheckoutUI() { // mount & re-render widget everytime params change useEffect(() => { - if (params == undefined) return; + if (params?.flow === undefined) return; if (renderAfterConnect && !web3Provider) return; mount(); @@ -659,6 +659,11 @@ function CheckoutUI() { {flow} ))} + + + {"INVALID FLOW TYPE"} + + )}