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

Store cart ID in cookies #721

Merged
merged 3 commits into from
Apr 4, 2023
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
7 changes: 7 additions & 0 deletions .changeset/kind-countries-return.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions templates/demo-store/app/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
11 changes: 8 additions & 3 deletions templates/demo-store/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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),
]);

Expand Down
13 changes: 6 additions & 7 deletions templates/demo-store/app/routes/($lang)/cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -71,6 +69,7 @@ export async function action({request, context}: ActionArgs) {

break;
case CartAction.REMOVE_FROM_CART:
invariant(cartId, 'Missing cartId');
Copy link
Contributor

Choose a reason for hiding this comment

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

@scottdixon cart was complaining here because when you parsed the cookie with getCartId the function would have string | undefined union.

Then some of the actions below required to have a string. So instead I just did a check with invariant if the cartId actually exists.

const lineIds = formData.get('linesIds')
? (JSON.parse(String(formData.get('linesIds'))) as CartType['id'][])
: ([] as CartType['id'][]);
Expand All @@ -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[]);
Expand Down Expand Up @@ -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)) {
Expand Down
6 changes: 3 additions & 3 deletions templates/demo-store/app/routes/($lang)/discount.$code.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
import {getCartId} from '~/lib/utils';
import {cartCreate, cartDiscountCodesUpdate} from './cart';

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down