From 4337200c7908d56c039171c283a4d92c31a8b7b6 Mon Sep 17 00:00:00 2001 From: Michelle Chen Date: Thu, 6 Jun 2024 14:45:28 -0400 Subject: [PATCH] add session utility for dirty state. Ensure this dirty state is set to true whenever session is changed. And set to false whenever session is commited. Then in server code, check if session is dirty, if its dirty, set cookie with sessin.commit. (#2137) move commit logic to template instead update all the examples bring back location header add changeset --- .changeset/bright-wombats-count.md | 5 + .changeset/red-lamps-switch.md | 65 +++++++++++ .changeset/six-pumas-allow.md | 5 + .../analytics-setup/js/app/root.jsx | 7 +- .../analytics-setup/js/app/routes/cart.jsx | 7 +- .../analytics-setup/ts/app/root.tsx | 7 +- .../analytics-setup/ts/app/routes/cart.tsx | 7 +- examples/b2b/app/root.tsx | 35 +++--- examples/b2b/app/routes/b2blocations.tsx | 9 +- examples/b2b/server.ts | 4 + examples/classic-remix/app/root.tsx | 32 ++--- examples/classic-remix/server.ts | 4 + .../custom-cart-method/app/routes/cart.tsx | 2 - examples/custom-cart-method/server.ts | 4 + examples/express/app/root.tsx | 17 +-- examples/gtm/app/root.tsx | 31 ++--- .../legacy-customer-account-flow/app/root.tsx | 1 - .../app/routes/account.profile.tsx | 9 +- .../app/routes/account.tsx | 12 +- ...account_.activate.$id.$activationToken.tsx | 6 +- .../app/routes/account_.login.tsx | 6 +- .../app/routes/account_.logout.tsx | 6 +- .../app/routes/account_.register.tsx | 1 - .../routes/account_.reset.$id.$resetToken.tsx | 6 +- .../app/routes/cart.tsx | 2 - .../legacy-customer-account-flow/server.ts | 4 + examples/metaobjects/app/root.tsx | 45 +++----- examples/multipass/app/root.tsx | 1 - .../multipass/app/routes/account.profile.tsx | 9 +- examples/multipass/app/routes/account.tsx | 12 +- ...account_.activate.$id.$activationToken.tsx | 6 +- .../multipass/app/routes/account_.login.tsx | 6 +- .../multipass/app/routes/account_.logout.tsx | 6 +- .../app/routes/account_.register.tsx | 1 - .../routes/account_.reset.$id.$resetToken.tsx | 6 +- examples/multipass/server.ts | 9 ++ examples/partytown/app/root.tsx | 18 +-- examples/subscriptions/app/root.tsx | 30 ++--- .../third-party-queries-caching/server.ts | 4 + .../cli/src/lib/setups/i18n/replacers.test.ts | 4 + .../hydrogen/src/customer/auth.helpers.ts | 3 - .../customer.auth-handler.example.jsx | 22 ++-- .../customer.auth-handler.example.tsx | 41 +++++-- .../src/customer/customer.example.jsx | 13 ++- .../src/customer/customer.example.tsx | 13 ++- .../customer.opt-out-handler.example.jsx | 109 ++++++++++++++++-- .../customer.opt-out-handler.example.tsx | 103 ++++++++++++----- .../hydrogen/src/customer/customer.test.ts | 8 -- packages/hydrogen/src/customer/customer.ts | 27 +---- packages/hydrogen/src/hydrogen.d.ts | 1 + rfc/cart.md | 1 - templates/skeleton/app/lib/session.ts | 5 + templates/skeleton/app/root.tsx | 31 ++--- templates/skeleton/app/routes/account.$.tsx | 6 +- .../skeleton/app/routes/account.addresses.tsx | 82 ++----------- .../app/routes/account.orders.$id.tsx | 21 ++-- .../app/routes/account.orders._index.tsx | 9 +- .../skeleton/app/routes/account.profile.tsx | 27 +---- templates/skeleton/app/routes/account.tsx | 1 - templates/skeleton/app/routes/cart.tsx | 2 - templates/skeleton/server.ts | 4 + 61 files changed, 496 insertions(+), 484 deletions(-) create mode 100644 .changeset/bright-wombats-count.md create mode 100644 .changeset/red-lamps-switch.md create mode 100644 .changeset/six-pumas-allow.md diff --git a/.changeset/bright-wombats-count.md b/.changeset/bright-wombats-count.md new file mode 100644 index 0000000000..08b973928b --- /dev/null +++ b/.changeset/bright-wombats-count.md @@ -0,0 +1,5 @@ +--- +'@shopify/hydrogen': patch +--- + +`customerAccount` no longer commit session automatically in any situation. diff --git a/.changeset/red-lamps-switch.md b/.changeset/red-lamps-switch.md new file mode 100644 index 0000000000..a5c69e40a6 --- /dev/null +++ b/.changeset/red-lamps-switch.md @@ -0,0 +1,65 @@ +--- +'skeleton': patch +--- + +Remove manual setting of session in headers and recommend setting it in server after response is created. + +Step 1: Add `isPending` implementation in session + +```diff +// in app/lib/session.ts +export class AppSession implements HydrogenSession { ++ public isPending = false; + + get unset() { ++ this.isPending = true; + return this.#session.unset; + } + + get set() { ++ this.isPending = true; + return this.#session.set; + } + + commit() { ++ this.isPending = false; + return this.#sessionStorage.commitSession(this.#session); + } +} +``` + +Step 2: update response header if `session.isPending` is true + +```diff +// in server.ts +export default { + async fetch(request: Request): Promise { + try { + const response = await handleRequest(request); + ++ if (session.isPending) { ++ response.headers.set('Set-Cookie', await session.commit()); ++ } + + return response; + } catch (error) { + ... + } + }, +}; +``` + +Step 3: remove setting cookie with session.commit() in routes + +```diff +// in route files +export async function loader({context}: LoaderFunctionArgs) { + return json({}, +- { +- headers: { +- 'Set-Cookie': await context.session.commit(), +- }, + }, + ); +} +``` diff --git a/.changeset/six-pumas-allow.md b/.changeset/six-pumas-allow.md new file mode 100644 index 0000000000..f37fd2fe09 --- /dev/null +++ b/.changeset/six-pumas-allow.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-hydrogen': patch +--- + +skeleton template was updated to do session commit in server call instead of routes diff --git a/docs/shopify-dev/analytics-setup/js/app/root.jsx b/docs/shopify-dev/analytics-setup/js/app/root.jsx index 1c38106ff3..75c6b2b2fb 100644 --- a/docs/shopify-dev/analytics-setup/js/app/root.jsx +++ b/docs/shopify-dev/analytics-setup/js/app/root.jsx @@ -105,12 +105,7 @@ export async function loader({context}) { storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, // [END consent] - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, + } ); } diff --git a/docs/shopify-dev/analytics-setup/js/app/routes/cart.jsx b/docs/shopify-dev/analytics-setup/js/app/routes/cart.jsx index 04e4870424..aa0426edef 100644 --- a/docs/shopify-dev/analytics-setup/js/app/routes/cart.jsx +++ b/docs/shopify-dev/analytics-setup/js/app/routes/cart.jsx @@ -1,9 +1,6 @@ import {Await} from '@remix-run/react'; import {Suspense} from 'react'; -import { - CartForm, - Analytics, -} from '@shopify/hydrogen'; +import {CartForm, Analytics} from '@shopify/hydrogen'; import {json} from '@shopify/remix-oxygen'; import {CartMain} from '~/components/Cart'; import {useRootLoaderData} from '~/lib/root-data'; @@ -74,8 +71,6 @@ export async function action({request, context}) { headers.set('Location', redirectTo); } - headers.append('Set-Cookie', await context.session.commit()); - return json( { cart: cartResult, diff --git a/docs/shopify-dev/analytics-setup/ts/app/root.tsx b/docs/shopify-dev/analytics-setup/ts/app/root.tsx index 35610d3b56..fb01bea882 100644 --- a/docs/shopify-dev/analytics-setup/ts/app/root.tsx +++ b/docs/shopify-dev/analytics-setup/ts/app/root.tsx @@ -106,12 +106,7 @@ export async function loader({context}: LoaderFunctionArgs) { storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, // [END consent] - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, + } ); } diff --git a/docs/shopify-dev/analytics-setup/ts/app/routes/cart.tsx b/docs/shopify-dev/analytics-setup/ts/app/routes/cart.tsx index 093ca559cb..81e6f1e4c9 100644 --- a/docs/shopify-dev/analytics-setup/ts/app/routes/cart.tsx +++ b/docs/shopify-dev/analytics-setup/ts/app/routes/cart.tsx @@ -1,10 +1,7 @@ import {Await, type MetaFunction} from '@remix-run/react'; import {Suspense} from 'react'; import type {CartQueryDataReturn} from '@shopify/hydrogen'; -import { - CartForm, - Analytics, -} from '@shopify/hydrogen'; +import {CartForm, Analytics} from '@shopify/hydrogen'; import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen'; import {CartMain} from '~/components/Cart'; import {useRootLoaderData} from '~/lib/root-data'; @@ -71,8 +68,6 @@ export async function action({request, context}: ActionFunctionArgs) { headers.set('Location', redirectTo); } - headers.append('Set-Cookie', await context.session.commit()); - return json( { cart: cartResult, diff --git a/examples/b2b/app/root.tsx b/examples/b2b/app/root.tsx index ab750fc051..3e2f17352a 100644 --- a/examples/b2b/app/root.tsx +++ b/examples/b2b/app/root.tsx @@ -111,28 +111,21 @@ export async function loader({context}: LoaderFunctionArgs) { }, }); - return defer( - { - cart: cartPromise, - footer: footerPromise, - header: await headerPromise, - isLoggedIn: isLoggedInPromise, - publicStoreDomain, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, + return defer({ + cart: cartPromise, + footer: footerPromise, + header: await headerPromise, + isLoggedIn: isLoggedInPromise, + publicStoreDomain, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + }); } function Layout({children}: {children?: React.ReactNode}) { diff --git a/examples/b2b/app/routes/b2blocations.tsx b/examples/b2b/app/routes/b2blocations.tsx index 3189765692..6cda28b53a 100644 --- a/examples/b2b/app/routes/b2blocations.tsx +++ b/examples/b2b/app/routes/b2blocations.tsx @@ -30,14 +30,7 @@ export async function loader({context}: LoaderFunctionArgs) { const modalOpen = Boolean(company) && !companyLocationId; - return defer( - {company, companyLocationId, modalOpen}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return defer({company, companyLocationId, modalOpen}); } export default function CartRoute() { diff --git a/examples/b2b/server.ts b/examples/b2b/server.ts index 84e0c87f47..7279d99887 100644 --- a/examples/b2b/server.ts +++ b/examples/b2b/server.ts @@ -101,6 +101,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/examples/classic-remix/app/root.tsx b/examples/classic-remix/app/root.tsx index 7e3ba5b9af..b9f34303dd 100644 --- a/examples/classic-remix/app/root.tsx +++ b/examples/classic-remix/app/root.tsx @@ -74,27 +74,19 @@ export async function loader(args: LoaderFunctionArgs) { const {storefront, env} = args.context; - return defer( - { - ...criticalData, - ...deferredData, - publicStoreDomain: env.PUBLIC_STORE_DOMAIN, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - }, - - { - headers: { - 'Set-Cookie': await args.context.session.commit(), - }, + return defer({ + ...criticalData, + ...deferredData, + publicStoreDomain: env.PUBLIC_STORE_DOMAIN, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + }); } /** diff --git a/examples/classic-remix/server.ts b/examples/classic-remix/server.ts index 9b8416f71e..34e7e709d4 100644 --- a/examples/classic-remix/server.ts +++ b/examples/classic-remix/server.ts @@ -95,6 +95,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/examples/custom-cart-method/app/routes/cart.tsx b/examples/custom-cart-method/app/routes/cart.tsx index 0835fbb257..f2ab4eb335 100644 --- a/examples/custom-cart-method/app/routes/cart.tsx +++ b/examples/custom-cart-method/app/routes/cart.tsx @@ -90,8 +90,6 @@ export async function action({request, context}: ActionFunctionArgs) { headers.set('Location', redirectTo); } - headers.append('Set-Cookie', await context.session.commit()); - return json( { cart: cartResult, diff --git a/examples/custom-cart-method/server.ts b/examples/custom-cart-method/server.ts index f788bdf367..4eae76427a 100644 --- a/examples/custom-cart-method/server.ts +++ b/examples/custom-cart-method/server.ts @@ -132,6 +132,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/examples/express/app/root.tsx b/examples/express/app/root.tsx index ec1c52b79a..72c430ec78 100644 --- a/examples/express/app/root.tsx +++ b/examples/express/app/root.tsx @@ -58,18 +58,11 @@ export async function loader({context}: LoaderFunctionArgs) { await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY), ]); - return defer( - { - isLoggedIn: Boolean(customerAccessToken), - cart, - layout, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return defer({ + isLoggedIn: Boolean(customerAccessToken), + cart, + layout, + }); } function Layout({children}: {children?: React.ReactNode}) { diff --git a/examples/gtm/app/root.tsx b/examples/gtm/app/root.tsx index 7879bedd30..2adb386451 100644 --- a/examples/gtm/app/root.tsx +++ b/examples/gtm/app/root.tsx @@ -66,26 +66,19 @@ export async function loader(args: LoaderFunctionArgs) { const {storefront, env} = args.context; - return defer( - { - ...deferredData, - ...criticalData, - publicStoreDomain: env.PUBLIC_STORE_DOMAIN, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - }, - { - headers: { - 'Set-Cookie': await args.context.session.commit(), - }, + return defer({ + ...deferredData, + ...criticalData, + publicStoreDomain: env.PUBLIC_STORE_DOMAIN, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + }); } /** diff --git a/examples/legacy-customer-account-flow/app/root.tsx b/examples/legacy-customer-account-flow/app/root.tsx index 219fc27fb8..177b6e1fad 100644 --- a/examples/legacy-customer-account-flow/app/root.tsx +++ b/examples/legacy-customer-account-flow/app/root.tsx @@ -215,7 +215,6 @@ async function validateCustomerAccessToken( if (customerAccessTokenExpired) { session.unset('customerAccessToken'); - headers.append('Set-Cookie', await session.commit()); } else { isLoggedIn = true; } diff --git a/examples/legacy-customer-account-flow/app/routes/account.profile.tsx b/examples/legacy-customer-account-flow/app/routes/account.profile.tsx index 49b2020a24..3edf686d8a 100644 --- a/examples/legacy-customer-account-flow/app/routes/account.profile.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account.profile.tsx @@ -94,14 +94,7 @@ export async function action({request, context}: ActionFunctionArgs) { ); } - return json( - {error: null, customer: updated.customerUpdate?.customer}, - { - headers: { - 'Set-Cookie': await session.commit(), - }, - }, - ); + return json({error: null, customer: updated.customerUpdate?.customer}); } catch (error: any) { return json({error: error.message, customer: null}, {status: 400}); } diff --git a/examples/legacy-customer-account-flow/app/routes/account.tsx b/examples/legacy-customer-account-flow/app/routes/account.tsx index d52e884aae..913cd65a93 100644 --- a/examples/legacy-customer-account-flow/app/routes/account.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account.tsx @@ -20,11 +20,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { if (!isLoggedIn) { if (isPrivateRoute || isAccountHome) { session.unset('customerAccessToken'); - return redirect('/account/login', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account/login'); } else { // public subroute such as /account/login... return json({ @@ -67,11 +63,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { // eslint-disable-next-line no-console console.error('There was a problem loading account', error); session.unset('customerAccessToken'); - return redirect('/account/login', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account/login'); } } diff --git a/examples/legacy-customer-account-flow/app/routes/account_.activate.$id.$activationToken.tsx b/examples/legacy-customer-account-flow/app/routes/account_.activate.$id.$activationToken.tsx index 69724e9e87..49d1c4106e 100644 --- a/examples/legacy-customer-account-flow/app/routes/account_.activate.$id.$activationToken.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account_.activate.$id.$activationToken.tsx @@ -70,11 +70,7 @@ export async function action({request, context, params}: ActionFunctionArgs) { } session.set('customerAccessToken', customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/legacy-customer-account-flow/app/routes/account_.login.tsx b/examples/legacy-customer-account-flow/app/routes/account_.login.tsx index 0277df9336..f3537f8ae2 100644 --- a/examples/legacy-customer-account-flow/app/routes/account_.login.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account_.login.tsx @@ -54,11 +54,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {customerAccessToken} = customerAccessTokenCreate; session.set('customerAccessToken', customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/legacy-customer-account-flow/app/routes/account_.logout.tsx b/examples/legacy-customer-account-flow/app/routes/account_.logout.tsx index ef1ecb269e..8981fc3993 100644 --- a/examples/legacy-customer-account-flow/app/routes/account_.logout.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account_.logout.tsx @@ -17,11 +17,7 @@ export async function action({request, context}: ActionFunctionArgs) { return json({error: 'Method not allowed'}, {status: 405}); } - return redirect('/', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/'); } export default function Logout() { diff --git a/examples/legacy-customer-account-flow/app/routes/account_.register.tsx b/examples/legacy-customer-account-flow/app/routes/account_.register.tsx index 325300744f..2bfa62a39e 100644 --- a/examples/legacy-customer-account-flow/app/routes/account_.register.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account_.register.tsx @@ -90,7 +90,6 @@ export async function action({request, context}: ActionFunctionArgs) { { status: 302, headers: { - 'Set-Cookie': await session.commit(), Location: '/account', }, }, diff --git a/examples/legacy-customer-account-flow/app/routes/account_.reset.$id.$resetToken.tsx b/examples/legacy-customer-account-flow/app/routes/account_.reset.$id.$resetToken.tsx index a116c13797..9838590d1f 100644 --- a/examples/legacy-customer-account-flow/app/routes/account_.reset.$id.$resetToken.tsx +++ b/examples/legacy-customer-account-flow/app/routes/account_.reset.$id.$resetToken.tsx @@ -47,11 +47,7 @@ export async function action({request, context, params}: ActionFunctionArgs) { } session.set('customerAccessToken', customerReset.customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/legacy-customer-account-flow/app/routes/cart.tsx b/examples/legacy-customer-account-flow/app/routes/cart.tsx index 2476219509..831ef58eff 100644 --- a/examples/legacy-customer-account-flow/app/routes/cart.tsx +++ b/examples/legacy-customer-account-flow/app/routes/cart.tsx @@ -80,8 +80,6 @@ export async function action({request, context}: ActionFunctionArgs) { headers.set('Location', redirectTo); } - headers.append('Set-Cookie', await context.session.commit()); - return json( { cart: cartResult, diff --git a/examples/legacy-customer-account-flow/server.ts b/examples/legacy-customer-account-flow/server.ts index 37277075b1..c9116e05ed 100644 --- a/examples/legacy-customer-account-flow/server.ts +++ b/examples/legacy-customer-account-flow/server.ts @@ -82,6 +82,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/examples/metaobjects/app/root.tsx b/examples/metaobjects/app/root.tsx index 90452ba499..0fdd654673 100644 --- a/examples/metaobjects/app/root.tsx +++ b/examples/metaobjects/app/root.tsx @@ -79,33 +79,26 @@ export async function loader({context}: LoaderFunctionArgs) { }, }); - return defer( - { - cart: cartPromise, - footer: footerPromise, - header: await headerPromise, - isLoggedIn: isLoggedInPromise, - publicStoreDomain, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - /***********************************************/ - /********** EXAMPLE UPDATE STARTS ************/ - publictoreSubdomain: context.env.PUBLIC_SHOPIFY_STORE_DOMAIN, - /********** EXAMPLE UPDATE END ************/ - /***********************************************/ - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, + return defer({ + cart: cartPromise, + footer: footerPromise, + header: await headerPromise, + isLoggedIn: isLoggedInPromise, + publicStoreDomain, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + /***********************************************/ + /********** EXAMPLE UPDATE STARTS ************/ + publictoreSubdomain: context.env.PUBLIC_SHOPIFY_STORE_DOMAIN, + /********** EXAMPLE UPDATE END ************/ + /***********************************************/ + }); } function Layout({children}: {children?: React.ReactNode}) { diff --git a/examples/multipass/app/root.tsx b/examples/multipass/app/root.tsx index c472eecdee..998afd7591 100644 --- a/examples/multipass/app/root.tsx +++ b/examples/multipass/app/root.tsx @@ -250,7 +250,6 @@ async function validateCustomerAccessToken( if (customerAccessTokenExpired) { session.unset('customerAccessToken'); - headers.append('Set-Cookie', await session.commit()); } else { isLoggedIn = true; } diff --git a/examples/multipass/app/routes/account.profile.tsx b/examples/multipass/app/routes/account.profile.tsx index a546e6ff9e..c5c782b75c 100644 --- a/examples/multipass/app/routes/account.profile.tsx +++ b/examples/multipass/app/routes/account.profile.tsx @@ -94,14 +94,7 @@ export async function action({request, context}: ActionFunctionArgs) { ); } - return json( - {error: null, customer: updated.customerUpdate?.customer}, - { - headers: { - 'Set-Cookie': await session.commit(), - }, - }, - ); + return json({error: null, customer: updated.customerUpdate?.customer}); } catch (error: any) { return json({error: error.message, customer: null}, {status: 400}); } diff --git a/examples/multipass/app/routes/account.tsx b/examples/multipass/app/routes/account.tsx index d52e884aae..913cd65a93 100644 --- a/examples/multipass/app/routes/account.tsx +++ b/examples/multipass/app/routes/account.tsx @@ -20,11 +20,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { if (!isLoggedIn) { if (isPrivateRoute || isAccountHome) { session.unset('customerAccessToken'); - return redirect('/account/login', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account/login'); } else { // public subroute such as /account/login... return json({ @@ -67,11 +63,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { // eslint-disable-next-line no-console console.error('There was a problem loading account', error); session.unset('customerAccessToken'); - return redirect('/account/login', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account/login'); } } diff --git a/examples/multipass/app/routes/account_.activate.$id.$activationToken.tsx b/examples/multipass/app/routes/account_.activate.$id.$activationToken.tsx index 69724e9e87..49d1c4106e 100644 --- a/examples/multipass/app/routes/account_.activate.$id.$activationToken.tsx +++ b/examples/multipass/app/routes/account_.activate.$id.$activationToken.tsx @@ -70,11 +70,7 @@ export async function action({request, context, params}: ActionFunctionArgs) { } session.set('customerAccessToken', customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/multipass/app/routes/account_.login.tsx b/examples/multipass/app/routes/account_.login.tsx index 0277df9336..f3537f8ae2 100644 --- a/examples/multipass/app/routes/account_.login.tsx +++ b/examples/multipass/app/routes/account_.login.tsx @@ -54,11 +54,7 @@ export async function action({request, context}: ActionFunctionArgs) { const {customerAccessToken} = customerAccessTokenCreate; session.set('customerAccessToken', customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/multipass/app/routes/account_.logout.tsx b/examples/multipass/app/routes/account_.logout.tsx index ef1ecb269e..8981fc3993 100644 --- a/examples/multipass/app/routes/account_.logout.tsx +++ b/examples/multipass/app/routes/account_.logout.tsx @@ -17,11 +17,7 @@ export async function action({request, context}: ActionFunctionArgs) { return json({error: 'Method not allowed'}, {status: 405}); } - return redirect('/', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/'); } export default function Logout() { diff --git a/examples/multipass/app/routes/account_.register.tsx b/examples/multipass/app/routes/account_.register.tsx index 325300744f..2bfa62a39e 100644 --- a/examples/multipass/app/routes/account_.register.tsx +++ b/examples/multipass/app/routes/account_.register.tsx @@ -90,7 +90,6 @@ export async function action({request, context}: ActionFunctionArgs) { { status: 302, headers: { - 'Set-Cookie': await session.commit(), Location: '/account', }, }, diff --git a/examples/multipass/app/routes/account_.reset.$id.$resetToken.tsx b/examples/multipass/app/routes/account_.reset.$id.$resetToken.tsx index a116c13797..9838590d1f 100644 --- a/examples/multipass/app/routes/account_.reset.$id.$resetToken.tsx +++ b/examples/multipass/app/routes/account_.reset.$id.$resetToken.tsx @@ -47,11 +47,7 @@ export async function action({request, context, params}: ActionFunctionArgs) { } session.set('customerAccessToken', customerReset.customerAccessToken); - return redirect('/account', { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect('/account'); } catch (error: unknown) { if (error instanceof Error) { return json({error: error.message}, {status: 400}); diff --git a/examples/multipass/server.ts b/examples/multipass/server.ts index a0549d6530..9e5308d278 100644 --- a/examples/multipass/server.ts +++ b/examples/multipass/server.ts @@ -76,6 +76,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. @@ -100,6 +104,8 @@ export default { * swap out the cookie-based implementation with something else! */ export class AppSession implements HydrogenSession { + public isPending = false; + #sessionStorage; #session; @@ -139,10 +145,12 @@ export class AppSession implements HydrogenSession { } get unset() { + this.isPending = true; return this.#session.unset; } get set() { + this.isPending = true; return this.#session.set; } @@ -151,6 +159,7 @@ export class AppSession implements HydrogenSession { } commit() { + this.isPending = false; return this.#sessionStorage.commitSession(this.#session); } } diff --git a/examples/partytown/app/root.tsx b/examples/partytown/app/root.tsx index 08a1ea5d5b..e1f15eee76 100644 --- a/examples/partytown/app/root.tsx +++ b/examples/partytown/app/root.tsx @@ -45,19 +45,11 @@ export async function loader(args: LoaderFunctionArgs) { const {env} = args.context; - return defer( - { - ...deferredData, - ...criticalData, - gtmContainerId: env.GTM_CONTAINER_ID, - }, - { - headers: { - ...partytownAtomicHeaders(), - 'Set-Cookie': await args.context.session.commit(), - }, - }, - ); + return defer({ + ...deferredData, + ...criticalData, + gtmContainerId: env.GTM_CONTAINER_ID, + }); } /** diff --git a/examples/subscriptions/app/root.tsx b/examples/subscriptions/app/root.tsx index 02b72d62f7..df9886015a 100644 --- a/examples/subscriptions/app/root.tsx +++ b/examples/subscriptions/app/root.tsx @@ -75,26 +75,18 @@ export async function loader(args: LoaderFunctionArgs) { const {storefront, env} = args.context; - return defer( - { - ...deferredData, - ...criticalData, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - }, - - { - headers: { - 'Set-Cookie': await args.context.session.commit(), - }, + return defer({ + ...deferredData, + ...criticalData, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + }); } /** diff --git a/examples/third-party-queries-caching/server.ts b/examples/third-party-queries-caching/server.ts index 899922ccdc..8b321527d4 100644 --- a/examples/third-party-queries-caching/server.ts +++ b/examples/third-party-queries-caching/server.ts @@ -109,6 +109,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/packages/cli/src/lib/setups/i18n/replacers.test.ts b/packages/cli/src/lib/setups/i18n/replacers.test.ts index 94c3ff94ac..f9eae280da 100644 --- a/packages/cli/src/lib/setups/i18n/replacers.test.ts +++ b/packages/cli/src/lib/setups/i18n/replacers.test.ts @@ -236,6 +236,10 @@ describe('i18n replacers', () => { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set("Set-Cookie", await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app. diff --git a/packages/hydrogen/src/customer/auth.helpers.ts b/packages/hydrogen/src/customer/auth.helpers.ts index 996f799e0f..5cf01365cb 100644 --- a/packages/hydrogen/src/customer/auth.helpers.ts +++ b/packages/hydrogen/src/customer/auth.helpers.ts @@ -202,9 +202,6 @@ export async function checkExpires({ throw new BadRequest( 'Unauthorized', 'Login before querying the Customer Account API.', - { - 'Set-Cookie': await session.commit(), - }, ); } } diff --git a/packages/hydrogen/src/customer/customer.auth-handler.example.jsx b/packages/hydrogen/src/customer/customer.auth-handler.example.jsx index 538b17d475..4dbd7464ea 100644 --- a/packages/hydrogen/src/customer/customer.auth-handler.example.jsx +++ b/packages/hydrogen/src/customer/customer.auth-handler.example.jsx @@ -36,11 +36,19 @@ export default { getLoadContext: () => ({customerAccount}), }); - return handleRequest(request); + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; }, }; class AppSession { + isPending = false; + static async init(request, secrets) { const storage = createCookieSessionStorage({ cookie: { @@ -70,14 +78,17 @@ class AppSession { } unset(key) { + this.isPending = true; this.session.unset(key); } set(key, value) { + this.isPending = true; this.session.set(key, value); } commit() { + this.isPending = false; return this.sessionStorage.commitSession(this.session); } } @@ -102,14 +113,7 @@ export async function loader({context}) { } `); - return json( - {customer: data.customer}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({customer: data.customer}); } export function ErrorBoundary() { diff --git a/packages/hydrogen/src/customer/customer.auth-handler.example.tsx b/packages/hydrogen/src/customer/customer.auth-handler.example.tsx index 48c070fa4a..a7e01db439 100644 --- a/packages/hydrogen/src/customer/customer.auth-handler.example.tsx +++ b/packages/hydrogen/src/customer/customer.auth-handler.example.tsx @@ -42,14 +42,22 @@ export default { build: remixBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ - getLoadContext: () => ({customerAccount}), + getLoadContext: () => ({session, customerAccount}), }); - return handleRequest(request); + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; }, }; class AppSession implements HydrogenSession { + public isPending = false; + constructor( private sessionStorage: SessionStorage, private session: Session, @@ -84,18 +92,38 @@ class AppSession implements HydrogenSession { } unset(key: string) { + this.isPending = true; this.session.unset(key); } set(key: string, value: any) { + this.isPending = true; this.session.set(key, value); } commit() { + this.isPending = false; return this.sessionStorage.commitSession(this.session); } } +// In env.d.ts +import type {CustomerAccount, HydrogenSessionData} from '@shopify/hydrogen'; +declare module '@shopify/remix-oxygen' { + /** + * Declare local additions to the Remix loader context. + */ + interface AppLoadContext { + customerAccount: CustomerAccount; + session: AppSession; + } + + /** + * Declare local additions to the Remix session data. + */ + interface SessionData extends HydrogenSessionData {} +} + ///////////////////////////////// // In a route import { @@ -118,14 +146,7 @@ export async function loader({context}: LoaderFunctionArgs) { } `); - return json( - {customer: data.customer}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({customer: data.customer}); } export function ErrorBoundary() { diff --git a/packages/hydrogen/src/customer/customer.example.jsx b/packages/hydrogen/src/customer/customer.example.jsx index e9836441cf..55085ee4be 100644 --- a/packages/hydrogen/src/customer/customer.example.jsx +++ b/packages/hydrogen/src/customer/customer.example.jsx @@ -28,11 +28,19 @@ export default { getLoadContext: () => ({customerAccount}), }); - return handleRequest(request); + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; }, }; class AppSession { + isPending = false; + static async init(request, secrets) { const storage = createCookieSessionStorage({ cookie: { @@ -62,14 +70,17 @@ class AppSession { } unset(key) { + this.isPending = true; this.session.unset(key); } set(key, value) { + this.isPending = true; this.session.set(key, value); } commit() { + this.isPending = false; return this.sessionStorage.commitSession(this.session); } } diff --git a/packages/hydrogen/src/customer/customer.example.tsx b/packages/hydrogen/src/customer/customer.example.tsx index 1f5b4420cc..9a06f43989 100644 --- a/packages/hydrogen/src/customer/customer.example.tsx +++ b/packages/hydrogen/src/customer/customer.example.tsx @@ -37,11 +37,19 @@ export default { getLoadContext: () => ({customerAccount}), }); - return handleRequest(request); + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; }, }; class AppSession implements HydrogenSession { + public isPending = false; + constructor( private sessionStorage: SessionStorage, private session: Session, @@ -76,14 +84,17 @@ class AppSession implements HydrogenSession { } unset(key: string) { + this.isPending = true; this.session.unset(key); } set(key: string, value: any) { + this.isPending = true; this.session.set(key, value); } commit() { + this.isPending = false; return this.sessionStorage.commitSession(this.session); } } diff --git a/packages/hydrogen/src/customer/customer.opt-out-handler.example.jsx b/packages/hydrogen/src/customer/customer.opt-out-handler.example.jsx index 12245478bf..e9e4d29ba7 100644 --- a/packages/hydrogen/src/customer/customer.opt-out-handler.example.jsx +++ b/packages/hydrogen/src/customer/customer.opt-out-handler.example.jsx @@ -1,3 +1,100 @@ +import {createCustomerAccountClient} from '@shopify/hydrogen'; +import * as remixBuild from '@remix-run/dev/server-build'; +import { + createRequestHandler, + createCookieSessionStorage, +} from '@shopify/remix-oxygen'; + +// In server.ts +export default { + async fetch(request, env, executionContext) { + const session = await AppSession.init(request, [env.SESSION_SECRET]); + + function customAuthStatusHandler() { + return new Response('Customer is not login', { + status: 401, + }); + } + + /* Create a Customer API client with your credentials and options */ + const customerAccount = createCustomerAccountClient({ + /* Runtime utility in serverless environments */ + waitUntil: (p) => executionContext.waitUntil(p), + /* Public Customer Account API client ID for your store */ + customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, + /* Public account URL for your store */ + customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_URL, + request, + session, + customAuthStatusHandler, + }); + + const handleRequest = createRequestHandler({ + build: remixBuild, + mode: process.env.NODE_ENV, + /* Inject the customer account client in the Remix context */ + getLoadContext: () => ({customerAccount}), + }); + + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; + }, +}; + +class AppSession { + isPending = false; + + static async init(request, secrets) { + const storage = createCookieSessionStorage({ + cookie: { + name: 'session', + httpOnly: true, + path: '/', + sameSite: 'lax', + secrets, + }, + }); + + const session = await storage.getSession(request.headers.get('Cookie')); + + return new this(storage, session); + } + + get(key) { + return this.session.get(key); + } + + destroy() { + return this.sessionStorage.destroySession(this.session); + } + + flash(key, value) { + this.session.flash(key, value); + } + + unset(key) { + this.isPending = true; + this.session.unset(key); + } + + set(key, value) { + this.isPending = true; + this.session.set(key, value); + } + + commit() { + this.isPending = false; + return this.sessionStorage.commitSession(this.session); + } +} + +///////////////////////////////// +// In a route import { useLoaderData, useRouteError, @@ -24,14 +121,7 @@ export async function loader({context}) { `, ); - return json( - {customer: data.customer}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({customer: data.customer}); } export function ErrorBoundary() { @@ -53,7 +143,8 @@ export function ErrorBoundary() { } } -export default function () { +// this should be an default export +export function Route() { const {customer} = useLoaderData(); return ( diff --git a/packages/hydrogen/src/customer/customer.opt-out-handler.example.tsx b/packages/hydrogen/src/customer/customer.opt-out-handler.example.tsx index 9cb22b2486..2960c3ff61 100644 --- a/packages/hydrogen/src/customer/customer.opt-out-handler.example.tsx +++ b/packages/hydrogen/src/customer/customer.opt-out-handler.example.tsx @@ -1,29 +1,63 @@ -import type {CustomerAccount} from '@shopify/hydrogen'; -import {type HydrogenSession} from '@shopify/hydrogen'; import { + createCustomerAccountClient, + type HydrogenSession, +} from '@shopify/hydrogen'; +import * as remixBuild from '@remix-run/dev/server-build'; +import { + createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; -import { - useLoaderData, - useRouteError, - isRouteErrorResponse, - useLocation, -} from '@remix-run/react'; -import {type LoaderFunctionArgs, json} from '@shopify/remix-oxygen'; -declare module '@shopify/remix-oxygen' { - /** - * Declare local additions to the Remix loader context. - */ - export interface AppLoadContext { - customerAccount: CustomerAccount; - session: AppSession; - } -} +// In server.ts +export default { + async fetch( + request: Request, + env: Record, + executionContext: ExecutionContext, + ) { + const session = await AppSession.init(request, [env.SESSION_SECRET]); + + function customAuthStatusHandler() { + return new Response('Customer is not login', { + status: 401, + }); + } + + /* Create a Customer API client with your credentials and options */ + const customerAccount = createCustomerAccountClient({ + /* Runtime utility in serverless environments */ + waitUntil: (p) => executionContext.waitUntil(p), + /* Public Customer Account API client ID for your store */ + customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, + /* Public account URL for your store */ + customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_URL, + request, + session, + customAuthStatusHandler, + }); + + const handleRequest = createRequestHandler({ + build: remixBuild, + mode: process.env.NODE_ENV, + /* Inject the customer account client in the Remix context */ + getLoadContext: () => ({customerAccount}), + }); + + const response = await handleRequest(request); + + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + + return response; + }, +}; class AppSession implements HydrogenSession { + public isPending = false; + constructor( private sessionStorage: SessionStorage, private session: Session, @@ -58,18 +92,31 @@ class AppSession implements HydrogenSession { } unset(key: string) { + this.isPending = true; this.session.unset(key); } set(key: string, value: any) { + this.isPending = true; this.session.set(key, value); } commit() { + this.isPending = false; return this.sessionStorage.commitSession(this.session); } } +///////////////////////////////// +// In a route +import { + useLoaderData, + useRouteError, + isRouteErrorResponse, + useLocation, +} from '@remix-run/react'; +import {type LoaderFunctionArgs, json} from '@shopify/remix-oxygen'; + export async function loader({context}: LoaderFunctionArgs) { if (!(await context.customerAccount.isLoggedIn())) { throw new Response('Customer is not login', { @@ -77,25 +124,18 @@ export async function loader({context}: LoaderFunctionArgs) { }); } - const {data} = await context.customerAccount.query<{ - customer: {firstName: string; lastName: string}; - }>(`#graphql + const {data} = await context.customerAccount.query( + `#graphql query getCustomer { customer { firstName lastName } } - `); - - return json( - {customer: data.customer}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, + `, ); + + return json({customer: data.customer}); } export function ErrorBoundary() { @@ -117,7 +157,8 @@ export function ErrorBoundary() { } } -export default function () { +// this should be an default export +export function Route() { const {customer} = useLoaderData(); return ( diff --git a/packages/hydrogen/src/customer/customer.test.ts b/packages/hydrogen/src/customer/customer.test.ts index 2894391f78..20f889d02e 100644 --- a/packages/hydrogen/src/customer/customer.test.ts +++ b/packages/hydrogen/src/customer/customer.test.ts @@ -98,7 +98,6 @@ describe('customer', () => { ); expect(response.status).toBe(302); - expect(response.headers.get('Set-Cookie')).toBe('cookie'); const url = new URL(response.headers.get('location')!); expect(url.origin).toBe('https://customer-api'); @@ -185,7 +184,6 @@ describe('customer', () => { const response = await customer.logout(); expect(response.status).toBe(302); - expect(response.headers.get('Set-Cookie')).toBe('cookie'); const url = new URL(response.headers.get('location')!); expect(url.origin).toBe('https://customer-api'); @@ -515,9 +513,6 @@ describe('customer', () => { expect(response.status).toBe(302); expect(response.headers.get('location')).toBe('/account'); - expect(response.headers.get('Set-Cookie')).toStrictEqual( - expect.any(String), - ); expect(session.set).toHaveBeenCalledWith( CUSTOMER_ACCOUNT_SESSION_KEY, @@ -565,9 +560,6 @@ describe('customer', () => { expect(response.status).toBe(302); expect(response.headers.get('location')).toBe(redirectPath); - expect(response.headers.get('Set-Cookie')).toStrictEqual( - expect.any(String), - ); expect(session.set).toHaveBeenCalledWith( CUSTOMER_ACCOUNT_SESSION_KEY, diff --git a/packages/hydrogen/src/customer/customer.ts b/packages/hydrogen/src/customer/customer.ts index af210ea547..25a6e0971f 100644 --- a/packages/hydrogen/src/customer/customer.ts +++ b/packages/hydrogen/src/customer/customer.ts @@ -166,9 +166,6 @@ export function createCustomerAccountClient({ clearSession(session); const authFailResponse = authStatusHandler(); - if (authFailResponse instanceof Response) { - authFailResponse.headers.set('Set-Cookie', await session.commit()); - } throw authFailResponse; } @@ -378,11 +375,7 @@ export function createCustomerAccountClient({ loginUrl.searchParams.append('code_challenge', challenge); loginUrl.searchParams.append('code_challenge_method', 'S256'); - return redirect(loginUrl.toString(), { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect(loginUrl.toString()); }, logout: async (options?: LogoutOptions) => { @@ -406,11 +399,7 @@ export function createCustomerAccountClient({ clearSession(session); - return redirect(logoutUrl, { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect(logoutUrl); }, isLoggedIn, handleAuthStatus, @@ -430,9 +419,6 @@ export function createCustomerAccountClient({ throw new BadRequest( 'Unauthorized', 'No code or state parameter found in the redirect URL.', - { - 'Set-Cookie': await session.commit(), - }, ); } @@ -442,9 +428,6 @@ export function createCustomerAccountClient({ throw new BadRequest( 'Unauthorized', 'The session state does not match the state parameter. Make sure that the session is configured correctly and passed to `createCustomerAccountClient`.', - { - 'Set-Cookie': await session.commit(), - }, ); } @@ -543,11 +526,7 @@ export function createCustomerAccountClient({ await exchangeForStorefrontCustomerAccessToken(); - return redirect(redirectPath || DEFAULT_REDIRECT_PATH, { - headers: { - 'Set-Cookie': await session.commit(), - }, - }); + return redirect(redirectPath || DEFAULT_REDIRECT_PATH); }, UNSTABLE_setBuyer: setBuyer, UNSTABLE_getBuyer: getBuyer, diff --git a/packages/hydrogen/src/hydrogen.d.ts b/packages/hydrogen/src/hydrogen.d.ts index 4ed414475f..de87038069 100644 --- a/packages/hydrogen/src/hydrogen.d.ts +++ b/packages/hydrogen/src/hydrogen.d.ts @@ -33,6 +33,7 @@ export interface HydrogenSession< commit: () => ReturnType< SessionStorage['commitSession'] >; + isPending?: boolean; } declare global { diff --git a/rfc/cart.md b/rfc/cart.md index 705b7bd0bf..7b6f719827 100644 --- a/rfc/cart.md +++ b/rfc/cart.md @@ -68,7 +68,6 @@ export async function action({request, context}) { // 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()); const {cart, errors} = result; return json({cart, errors}, {status, headers}); diff --git a/templates/skeleton/app/lib/session.ts b/templates/skeleton/app/lib/session.ts index dec9ebfd74..de71699ead 100644 --- a/templates/skeleton/app/lib/session.ts +++ b/templates/skeleton/app/lib/session.ts @@ -11,6 +11,8 @@ import { * swap out the cookie-based implementation with something else! */ export class AppSession implements HydrogenSession { + public isPending = false; + #sessionStorage; #session; @@ -50,10 +52,12 @@ export class AppSession implements HydrogenSession { } get unset() { + this.isPending = true; return this.#session.unset; } get set() { + this.isPending = true; return this.#session.set; } @@ -62,6 +66,7 @@ export class AppSession implements HydrogenSession { } commit() { + this.isPending = false; return this.#sessionStorage.commitSession(this.#session); } } diff --git a/templates/skeleton/app/root.tsx b/templates/skeleton/app/root.tsx index 5da14e0016..126e7d0e28 100644 --- a/templates/skeleton/app/root.tsx +++ b/templates/skeleton/app/root.tsx @@ -65,26 +65,19 @@ export async function loader(args: LoaderFunctionArgs) { const {storefront, env} = args.context; - return defer( - { - ...deferredData, - ...criticalData, - publicStoreDomain: env.PUBLIC_STORE_DOMAIN, - shop: getShopAnalytics({ - storefront, - publicStorefrontId: env.PUBLIC_STOREFRONT_ID, - }), - consent: { - checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, - storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, - }, - }, - { - headers: { - 'Set-Cookie': await args.context.session.commit(), - }, + return defer({ + ...deferredData, + ...criticalData, + publicStoreDomain: env.PUBLIC_STORE_DOMAIN, + shop: getShopAnalytics({ + storefront, + publicStorefrontId: env.PUBLIC_STOREFRONT_ID, + }), + consent: { + checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN, + storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN, }, - ); + }); } /** diff --git a/templates/skeleton/app/routes/account.$.tsx b/templates/skeleton/app/routes/account.$.tsx index ca52f2b68e..53543f62be 100644 --- a/templates/skeleton/app/routes/account.$.tsx +++ b/templates/skeleton/app/routes/account.$.tsx @@ -4,9 +4,5 @@ import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen'; export async function loader({context}: LoaderFunctionArgs) { await context.customerAccount.handleAuthStatus(); - return redirect('/account', { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }); + return redirect('/account'); } diff --git a/templates/skeleton/app/routes/account.addresses.tsx b/templates/skeleton/app/routes/account.addresses.tsx index 2075c50cf4..b45bced01f 100644 --- a/templates/skeleton/app/routes/account.addresses.tsx +++ b/templates/skeleton/app/routes/account.addresses.tsx @@ -37,14 +37,7 @@ export const meta: MetaFunction = () => { export async function loader({context}: LoaderFunctionArgs) { await context.customerAccount.handleAuthStatus(); - return json( - {}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({}); } export async function action({request, context}: ActionFunctionArgs) { @@ -67,9 +60,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: {[addressId]: 'Unauthorized'}}, { status: 401, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -121,27 +111,17 @@ export async function action({request, context}: ActionFunctionArgs) { throw new Error('Customer address create failed.'); } - return json( - { - error: null, - createdAddress: data?.customerAddressCreate?.customerAddress, - defaultAddress, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({ + error: null, + createdAddress: data?.customerAddressCreate?.customerAddress, + defaultAddress, + }); } catch (error: unknown) { if (error instanceof Error) { return json( {error: {[addressId]: error.message}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -149,9 +129,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: {[addressId]: error}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -183,27 +160,17 @@ export async function action({request, context}: ActionFunctionArgs) { throw new Error('Customer address update failed.'); } - return json( - { - error: null, - updatedAddress: address, - defaultAddress, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({ + error: null, + updatedAddress: address, + defaultAddress, + }); } catch (error: unknown) { if (error instanceof Error) { return json( {error: {[addressId]: error.message}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -211,9 +178,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: {[addressId]: error}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -241,23 +205,13 @@ export async function action({request, context}: ActionFunctionArgs) { throw new Error('Customer address delete failed.'); } - return json( - {error: null, deletedAddress: addressId}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({error: null, deletedAddress: addressId}); } catch (error: unknown) { if (error instanceof Error) { return json( {error: {[addressId]: error.message}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -265,9 +219,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: {[addressId]: error}}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -278,9 +229,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: {[addressId]: 'Method not allowed'}}, { status: 405, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -291,9 +239,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error: error.message}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } @@ -301,9 +246,6 @@ export async function action({request, context}: ActionFunctionArgs) { {error}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } diff --git a/templates/skeleton/app/routes/account.orders.$id.tsx b/templates/skeleton/app/routes/account.orders.$id.tsx index 29af53a73c..18e9f596ce 100644 --- a/templates/skeleton/app/routes/account.orders.$id.tsx +++ b/templates/skeleton/app/routes/account.orders.$id.tsx @@ -40,20 +40,13 @@ export async function loader({params, context}: LoaderFunctionArgs) { firstDiscount?.__typename === 'PricingPercentageValue' && firstDiscount?.percentage; - return json( - { - order, - lineItems, - discountValue, - discountPercentage, - fulfillmentStatus, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({ + order, + lineItems, + discountValue, + discountPercentage, + fulfillmentStatus, + }); } export default function OrderRoute() { diff --git a/templates/skeleton/app/routes/account.orders._index.tsx b/templates/skeleton/app/routes/account.orders._index.tsx index 2b41dfc0e2..76ae6a63a2 100644 --- a/templates/skeleton/app/routes/account.orders._index.tsx +++ b/templates/skeleton/app/routes/account.orders._index.tsx @@ -34,14 +34,7 @@ export async function loader({request, context}: LoaderFunctionArgs) { throw Error('Customer orders not found'); } - return json( - {customer: data.customer}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({customer: data.customer}); } export default function Orders() { diff --git a/templates/skeleton/app/routes/account.profile.tsx b/templates/skeleton/app/routes/account.profile.tsx index 942a9981a7..2068e8fae5 100644 --- a/templates/skeleton/app/routes/account.profile.tsx +++ b/templates/skeleton/app/routes/account.profile.tsx @@ -26,14 +26,7 @@ export const meta: MetaFunction = () => { export async function loader({context}: LoaderFunctionArgs) { await context.customerAccount.handleAuthStatus(); - return json( - {}, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({}); } export async function action({request, context}: ActionFunctionArgs) { @@ -75,25 +68,15 @@ export async function action({request, context}: ActionFunctionArgs) { throw new Error('Customer profile update failed.'); } - return json( - { - error: null, - customer: data?.customerUpdate?.customer, - }, - { - headers: { - 'Set-Cookie': await context.session.commit(), - }, - }, - ); + return json({ + error: null, + customer: data?.customerUpdate?.customer, + }); } catch (error: any) { return json( {error: error.message, customer: null}, { status: 400, - headers: { - 'Set-Cookie': await context.session.commit(), - }, }, ); } diff --git a/templates/skeleton/app/routes/account.tsx b/templates/skeleton/app/routes/account.tsx index 574c2ea664..583e62c549 100644 --- a/templates/skeleton/app/routes/account.tsx +++ b/templates/skeleton/app/routes/account.tsx @@ -20,7 +20,6 @@ export async function loader({context}: LoaderFunctionArgs) { { headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Set-Cookie': await context.session.commit(), }, }, ); diff --git a/templates/skeleton/app/routes/cart.tsx b/templates/skeleton/app/routes/cart.tsx index 3c6eb69b65..12b475f071 100644 --- a/templates/skeleton/app/routes/cart.tsx +++ b/templates/skeleton/app/routes/cart.tsx @@ -68,8 +68,6 @@ export async function action({request, context}: ActionFunctionArgs) { headers.set('Location', redirectTo); } - headers.append('Set-Cookie', await context.session.commit()); - return json( { cart: cartResult, diff --git a/templates/skeleton/server.ts b/templates/skeleton/server.ts index ff1ce2ba87..448553607c 100644 --- a/templates/skeleton/server.ts +++ b/templates/skeleton/server.ts @@ -96,6 +96,10 @@ export default { const response = await handleRequest(request); + if (session.isPending) { + response.headers.set('Set-Cookie', await session.commit()); + } + if (response.status === 404) { /** * Check for redirects only when there's a 404 from the app.