diff --git a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts index d9e9e9c081..3bca542797 100644 --- a/packages/checkout/widgets-lib/src/resources/text/textConfig.ts +++ b/packages/checkout/widgets-lib/src/resources/text/textConfig.ts @@ -321,6 +321,11 @@ export const text = { }, }, }, + [SaleWidgetViews.FUND_WITH_SMART_CHECKOUT]: { + loading: { + checkingBalances: 'Crunching numbers', + }, + }, [SaleWidgetViews.PAYMENT_METHODS]: { header: { heading: 'How would you like to pay?', @@ -379,7 +384,7 @@ export const text = { secondaryAction: 'Dismiss', }, [SaleErrorTypes.WALLET_REJECTED_NO_FUNDS]: { - description: 'Sorry, something went wrong. Plese try again.', + description: 'Sorry, something went wrong. Please try again.', primaryAction: 'Go back', secondaryAction: 'Dismiss', }, @@ -389,6 +394,18 @@ export const text = { primaryAction: 'Try again', secondaryAction: 'Cancel', }, + [SaleErrorTypes.SMART_CHECKOUT_NO_ROUTES_FOUND]: { + description: + 'Your wallet has insufficent balance. Try paying with card instead.', + primaryAction: 'Try again', + secondaryAction: 'Cancel', + }, + [SaleErrorTypes.SMART_CHECKOUT_ERROR]: { + description: + 'Unable to check your wallets balance. Please try again.', + primaryAction: 'Try again', + secondaryAction: 'Cancel', + }, [SaleErrorTypes.DEFAULT]: { description: 'Sorry, something went wrong. Please try again.', primaryAction: 'Try again', diff --git a/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx b/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx index dd7e554df5..1676ec531c 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx @@ -160,6 +160,20 @@ export function SaleWidget(props: SaleWidgetProps) { onSecondaryActionClick: closeWidget, statusType: StatusType.INFORMATION, }, + [SaleErrorTypes.SMART_CHECKOUT_NO_ROUTES_FOUND]: { + onActionClick: () => { + goBackToPaymentMethods(); + }, + onSecondaryActionClick: closeWidget, + statusType: StatusType.INFORMATION, + }, + [SaleErrorTypes.SMART_CHECKOUT_ERROR]: { + onActionClick: () => { + goBackToPaymentMethods(); + }, + onSecondaryActionClick: closeWidget, + statusType: StatusType.INFORMATION, + }, [SaleErrorTypes.DEFAULT]: { onActionClick: goBackToPaymentMethods, onSecondaryActionClick: closeWidget, diff --git a/packages/checkout/widgets-lib/src/widgets/sale/components/FundingRouteSelectDrawer/FundingRouteDrawer.tsx b/packages/checkout/widgets-lib/src/widgets/sale/components/FundingRouteSelectDrawer/FundingRouteDrawer.tsx index c685b79760..25ddca8091 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/components/FundingRouteSelectDrawer/FundingRouteDrawer.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/components/FundingRouteSelectDrawer/FundingRouteDrawer.tsx @@ -33,7 +33,7 @@ FundingRouteDrawerProps) { onClick={() => onClickMenuItem(i)} fundingRoute={fundingRoute} selected={activeFundingRouteIndex === i} - key={fundingRoute.priority} + key={fundingRoute.steps[0].fundingItem.type + fundingRoute.steps[0].fundingItem.token} /> ))} diff --git a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx index 46397d2429..0fa45a1ec2 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/context/SaleContextProvider.tsx @@ -1,31 +1,34 @@ +import { FundingRoute, RoutingOutcomeType, SmartCheckoutResult } from '@imtbl/checkout-sdk'; +import { Passport } from '@imtbl/passport'; import { - useContext, - createContext, - useMemo, ReactNode, - useEffect, - useState, + createContext, useCallback, + useContext, + useEffect, + useMemo, useRef, + useState, } from 'react'; -import { Passport } from '@imtbl/passport'; +import { ConnectLoaderState } from '../../../context/connect-loader-context/ConnectLoaderContext'; +import { FundWithSmartCheckoutSubViews, SaleWidgetViews } from '../../../context/view-context/SaleViewContextTypes'; import { + ViewActions, + ViewContext, +} from '../../../context/view-context/ViewContext'; +import { StrongCheckoutWidgetsConfig } from '../../../lib/withDefaultWidgetConfig'; +import { useSignOrder } from '../hooks/useSignOrder'; +import { + ExecuteOrderResponse, + ExecutedTransaction, Item, PaymentTypes, - SignResponse, SaleErrorTypes, SignOrderError, - ExecutedTransaction, - ExecuteOrderResponse, + SignResponse, } from '../types'; -import { useSignOrder } from '../hooks/useSignOrder'; -import { ConnectLoaderState } from '../../../context/connect-loader-context/ConnectLoaderContext'; -import { StrongCheckoutWidgetsConfig } from '../../../lib/withDefaultWidgetConfig'; -import { - ViewActions, - ViewContext, -} from '../../../context/view-context/ViewContext'; -import { SaleWidgetViews } from '../../../context/view-context/SaleViewContextTypes'; + +import { useSmartCheckout } from '../hooks/useSmartCheckout'; type SaleContextProps = { config: StrongCheckoutWidgetsConfig; @@ -58,6 +61,9 @@ type SaleContextValues = SaleContextProps & { goBackToPaymentMethods: (paymentMethod?: PaymentTypes | undefined) => void; goToErrorView: (type: SaleErrorTypes, data?: Record) => void; goToSuccessView: () => void; + querySmartCheckout: ((callback?: (r?: SmartCheckoutResult) => void) => Promise); + smartCheckoutResult: SmartCheckoutResult | undefined; + fundingRoutes: FundingRoute[]; }; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -84,6 +90,9 @@ const SaleContext = createContext({ goToErrorView: () => {}, goToSuccessView: () => {}, config: {} as StrongCheckoutWidgetsConfig, + querySmartCheckout: () => Promise.resolve(undefined), + smartCheckoutResult: undefined, + fundingRoutes: [], }); SaleContext.displayName = 'SaleSaleContext'; @@ -124,6 +133,8 @@ export function SaleContextProvider(props: { undefined, ); + const [fundingRoutes, setFundingRoutes] = useState([]); + const goBackToPaymentMethods = useCallback( (type?: PaymentTypes | undefined) => { setPaymentMethod(type); @@ -215,6 +226,65 @@ export function SaleContextProvider(props: { goToErrorView(signError.type, signError.data); }, [signError]); + const { smartCheckout, smartCheckoutResult, smartCheckoutError } = useSmartCheckout({ + provider, + checkout, + items, + amount, + contractAddress: fromContractAddress, + }); + + useEffect(() => { + if (!smartCheckoutError) return; + goToErrorView(smartCheckoutError.type, smartCheckoutError.data); + }, [smartCheckoutError]); + + const querySmartCheckout = useCallback(async (callback?: (r?: SmartCheckoutResult) => void) => { + const result = await smartCheckout(); + callback?.(result); + return result; + }, [smartCheckout]); + + useEffect(() => { + if (!smartCheckoutResult) { + return; + } + if (smartCheckoutResult.sufficient) { + sign(PaymentTypes.CRYPTO); + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SaleWidgetViews.PAY_WITH_COINS, + }, + }, + }); + } + if (!smartCheckoutResult.sufficient) { + switch (smartCheckoutResult.router.routingOutcome.type) { + case RoutingOutcomeType.ROUTES_FOUND: + setFundingRoutes(smartCheckoutResult.router.routingOutcome.fundingRoutes); + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SaleWidgetViews.FUND_WITH_SMART_CHECKOUT, + subView: FundWithSmartCheckoutSubViews.FUNDING_ROUTE_SELECT, + }, + }, + }); + + break; + case RoutingOutcomeType.NO_ROUTES_FOUND: + case RoutingOutcomeType.NO_ROUTE_OPTIONS: + default: + setFundingRoutes([]); + goToErrorView(SaleErrorTypes.SMART_CHECKOUT_NO_ROUTES_FOUND); + break; + } + } + }, [smartCheckoutResult]); + const values = useMemo( () => ({ config, @@ -238,6 +308,9 @@ export function SaleContextProvider(props: { goToErrorView, goToSuccessView, isPassportWallet: !!(provider?.provider as any)?.isPassport, + querySmartCheckout, + smartCheckoutResult, + fundingRoutes, }), [ config, @@ -257,6 +330,10 @@ export function SaleContextProvider(props: { goBackToPaymentMethods, goToErrorView, goToSuccessView, + sign, + querySmartCheckout, + smartCheckoutResult, + fundingRoutes, ], ); diff --git a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSmartCheckout.ts b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSmartCheckout.ts new file mode 100644 index 0000000000..b0d2bda00a --- /dev/null +++ b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useSmartCheckout.ts @@ -0,0 +1,87 @@ +import { Web3Provider } from '@ethersproject/providers'; +import { + Checkout, + ERC20ItemRequirement, + GasAmount, + GasTokenType, + ItemType, + SmartCheckoutResult, + TransactionOrGasType, +} from '@imtbl/checkout-sdk'; +import { BigNumber } from 'ethers'; +import { useCallback, useState } from 'react'; +import { Item, SaleErrorTypes, SmartCheckoutError } from '../types'; + +type UseSmartCheckoutInput = { + checkout: Checkout | undefined; + provider: Web3Provider | undefined; + items: Item[], + amount: string, + contractAddress: string, +}; + +const MAX_GAS_LIMIT = '30000000'; + +const getItemRequirements = (amount: string, spenderAddress: string, contractAddress: string) +: ERC20ItemRequirement[] => [ + { + type: ItemType.ERC20, + contractAddress, + spenderAddress, + amount, + }, +]; + +const getGasEstimate = (): GasAmount => ({ + type: TransactionOrGasType.GAS, + gasToken: { + type: GasTokenType.NATIVE, + limit: BigNumber.from(MAX_GAS_LIMIT), + }, +}); + +export const useSmartCheckout = ({ + checkout, provider, items, amount, contractAddress, +}: UseSmartCheckoutInput) => { + const [smartCheckoutResult, setSmartCheckoutResult] = useState( + undefined, + ); + const [smartCheckoutError, setSmartCheckoutError] = useState( + undefined, + ); + + const smartCheckout = useCallback(async () => { + if (!checkout || !provider) { + return undefined; + } + + const signer = provider.getSigner(); + const spenderAddress = await signer?.getAddress() || ''; + + const itemRequirements = getItemRequirements(amount, spenderAddress, contractAddress); + const gasEstimate = getGasEstimate(); + + try { + const res = await checkout.smartCheckout( + { + provider, + itemRequirements, + transactionOrGasAmount: gasEstimate, + }, + ); + + setSmartCheckoutResult(res); + return res; + } catch (err: any) { + setSmartCheckoutError({ + type: SaleErrorTypes.SMART_CHECKOUT_ERROR, + data: { error: err }, + }); + } + return undefined; + }, [checkout, provider, items, amount, contractAddress]); + + return { + smartCheckout, smartCheckoutResult, smartCheckoutError, + }; +}; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/types.ts b/packages/checkout/widgets-lib/src/widgets/sale/types.ts index 4290cfc69a..e317a97409 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/types.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/types.ts @@ -64,6 +64,11 @@ export type SignOrderError = { data?: Record; }; +export type SmartCheckoutError = { + type: SaleErrorTypes; + data?: Record; +}; + export type ExecutedTransaction = { method: string; hash: string | undefined; @@ -82,4 +87,6 @@ export enum SaleErrorTypes { WALLET_FAILED = 'WALLET_FAILED', WALLET_REJECTED = 'WALLET_REJECTED', WALLET_REJECTED_NO_FUNDS = 'WALLET_REJECTED_NO_FUNDS', + SMART_CHECKOUT_NO_ROUTES_FOUND = 'SMART_CHECKOUT_NO_ROUTES_FOUND', + SMART_CHECKOUT_ERROR = 'SMART_CHECKOUT_ERROR', } diff --git a/packages/checkout/widgets-lib/src/widgets/sale/views/FundWithSmartCheckout.tsx b/packages/checkout/widgets-lib/src/widgets/sale/views/FundWithSmartCheckout.tsx index b86cff90db..f76183bc17 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/views/FundWithSmartCheckout.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/views/FundWithSmartCheckout.tsx @@ -2,6 +2,7 @@ import { Box } from '@biom3/react'; import { FundingRoute } from '@imtbl/checkout-sdk'; import { useContext, + useEffect, useMemo, useState, } from 'react'; import { @@ -9,8 +10,10 @@ import { } from '../../../context/view-context/SaleViewContextTypes'; import { ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; import { LoadingView } from '../../../views/loading/LoadingView'; -import { FundingRouteSelect } from '../components/FundingRouteSelect/FundingRouteSelect'; import { FundingRouteExecute } from '../components/FundingRouteExecute/FundingRouteExecute'; +import { FundingRouteSelect } from '../components/FundingRouteSelect/FundingRouteSelect'; +import { useSaleContext } from '../context/SaleContextProvider'; +import { text as textConfig } from '../../../resources/text/textConfig'; type FundWithSmartCheckoutProps = { subView: FundWithSmartCheckoutSubViews; @@ -20,11 +23,25 @@ export function FundWithSmartCheckout({ subView }: FundWithSmartCheckoutProps) { const { viewDispatch } = useContext(ViewContext); const [selectedFundingRoute, setSelectedFundingRoute] = useState(undefined); const [fundingRouteStepIndex, setFundingRouteStepIndex] = useState(0); + const text = textConfig.views[SaleWidgetViews.FUND_WITH_SMART_CHECKOUT]; + + const { querySmartCheckout, fundingRoutes } = useSaleContext(); + + let smartCheckoutLoading = false; const onFundingRouteSelected = (fundingRoute: FundingRoute) => { setSelectedFundingRoute(fundingRoute); }; + useEffect(() => { + if (subView === FundWithSmartCheckoutSubViews.INIT && !smartCheckoutLoading) { + smartCheckoutLoading = true; + querySmartCheckout().finally(() => { + smartCheckoutLoading = false; + }); + } + }, []); + const fundingRouteStep = useMemo(() => { if (!selectedFundingRoute) { return undefined; @@ -54,12 +71,12 @@ export function FundWithSmartCheckout({ subView }: FundWithSmartCheckoutProps) { return ( { subView === FundWithSmartCheckoutSubViews.INIT && ( - + )} { subView === FundWithSmartCheckoutSubViews.FUNDING_ROUTE_SELECT && ( )} { subView === FundWithSmartCheckoutSubViews.FUNDING_ROUTE_EXECUTE && ( diff --git a/packages/checkout/widgets-lib/src/widgets/sale/views/PaymentMethods.tsx b/packages/checkout/widgets-lib/src/widgets/sale/views/PaymentMethods.tsx index 42a28c3ea6..58c0c0b077 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/views/PaymentMethods.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/views/PaymentMethods.tsx @@ -1,20 +1,20 @@ -import { useCallback, useContext, useEffect } from 'react'; import { Box, Heading } from '@biom3/react'; +import { useContext, useEffect } from 'react'; -import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { FooterLogo } from '../../../components/Footer/FooterLogo'; import { HeaderNavigation } from '../../../components/Header/HeaderNavigation'; +import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; +import { FundWithSmartCheckoutSubViews, SaleWidgetViews } from '../../../context/view-context/SaleViewContextTypes'; import { text as textConfig } from '../../../resources/text/textConfig'; -import { SaleWidgetViews } from '../../../context/view-context/SaleViewContextTypes'; import { - ViewContext, - ViewActions, SharedViews, + ViewActions, + ViewContext, } from '../../../context/view-context/ViewContext'; -import { sendSaleWidgetCloseEvent } from '../SaleWidgetEvents'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; +import { sendSaleWidgetCloseEvent } from '../SaleWidgetEvents'; import { PaymentOptions } from '../components/PaymentOptions'; import { useSaleContext } from '../context/SaleContextProvider'; @@ -28,19 +28,19 @@ export function PaymentMethods() { const handleOptionClick = (type: PaymentTypes) => setPaymentMethod(type); - const handleGoToPaymentView = useCallback((type: PaymentTypes, signed = false) => { - if (type === PaymentTypes.CRYPTO && !signed) { - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SaleWidgetViews.PAY_WITH_COINS, + useEffect(() => { + if (paymentMethod === PaymentTypes.FIAT) { + sign(paymentMethod, () => { + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SaleWidgetViews.PAY_WITH_CARD, + }, }, - }, + }); }); - } - if (type === PaymentTypes.FIAT && !signed) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, @@ -52,23 +52,17 @@ export function PaymentMethods() { }); } - if (type === PaymentTypes.FIAT && signed) { + if (paymentMethod === PaymentTypes.CRYPTO) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { - type: SaleWidgetViews.PAY_WITH_CARD, + type: SaleWidgetViews.FUND_WITH_SMART_CHECKOUT, + subView: FundWithSmartCheckoutSubViews.INIT, }, }, }); } - }, []); - - useEffect(() => { - if (paymentMethod) { - sign(paymentMethod, (response) => handleGoToPaymentView(paymentMethod, !!response)); - handleGoToPaymentView(paymentMethod); - } }, [paymentMethod]); return (