Skip to content

Commit

Permalink
[NO-CHANGELOG] Adds Smart Checkout to state and flow of Sale Widget (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jwhardwick committed Oct 19, 2023
1 parent 5f45c15 commit e4b8b11
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 47 deletions.
19 changes: 18 additions & 1 deletion packages/checkout/widgets-lib/src/resources/text/textConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?',
Expand Down Expand Up @@ -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',
},
Expand All @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/sale/SaleWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
))}
</BottomSheet.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -58,6 +61,9 @@ type SaleContextValues = SaleContextProps & {
goBackToPaymentMethods: (paymentMethod?: PaymentTypes | undefined) => void;
goToErrorView: (type: SaleErrorTypes, data?: Record<string, unknown>) => void;
goToSuccessView: () => void;
querySmartCheckout: ((callback?: (r?: SmartCheckoutResult) => void) => Promise<SmartCheckoutResult | undefined>);
smartCheckoutResult: SmartCheckoutResult | undefined;
fundingRoutes: FundingRoute[];
};

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -84,6 +90,9 @@ const SaleContext = createContext<SaleContextValues>({
goToErrorView: () => {},
goToSuccessView: () => {},
config: {} as StrongCheckoutWidgetsConfig,
querySmartCheckout: () => Promise.resolve(undefined),
smartCheckoutResult: undefined,
fundingRoutes: [],
});

SaleContext.displayName = 'SaleSaleContext';
Expand Down Expand Up @@ -124,6 +133,8 @@ export function SaleContextProvider(props: {
undefined,
);

const [fundingRoutes, setFundingRoutes] = useState<FundingRoute[]>([]);

const goBackToPaymentMethods = useCallback(
(type?: PaymentTypes | undefined) => {
setPaymentMethod(type);
Expand Down Expand Up @@ -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,
Expand All @@ -238,6 +308,9 @@ export function SaleContextProvider(props: {
goToErrorView,
goToSuccessView,
isPassportWallet: !!(provider?.provider as any)?.isPassport,
querySmartCheckout,
smartCheckoutResult,
fundingRoutes,
}),
[
config,
Expand All @@ -257,6 +330,10 @@ export function SaleContextProvider(props: {
goBackToPaymentMethods,
goToErrorView,
goToSuccessView,
sign,
querySmartCheckout,
smartCheckoutResult,
fundingRoutes,
],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SmartCheckoutResult | undefined>(
undefined,
);
const [smartCheckoutError, setSmartCheckoutError] = useState<SmartCheckoutError | undefined>(
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,
};
};
7 changes: 7 additions & 0 deletions packages/checkout/widgets-lib/src/widgets/sale/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export type SignOrderError = {
data?: Record<string, unknown>;
};

export type SmartCheckoutError = {
type: SaleErrorTypes;
data?: Record<string, unknown>;
};

export type ExecutedTransaction = {
method: string;
hash: string | undefined;
Expand All @@ -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',
}
Loading

0 comments on commit e4b8b11

Please sign in to comment.