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

Checkout: Remove CartStore from checkout #50681

Merged
merged 7 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions client/my-sites/checkout/checkout-system-decider.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import debugFactory from 'debug';
import { CheckoutErrorBoundary } from '@automattic/composite-checkout';
import { useTranslate } from 'i18n-calypso';
import { StripeHookProvider } from '@automattic/calypso-stripe';
import { getEmptyResponseCart } from '@automattic/shopping-cart';

/**
* Internal Dependencies
Expand All @@ -26,6 +27,8 @@ import CalypsoShoppingCartProvider from './calypso-shopping-cart-provider';
// otherwise we get `this is not defined` errors.
const wpcom = wp.undocumented();

const emptyCart = getEmptyResponseCart();

const debug = debugFactory( 'calypso:checkout-system-decider' );

export default function CheckoutSystemDecider( {
Expand All @@ -39,7 +42,6 @@ export default function CheckoutSystemDecider( {
redirectTo,
isLoggedOutCart,
isNoSiteCart,
cart: otherCart,
} ) {
const reduxDispatch = useDispatch();
const translate = useTranslate();
Expand Down Expand Up @@ -80,23 +82,14 @@ export default function CheckoutSystemDecider( {
[ reduxDispatch ]
);

// We have to monitor the old cart manager in case it's waiting on a
// requested change. To prevent race conditions, we will return undefined in
// that case, which will cause the ShoppingCartProvider to enter a loading
// state. We have to use null because CalypsoShoppingCartProvider assumes
// undefined means to try for its own cartKey.
const waitForOtherCartUpdates =
otherCart?.hasPendingServerUpdates || ! otherCart?.hasLoadedFromServer;
const cartKey = useMemo(
() =>
waitForOtherCartUpdates
? null
: getCartKey( {
selectedSite,
isLoggedOutCart,
isNoSiteCart,
} ),
[ waitForOtherCartUpdates, selectedSite, isLoggedOutCart, isNoSiteCart ]
getCartKey( {
selectedSite,
isLoggedOutCart,
isNoSiteCart,
} ),
[ selectedSite, isLoggedOutCart, isNoSiteCart ]
);
debug( 'cartKey is', cartKey );

Expand All @@ -110,8 +103,9 @@ export default function CheckoutSystemDecider( {
}
}

const getCart = isLoggedOutCart || isNoSiteCart ? () => Promise.resolve( otherCart ) : undefined;
debug( 'getCart being controlled by', { isLoggedOutCart, isNoSiteCart, otherCart } );
// If we do not have a site or user, we cannot fetch the initial cart from
// the server, so we'll just mock it as an empty cart here.
const getCart = isLoggedOutCart || isNoSiteCart ? () => Promise.resolve( emptyCart ) : undefined;

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export default function CompositeCheckout( {
isJetpackNotAtomic,
isPrivate,
siteSlug,
isLoggedOutCart,
isNoSiteCart,
} );

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { getProductsList, isProductsListFetching } from 'calypso/state/products-
import useFetchProductsIfNotLoaded from './use-fetch-products-if-not-loaded';
import doesValueExist from '../lib/does-value-exist';
import useStripProductsFromUrl from './use-strip-products-from-url';
import getCartFromLocalStorage from '../lib/get-cart-from-local-storage';
import { fillInSingleCartItemAttributes } from 'calypso/lib/cart-values';

const debug = debugFactory( 'calypso:composite-checkout:use-prepare-products-for-cart' );

Expand All @@ -46,30 +48,29 @@ export default function usePrepareProductsForCart( {
isJetpackNotAtomic,
isPrivate,
siteSlug,
isLoggedOutCart,
isNoSiteCart,
}: {
productAliasFromUrl: string | null | undefined;
purchaseId: string | number | null | undefined;
isInEditor?: boolean;
isJetpackNotAtomic: boolean;
isPrivate: boolean;
siteSlug: string | undefined;
isLoggedOutCart?: boolean;
isNoSiteCart?: boolean;
} ): PreparedProductsForCart {
const initializePreparedProductsState = (
initialState: PreparedProductsForCart
): PreparedProductsForCart => ( {
...initialState,
isLoading: !! productAliasFromUrl,
} );
const [ state, dispatch ] = useReducer(
preparedProductsReducer,
initialPreparedProductsState,
initializePreparedProductsState
);
const [ state, dispatch ] = useReducer( preparedProductsReducer, initialPreparedProductsState );

debug(
'preparing products for cart from url string',
productAliasFromUrl,
'and purchase id',
originalPurchaseId
originalPurchaseId,
'and isLoggedOutCart',
isLoggedOutCart,
'and isNoSiteCart',
isNoSiteCart
);

useFetchProductsIfNotLoaded();
Expand All @@ -78,10 +79,18 @@ export default function usePrepareProductsForCart( {
isLoading: state.isLoading,
originalPurchaseId,
productAliasFromUrl,
isLoggedOutCart,
isNoSiteCart,
} );
debug( 'isLoading', state.isLoading );
debug( 'handler is', addHandler );

// Only one of these should ever operate. The others should bail if they
// think another hook will handle the data.
useAddProductsFromLocalStorage( {
dispatch,
addHandler,
} );
useAddProductFromSlug( {
productAliasFromUrl,
dispatch,
Expand All @@ -95,6 +104,7 @@ export default function usePrepareProductsForCart( {
dispatch,
addHandler,
} );
useNothingToAdd( { addHandler, dispatch } );

// Do not strip products from url until the URL has been parsed
const areProductsRetrievedFromUrl = ! state.isLoading && ! isInEditor;
Expand Down Expand Up @@ -130,32 +140,100 @@ function preparedProductsReducer(
}
}

type AddHandler = 'addProductFromSlug' | 'addRenewalItems' | 'doNotAdd';
type AddHandler = 'addProductFromSlug' | 'addRenewalItems' | 'doNotAdd' | 'addFromLocalStorage';

function chooseAddHandler( {
isLoading,
originalPurchaseId,
productAliasFromUrl,
isLoggedOutCart,
isNoSiteCart,
}: {
isLoading: boolean;
originalPurchaseId: string | number | null | undefined;
productAliasFromUrl: string | null | undefined;
isLoggedOutCart?: boolean;
isNoSiteCart?: boolean;
} ): AddHandler {
if ( ! isLoading ) {
return 'doNotAdd';
}

if ( isLoading && originalPurchaseId ) {
if ( isLoggedOutCart || isNoSiteCart ) {
return 'addFromLocalStorage';
}

if ( originalPurchaseId ) {
return 'addRenewalItems';
}

if ( isLoading && ! originalPurchaseId && productAliasFromUrl ) {
if ( ! originalPurchaseId && productAliasFromUrl ) {
return 'addProductFromSlug';
}

return 'doNotAdd';
}

function useNothingToAdd( {
dispatch,
addHandler,
}: {
dispatch: ( action: PreparedProductsAction ) => void;
addHandler: AddHandler;
} ) {
useEffect( () => {
if ( addHandler !== 'doNotAdd' ) {
return;
}

debug( 'nothing to add' );
dispatch( { type: 'PRODUCTS_ADD', products: [] } );
}, [ addHandler, dispatch ] );
}

function useAddProductsFromLocalStorage( {
dispatch,
addHandler,
}: {
dispatch: ( action: PreparedProductsAction ) => void;
addHandler: AddHandler;
} ) {
const translate = useTranslate();
const products: Record<
string,
{
product_id: number;
product_slug: string;
}
> = useSelector( getProductsList );

useEffect( () => {
if ( addHandler !== 'addFromLocalStorage' ) {
return;
}
if ( Object.keys( products || {} ).length < 1 ) {
debug( 'waiting on products fetch' );
return;
}

const productsForCart: RequestCartProduct[] = getCartFromLocalStorage().map( ( product ) =>
fillInSingleCartItemAttributes( product, products )
);

if ( productsForCart.length < 1 ) {
debug( 'creating products from localStorage failed' );
dispatch( {
type: 'PRODUCTS_ADD_ERROR',
message: String( translate( 'I tried and failed to create products from signup' ) ),
} );
return;
}

debug( 'preparing products requested in localStorage', productsForCart );
dispatch( { type: 'PRODUCTS_ADD', products: productsForCart } );
}, [ addHandler, dispatch, translate, products ] );
}

function useAddRenewalItems( {
originalPurchaseId,
productAlias,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* External dependencies
*/
import type { RequestCartProduct } from '@automattic/shopping-cart';

// Used by signup; see https://github.com/Automattic/wp-calypso/pull/44206
// These products are likely missing product_id.
export default function getCartFromLocalStorage(): Partial< RequestCartProduct >[] {
try {
return JSON.parse( window.localStorage.getItem( 'shoppingCart' ) || '[]' );
} catch ( err ) {
return [];
}
}
27 changes: 12 additions & 15 deletions client/my-sites/checkout/controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { canUserPurchaseGSuite } from 'calypso/lib/gsuite';
import { getRememberedCoupon } from 'calypso/lib/cart/actions';
import { setSectionMiddleware } from 'calypso/controller';
import { sites } from 'calypso/my-sites/controller';
import CartData from 'calypso/components/data/cart';
import userFactory from 'calypso/lib/user';
import { getCurrentUser } from 'calypso/state/current-user/selectors';
import {
Expand Down Expand Up @@ -93,20 +92,18 @@ export function checkout( context, next ) {
}

context.primary = (
<CartData>
<CheckoutSystemDecider
productAliasFromUrl={ product }
purchaseId={ purchaseId }
selectedFeature={ feature }
couponCode={ couponCode }
isComingFromUpsell={ !! context.query.upgrade }
plan={ plan }
selectedSite={ selectedSite }
redirectTo={ context.query.redirect_to }
isLoggedOutCart={ isLoggedOutCart }
isNoSiteCart={ isNoSiteCart }
/>
</CartData>
<CheckoutSystemDecider
productAliasFromUrl={ product }
purchaseId={ purchaseId }
selectedFeature={ feature }
couponCode={ couponCode }
isComingFromUpsell={ !! context.query.upgrade }
plan={ plan }
selectedSite={ selectedSite }
redirectTo={ context.query.redirect_to }
isLoggedOutCart={ isLoggedOutCart }
isNoSiteCart={ isNoSiteCart }
/>
);

next();
Expand Down