diff --git a/client/my-sites/checkout/checkout-system-decider.js b/client/my-sites/checkout/checkout-system-decider.js index f71ca4ed401f4..cec762dcd20b7 100644 --- a/client/my-sites/checkout/checkout-system-decider.js +++ b/client/my-sites/checkout/checkout-system-decider.js @@ -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 @@ -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( { @@ -39,7 +42,6 @@ export default function CheckoutSystemDecider( { redirectTo, isLoggedOutCart, isNoSiteCart, - cart: otherCart, } ) { const reduxDispatch = useDispatch(); const translate = useTranslate(); @@ -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 ); @@ -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 ( <> diff --git a/client/my-sites/checkout/composite-checkout/composite-checkout.tsx b/client/my-sites/checkout/composite-checkout/composite-checkout.tsx index a1988f0e6a112..17b72bc5e4b44 100644 --- a/client/my-sites/checkout/composite-checkout/composite-checkout.tsx +++ b/client/my-sites/checkout/composite-checkout/composite-checkout.tsx @@ -232,6 +232,8 @@ export default function CompositeCheckout( { isJetpackNotAtomic, isPrivate, siteSlug, + isLoggedOutCart, + isNoSiteCart, } ); const { diff --git a/client/my-sites/checkout/composite-checkout/hooks/use-prepare-products-for-cart.ts b/client/my-sites/checkout/composite-checkout/hooks/use-prepare-products-for-cart.ts index 506c077bfbbce..557d92392984c 100644 --- a/client/my-sites/checkout/composite-checkout/hooks/use-prepare-products-for-cart.ts +++ b/client/my-sites/checkout/composite-checkout/hooks/use-prepare-products-for-cart.ts @@ -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' ); @@ -46,6 +48,8 @@ export default function usePrepareProductsForCart( { isJetpackNotAtomic, isPrivate, siteSlug, + isLoggedOutCart, + isNoSiteCart, }: { productAliasFromUrl: string | null | undefined; purchaseId: string | number | null | undefined; @@ -53,23 +57,20 @@ export default function usePrepareProductsForCart( { 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(); @@ -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, @@ -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; @@ -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, diff --git a/client/my-sites/checkout/composite-checkout/lib/get-cart-from-local-storage.ts b/client/my-sites/checkout/composite-checkout/lib/get-cart-from-local-storage.ts new file mode 100644 index 0000000000000..b5df51a84b0dc --- /dev/null +++ b/client/my-sites/checkout/composite-checkout/lib/get-cart-from-local-storage.ts @@ -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 []; + } +} diff --git a/client/my-sites/checkout/controller.jsx b/client/my-sites/checkout/controller.jsx index d73277e162640..a3028cff2a516 100644 --- a/client/my-sites/checkout/controller.jsx +++ b/client/my-sites/checkout/controller.jsx @@ -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 { @@ -93,20 +92,18 @@ export function checkout( context, next ) { } context.primary = ( - - - + ); next();