Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NO-CHANGELOG] Adds Smart Checkout to state and flow of Sale Widget #1005

Merged
merged 24 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bfcd00e
Adds useSmartCheckout hook for querying smart checkout in sales widget.
jwhardwick Oct 11, 2023
69ecde3
Adds useSmartCheckout to SaleContextProvider.
jwhardwick Oct 11, 2023
cc07fe4
Conditionally render pay with crypto based on smart checkout result.
jwhardwick Oct 11, 2023
1a2a3a9
FundWithSmartCheckout gets SmartCheckoutResult from SalesContextProvi…
jwhardwick Oct 17, 2023
206a952
Sets a unique key for funding route options.
jwhardwick Oct 17, 2023
0462145
Adds a screen to handle insufficient crypto balance after smart check…
jwhardwick Oct 17, 2023
c51cb2e
useSmartCheckout gets signer address directly from provider. SmartChe…
jwhardwick Oct 17, 2023
f344b98
Adds smart checkout query to PaymentMethods screen after user clicks …
jwhardwick Oct 17, 2023
dba1d58
Merge branch 'main' of github.com:immutable/ts-immutable-sdk into gpr…
jwhardwick Oct 17, 2023
ff42b1c
Removes some logs and unecessary code
jwhardwick Oct 17, 2023
2481aa6
FundWithSmartCheckout now responsible for calling smart checkout.
jwhardwick Oct 18, 2023
937d923
removes unused callback from inside useSmartCheckout.
jwhardwick Oct 18, 2023
64a703e
Adds error handling to useSmartCheckout.
jwhardwick Oct 18, 2023
aba609b
Changes querySmartCheckout from 'undefined' to a promise resolving un…
jwhardwick Oct 18, 2023
aa6a25a
Cleans up PaymentMethods and SaleContextProvider.
jwhardwick Oct 18, 2023
327007a
Removes unecessary change.
jwhardwick Oct 18, 2023
d06e18c
Merge branch 'main' into gpr-210/add-smart-checkout-shared-context-pr…
jwhardwick Oct 18, 2023
2bdc689
removes unecessary setFundingRoutes
jwhardwick Oct 18, 2023
9b4299d
Merge branch 'gpr-210/add-smart-checkout-shared-context-pr-branch' of…
jwhardwick Oct 18, 2023
5ad0404
Fixes typo in SaleContextProvider - duplicate signResponse in useMemo…
jwhardwick Oct 18, 2023
21de68c
Removes useCallback in PaymentMethods, unecessary and simpler just us…
jwhardwick Oct 18, 2023
00fac78
Adds FundWithSmartCheckout loading text to textConfig.
jwhardwick Oct 18, 2023
6bac19b
modify smart checkout loading text.
jwhardwick Oct 19, 2023
855e4a1
Merge branch 'main' into gpr-210/add-smart-checkout-shared-context-pr…
jwhardwick Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>);
jwhardwick marked this conversation as resolved.
Show resolved Hide resolved
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(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved all the SC logic stuff here, including calling sign before moving to PayWithCoins.

If there are funding routes, they get set here setFundingRoutes(). Then FundWithSmartCheckout only cares about two things globally:

  1. calling querySmartCheckout on INIT
  2. once FundingRoutes are set, showing these to user and letting them complete each step

if (!smartCheckoutResult) {
jwhardwick marked this conversation as resolved.
Show resolved Hide resolved
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() || '';
jwhardwick marked this conversation as resolved.
Show resolved Hide resolved

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',
}