diff --git a/.changeset/kind-countries-return.md b/.changeset/kind-countries-return.md new file mode 100644 index 0000000000..2e7dcea133 --- /dev/null +++ b/.changeset/kind-countries-return.md @@ -0,0 +1,7 @@ +--- +'@shopify/hydrogen': patch +--- + +Carts created in liquid will soon be compatible with the Storefront API and vice versa, making it possible to share carts between channels. + +This change updates the Demo Store to use Online Store's `cart` cookie (instead of sessions) which prevents customers from losing carts when merchants migrate to/from Hydrogen. diff --git a/templates/demo-store/app/lib/utils.ts b/templates/demo-store/app/lib/utils.ts index d7104cbdcd..8718d291a4 100644 --- a/templates/demo-store/app/lib/utils.ts +++ b/templates/demo-store/app/lib/utils.ts @@ -306,3 +306,22 @@ export function isLocalPath(url: string) { return false; } + +/** + * Shopify's 'Online Store' stores cart IDs in a 'cart' cookie. + * By doing the same, merchants can switch from the Online Store to Hydrogen + * without customers losing carts. + */ +export function getCartId(request: Request) { + const cookies = request.headers.get('Cookie'); + + let cart = cookies + ?.split(';') + .find((cookie) => cookie.trim().startsWith('cart=')) + ?.substring(6); + + if (cart) { + cart = `gid://shopify/Cart/${cart}`; + } + return cart; +} diff --git a/templates/demo-store/app/root.tsx b/templates/demo-store/app/root.tsx index fffe899a02..fb29448186 100644 --- a/templates/demo-store/app/root.tsx +++ b/templates/demo-store/app/root.tsx @@ -22,7 +22,12 @@ import {NotFound} from './components/NotFound'; import styles from './styles/app.css'; import favicon from '../public/favicon.svg'; import {seoPayload} from '~/lib/seo.server'; -import {DEFAULT_LOCALE, parseMenu, type EnhancedMenu} from './lib/utils'; +import { + DEFAULT_LOCALE, + parseMenu, + getCartId, + type EnhancedMenu, +} from './lib/utils'; import invariant from 'tiny-invariant'; import {Shop, Cart} from '@shopify/hydrogen/storefront-api-types'; import {useAnalytics} from './hooks/useAnalytics'; @@ -43,9 +48,9 @@ export const links: LinksFunction = () => { }; export async function loader({request, context}: LoaderArgs) { - const [customerAccessToken, cartId, layout] = await Promise.all([ + const cartId = getCartId(request); + const [customerAccessToken, layout] = await Promise.all([ context.session.get('customerAccessToken'), - context.session.get('cartId'), getLayoutData(context), ]); diff --git a/templates/demo-store/app/routes/($lang)/cart.tsx b/templates/demo-store/app/routes/($lang)/cart.tsx index 15f185ffb8..0fa7b74a8f 100644 --- a/templates/demo-store/app/routes/($lang)/cart.tsx +++ b/templates/demo-store/app/routes/($lang)/cart.tsx @@ -16,21 +16,19 @@ import type { UserError, CartBuyerIdentityInput, } from '@shopify/hydrogen/storefront-api-types'; -import {isLocalPath} from '~/lib/utils'; +import {isLocalPath, getCartId} from '~/lib/utils'; import {CartAction, type CartActions} from '~/lib/type'; export async function action({request, context}: ActionArgs) { const {session, storefront} = context; const headers = new Headers(); + let cartId = getCartId(request); - const [formData, storedCartId, customerAccessToken] = await Promise.all([ + const [formData, customerAccessToken] = await Promise.all([ request.formData(), - session.get('cartId'), session.get('customerAccessToken'), ]); - let cartId = storedCartId; - const cartAction = formData.get('cartAction') as CartActions; invariant(cartAction, 'No cartAction defined'); @@ -71,6 +69,7 @@ export async function action({request, context}: ActionArgs) { break; case CartAction.REMOVE_FROM_CART: + invariant(cartId, 'Missing cartId'); const lineIds = formData.get('linesIds') ? (JSON.parse(String(formData.get('linesIds'))) as CartType['id'][]) : ([] as CartType['id'][]); @@ -86,6 +85,7 @@ export async function action({request, context}: ActionArgs) { break; case CartAction.UPDATE_CART: + invariant(cartId, 'Missing cartId'); const updateLines = formData.get('lines') ? (JSON.parse(String(formData.get('lines'))) as CartLineUpdateInput[]) : ([] as CartLineUpdateInput[]); @@ -151,8 +151,7 @@ export async function action({request, context}: ActionArgs) { /** * The Cart ID may change after each mutation. We need to update it each time in the session. */ - session.set('cartId', cartId); - headers.set('Set-Cookie', await session.commit()); + headers.append('Set-Cookie', `cart=${cartId.split('/').pop()}`); const redirectTo = formData.get('redirectTo') ?? null; if (typeof redirectTo === 'string' && isLocalPath(redirectTo)) { diff --git a/templates/demo-store/app/routes/($lang)/discount.$code.tsx b/templates/demo-store/app/routes/($lang)/discount.$code.tsx index 52cb7b4851..04b8f6d170 100644 --- a/templates/demo-store/app/routes/($lang)/discount.$code.tsx +++ b/templates/demo-store/app/routes/($lang)/discount.$code.tsx @@ -1,4 +1,5 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen'; +import {getCartId} from '~/lib/utils'; import {cartCreate, cartDiscountCodesUpdate} from './cart'; /** @@ -30,7 +31,7 @@ export async function loader({request, context, params}: LoaderArgs) { return redirect(redirectUrl); } - let cartId = await session.get('cartId'); + let cartId = getCartId(request); //! if no existing cart, create one if (!cartId) { @@ -45,8 +46,7 @@ export async function loader({request, context, params}: LoaderArgs) { //! cart created - we only need a Set-Cookie header if we're creating cartId = cart.id; - session.set('cartId', cartId); - headers.set('Set-Cookie', await session.commit()); + headers.append('Set-Cookie', `cart=${cartId.split('/').pop()}`); } //! apply discount to the cart