Skip to main content

Legacy customer account flow in Hydrogen

This recipe converts a Hydrogen app from the new Customer Account API authentication to the legacy customer account flow using deprecated customer endpoints in Storefront API. This is useful for stores that haven't migrated to the new Customer Account API yet or need to maintain compatibility with existing customer authentication systems.

Key features:

  • Full customer registration and login flow with form-based authentication
  • Password recovery and reset functionality via email
  • Account activation via email tokens
  • Customer profile management with editable fields
  • Order history with detailed order views
  • Address management (create, edit, delete, set default)
  • Session-based authentication using customer access tokens
  • Secure server-side rendering for all account routes

Technical details:

  • Customer access tokens are stored in session cookies for authentication.
  • The login/register/recover routes use the account_ prefix to avoid layout nesting.
  • Account data routes use the account. prefix to inherit the account layout
Note

Consider migrating to the new Customer Account API for better security and features.


  • A Shopify store with customer accounts enabled (classic accounts, not new customer accounts)
  • Storefront API access with customer read/write permissions
  • Email notifications configured in your Shopify admin for:
    • Account activation emails
    • Password reset emails
    • Welcome emails (optional)

New files added to the template by this recipe.


Anchor to Step 1: Document legacy customer accounts in the READMEStep 1: Document legacy customer accounts in the README

Update the README file to document the legacy customer account flow.

@@ -1,10 +1,26 @@
-# Hydrogen template: Skeleton
+# Hydrogen template: Skeleton with Legacy Customer Account Flow
-Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
+Hydrogen is Shopify's stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify's full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen, enhanced with legacy customer account authentication flow.
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
[Get familiar with Remix](https://remix.run/docs/en/v1)
+## Legacy Customer Account Flow
+
+🚨 **Caution**: This legacy authentication strategy will not maintain authentication between your Hydrogen storefront and checkout; for better support, use either the New Customer Accounts strategy or Multipass.
+
+This template includes the legacy customer account flow made with [Storefront API](https://shopify.dev/docs/api/storefront) which provides:
+- Full customer registration and login flow
+- Password recovery and reset functionality
+- Account activation via email
+- Customer profile management
+- Order history and address management
+- Session-based authentication using customer access tokens
+
+**Note**: Passwordless login with [Customer Account API](https://shopify.dev/docs/api/customer) (introduced Jan 2024) is Shopify's recommended way to build headless customer experiences. Consider migrating to the new API for better security and features.
+
+🗒️ Read about the Customer Account API: [https://www.shopify.com/partners/blog/introducing-customer-account-api-for-headless-stores](https://www.shopify.com/partners/blog/introducing-customer-account-api-for-headless-stores)
+
## What's included
- Remix

Add an account link to the header navigation.

@@ -11,7 +11,8 @@ import {useAside} from '~/components/Aside';
interface HeaderProps {
header: HeaderQuery;
cart: Promise<CartApiQueryFragment | null>;
- isLoggedIn: Promise<boolean>;
+ // @description Use boolean instead of Promise for legacy authentication
+ isLoggedIn: boolean;
publicStoreDomain: string;
}
@@ -103,11 +104,8 @@ function HeaderCtas({
<nav className="header-ctas" role="navigation">
<HeaderMenuMobileToggle />
<NavLink prefetch="intent" to="/account" style={activeLinkStyle}>
- <Suspense fallback="Sign in">
- <Await resolve={isLoggedIn} errorElement="Sign in">
- {(isLoggedIn) => (isLoggedIn ? 'Account' : 'Sign in')}
- </Await>
- </Suspense>
+ {/* @description Display Account/Sign in based on legacy authentication status */}
+ {isLoggedIn ? 'Account' : 'Sign in'}
</NavLink>
<SearchToggle />
<CartToggle cart={cart} />

Anchor to Step 3: Create account activation flowStep 3: Create account activation flow

Add an account activation route for email verification.

File

import {Form, useActionData, data, redirect} from 'react-router';
import type {Route} from './+types/account_.activate.$id.$activationToken';

type ActionResponse = {
error: string | null;
};

export const meta: Route.MetaFunction = () => {
return [{title: 'Activate Account'}];
};

export async function loader({context}: Route.LoaderArgs) {
if (await context.session.get('customerAccessToken')) {
return redirect('/account');
}
return {};
}

export async function action({request, context, params}: Route.ActionArgs) {
const {session, storefront} = context;
const {id, activationToken} = params;

if (request.method !== 'POST') {
return data({error: 'Method not allowed'}, {status: 405});
}

try {
if (!id || !activationToken) {
throw new Error('Missing token. The link you followed might be wrong.');
}

const form = await request.formData();
const password = form.has('password') ? String(form.get('password')) : null;
const passwordConfirm = form.has('passwordConfirm')
? String(form.get('passwordConfirm'))
: null;

Anchor to Step 4: Update PageLayout for legacy authStep 4: Update PageLayout for legacy auth

Update PageLayout to handle account routes.

@@ -19,7 +19,8 @@ interface PageLayoutProps {
cart: Promise<CartApiQueryFragment | null>;
footer: Promise<FooterQuery | null>;
header: HeaderQuery;
- isLoggedIn: Promise<boolean>;
+ // @description Use boolean instead of Promise for legacy authentication
+ isLoggedIn: boolean;
publicStoreDomain: string;
children?: React.ReactNode;
}

Anchor to Step 5: Build password recovery flowStep 5: Build password recovery flow

Add a password recovery form.

File

import {Form, Link, useActionData, data, redirect} from 'react-router';
import type {Route} from './+types/account_.recover';

type ActionResponse = {
error?: string;
resetRequested?: boolean;
};

export async function loader({context}: Route.LoaderArgs) {
const customerAccessToken = await context.session.get('customerAccessToken');
if (customerAccessToken) {
return redirect('/account');
}

return {};
}

export async function action({request, context}: Route.ActionArgs) {
const {storefront} = context;
const form = await request.formData();
const email = form.has('email') ? String(form.get('email')) : null;

if (request.method !== 'POST') {
return data({error: 'Method not allowed'}, {status: 405});
}

try {
if (!email) {
throw new Error('Please provide an email.');
}
await storefront.mutate(CUSTOMER_RECOVER_MUTATION, {
variables: {email},
});

return {resetRequested: true};
} catch (error: unknown) {
const resetRequested = false;
if (error instanceof Error) {
return data({error: error.message, resetRequested}, {status: 400});
}
return data({error, resetRequested}, {status: 400});
}
}

export default function Recover() {
const action = useActionData<ActionResponse>();

return (
<div className="account-recover">
<div>
{action?.resetRequested ? (
<>
<h1>Request Sent.</h1>
<p>
If that email address is in our system, you will receive an email
with instructions about how to reset your password in a few
minutes.
</p>
<br />
<Link to="/account/login">Return to Login</Link>
</>
) : (
<>
<h1>Forgot Password.</h1>
<p>
Enter the email address associated with your account to receive a
link to reset your password.
</p>
<br />
<Form method="POST">
<fieldset>
<label htmlFor="email">Email</label>
<input
aria-label="Email address"
autoComplete="email"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
id="email"
name="email"
placeholder="Email address"
required
type="email"
/>
</fieldset>
{action?.error ? (
<p>
<mark>
<small>{action.error}</small>
</mark>
</p>
) : (
<br />
)}
<button type="submit">Request Reset Link</button>
</Form>
<div>
<br />
<p>
<Link to="/account/login">Login →</Link>
</p>
</div>
</>
)}
</div>
</div>
);
}

// NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerrecover
const CUSTOMER_RECOVER_MUTATION = `#graphql
mutation customerRecover(
$email: String!,
$country: CountryCode,
$language: LanguageCode
) @inContext(country: $country, language: $language) {
customerRecover(email: $email) {
customerUserErrors {
code
field
message
}
}
}
` as const;

Anchor to Step 6: Validate customer access tokensStep 6: Validate customer access tokens

Add customer access token validation to the root loader.

File

@@ -1,5 +1,6 @@
import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
import {
+ data,
Outlet,
useRouteError,
isRouteErrorResponse,
@@ -11,6 +12,7 @@ import {
useRouteLoaderData,
} from 'react-router';
import type {Route} from './+types/root';
+import type {CustomerAccessToken} from '@shopify/hydrogen/storefront-api-types';
import favicon from '~/assets/favicon.svg';
import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
import resetStyles from '~/styles/reset.css?url';
@@ -65,6 +67,9 @@ export function links() {
];
}
+// @description Export headers for legacy customer account flow
+export const headers: Route.HeadersFunction = ({loaderHeaders}) => loaderHeaders;
+
export async function loader(args: Route.LoaderArgs) {
// Start fetching non-critical data without blocking time to first byte
const deferredData = loadDeferredData(args);
@@ -74,23 +79,38 @@ export async function loader(args: Route.LoaderArgs) {
const {storefront, env} = args.context;
- return {
- ...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,
- withPrivacyBanner: false,
- // localize the privacy banner
- country: args.context.storefront.i18n.country,
- language: args.context.storefront.i18n.language,
+ // @description Validate customer access token for legacy authentication
+ const customerAccessToken = await args.context.session.get('customerAccessToken');
+
+ // validate the customer access token is valid
+ const {isLoggedIn, headers} = await validateCustomerAccessToken(
+ args.context.session,
+ customerAccessToken,
+ );
+
+ return data(
+ {
+ ...deferredData,
+ ...criticalData,
+ // @description Include isLoggedIn status for legacy authentication
+ isLoggedIn,
+ 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,
+ withPrivacyBanner: false,
+ // localize the privacy banner
+ country: args.context.storefront.i18n.country,
+ language: args.context.storefront.i18n.language,
+ },
},
- };
+ // @description Include headers for legacy authentication flow
+ {headers},
+ );
}
/**
@@ -207,3 +227,39 @@ export function ErrorBoundary() {
</div>
);
}
+
+// @description Validate customer access token for legacy authentication
+/**
+ * Validates the customer access token and returns a boolean and headers
+ * @see https://shopify.dev/docs/api/storefront/latest/objects/CustomerAccessToken
+ *
+ * @example
+ * ```js
+ * const {isLoggedIn, headers} = await validateCustomerAccessToken(
+ * customerAccessToken,
+ * session,
+ * );
+ * ```
+ */
+async function validateCustomerAccessToken(
+ session: Route.LoaderArgs['context']['session'],
+ customerAccessToken?: CustomerAccessToken,
+) {
+ let isLoggedIn = false;
+ const headers = new Headers();
+ if (!customerAccessToken?.accessToken || !customerAccessToken?.expiresAt) {
+ return {isLoggedIn, headers};
+ }
+
+ const expiresAt = new Date(customerAccessToken.expiresAt).getTime();
+ const dateNow = Date.now();
+ const customerAccessTokenExpired = expiresAt < dateNow;
+
+ if (customerAccessTokenExpired) {
+ session.unset('customerAccessToken');
+ } else {
+ isLoggedIn = true;
+ }
+
+ return {isLoggedIn, headers};
+}

Anchor to Step 7: Create customer registration flowStep 7: Create customer registration flow

Add a customer registration form.

File

import {Form, Link, useActionData, data, redirect} from 'react-router';
import type {Route} from './+types/account_.register';
import type {CustomerCreateMutation} from 'storefrontapi.generated';

type ActionResponse = {
error: string | null;
newCustomer:
| NonNullable<CustomerCreateMutation['customerCreate']>['customer']
| null;
};

export const headers: Route.HeadersFunction = ({actionHeaders}) => actionHeaders;

export async function loader({context}: Route.LoaderArgs) {
const customerAccessToken = await context.session.get('customerAccessToken');
if (customerAccessToken) {
return redirect('/account');
}

return {};
}

export async function action({request, context}: Route.ActionArgs) {
if (request.method !== 'POST') {
return data({error: 'Method not allowed'}, {status: 405});
}

const {storefront, session} = context;
const form = await request.formData();
const email = String(form.has('email') ? form.get('email') : '');
const password = form.has('password') ? String(form.get('password')) : null;
const passwordConfirm = form.has('passwordConfirm')
? String(form.get('passwordConfirm'))
: null;

const validPasswords =

Anchor to Step 8: Handle unauthenticated account routesStep 8: Handle unauthenticated account routes

Convert the catch-all route to use Storefront API authentication.

@@ -1,9 +1,9 @@
import {redirect} from 'react-router';
import type {Route} from './+types/account.$';
-// fallback wild card for all unauthenticated routes in account section
export async function loader({context}: Route.LoaderArgs) {
- context.customerAccount.handleAuthStatus();
-
- return redirect('/account');
+ if (await context.session.get('customerAccessToken')) {
+ return redirect('/account');
+ }
+ return redirect('/account/login');
}

Anchor to Step 9: Build password reset flowStep 9: Build password reset flow

Add a password reset form with token validation.

File

import {Form, useActionData, data, redirect} from 'react-router';
import type {Route} from './+types/account_.reset.$id.$resetToken';

type ActionResponse = {
error: string | null;
};

export const meta: Route.MetaFunction = () => {
return [{title: 'Reset Password'}];
};

export async function action({request, context, params}: Route.ActionArgs) {
if (request.method !== 'POST') {
return data({error: 'Method not allowed'}, {status: 405});
}
const {id, resetToken} = params;
const {session, storefront} = context;

try {
if (!id || !resetToken) {
throw new Error('customer token or id not found');
}

const form = await request.formData();
const password = form.has('password') ? String(form.get('password')) : '';
const passwordConfirm = form.has('passwordConfirm')
? String(form.get('passwordConfirm'))
: '';
const validInputs = Boolean(password && passwordConfirm);
if (validInputs && password !== passwordConfirm) {
throw new Error('Please provide matching passwords');
}

const {customerReset} = await storefront.mutate(CUSTOMER_RESET_MUTATION, {
variables: {
id: `gid://shopify/Customer/${id}`,
input: {password, resetToken},
},
});

if (customerReset?.customerUserErrors?.length) {
throw new Error(customerReset?.customerUserErrors[0].message);
}

if (!customerReset?.customerAccessToken) {
throw new Error('Access token not found. Please try again.');
}
session.set('customerAccessToken', customerReset.customerAccessToken);

return redirect('/account');
} catch (error: unknown) {
if (error instanceof Error) {
return data({error: error.message}, {status: 400});
}
return data({error}, {status: 400});
}
}

export default function Reset() {
const action = useActionData<ActionResponse>();

return (
<div className="account-reset">
<h1>Reset Password.</h1>
<p>Enter a new password for your account.</p>
<Form method="POST">
<fieldset>
<label htmlFor="password">Password</label>
<input
aria-label="Password"
autoComplete="current-password"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
id="password"
minLength={8}
name="password"
placeholder="Password"
required
type="password"
/>
<label htmlFor="passwordConfirm">Re-enter password</label>
<input
aria-label="Re-enter password"
autoComplete="current-password"
id="passwordConfirm"
minLength={8}
name="passwordConfirm"
placeholder="Re-enter password"
required
type="password"
/>
</fieldset>
{action?.error ? (
<p>
<mark>
<small>{action.error}</small>
</mark>
</p>
) : (
<br />
)}
<button type="submit">Reset</button>
</Form>
<br />
<p>
<a href="/account/login">Back to login →</a>
</p>
</div>
);
}

// NOTE: https://shopify.dev/docs/api/storefront/latest/mutations/customerreset
const CUSTOMER_RESET_MUTATION = `#graphql
mutation customerReset(
$id: ID!,
$input: CustomerResetInput!
$country: CountryCode
$language: LanguageCode
) @inContext(country: $country, language: $language) {
customerReset(id: $id, input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
` as const;

Anchor to Step 10: Add address managementStep 10: Add address management

Convert address management to use Storefront API mutations.

File

@@ -1,22 +1,14 @@
-import type {CustomerAddressInput} from '@shopify/hydrogen/customer-account-api-types';
-import type {
- AddressFragment,
- CustomerFragment,
-} from 'customer-accountapi.generated';
+import type {MailingAddressInput} from '@shopify/hydrogen/storefront-api-types';
+import type {AddressFragment, CustomerFragment} from 'storefrontapi.generated';
import {
data,
Form,
+ redirect,
useActionData,
useNavigation,
useOutletContext,
- type Fetcher,
} from 'react-router';
import type {Route} from './+types/account.addresses';
-import {
- UPDATE_ADDRESS_MUTATION,
- DELETE_ADDRESS_MUTATION,
- CREATE_ADDRESS_MUTATION,
-} from '~/graphql/customer-account/CustomerAddressMutations';
export type ActionResponse = {
addressId?: string | null;
@@ -32,13 +24,16 @@ export const meta: Route.MetaFunction = () => {
};
export async function loader({context}: Route.LoaderArgs) {
- context.customerAccount.handleAuthStatus();
-
+ const {session} = context;
+ const customerAccessToken = await session.get('customerAccessToken');
+ if (!customerAccessToken) {
+ return redirect('/account/login');

Anchor to Step 11: Show order detailsStep 11: Show order details

Convert the order details page to use Storefront API queries.

File

@@ -1,67 +1,50 @@
-import {redirect, useLoaderData} from 'react-router';
+import {Link, useLoaderData, redirect} from 'react-router';
import type {Route} from './+types/account.orders.$id';
-import {Money, Image} from '@shopify/hydrogen';
+import {Money, Image, flattenConnection} from '@shopify/hydrogen';
import type {
OrderLineItemFullFragment,
- OrderQuery,
-} from 'customer-accountapi.generated';
-import {CUSTOMER_ORDER_QUERY} from '~/graphql/customer-account/CustomerOrderQuery';
+ DiscountApplicationFragment,
+} from 'storefrontapi.generated';
export const meta: Route.MetaFunction = ({data}) => {
return [{title: `Order ${data?.order?.name}`}];
};
export async function loader({params, context}: Route.LoaderArgs) {
- const {customerAccount} = context;
+ const {session, storefront} = context;
+
if (!params.id) {
return redirect('/account/orders');
}
const orderId = atob(params.id);
- const {data, errors}: {data: OrderQuery; errors?: Array<{message: string}>} =
- await customerAccount.query(CUSTOMER_ORDER_QUERY, {
- variables: {
- orderId,
- language: customerAccount.i18n.language,
- },
- });
+ const customerAccessToken = await session.get('customerAccessToken');

Anchor to Step 12: Display order historyStep 12: Display order history

Convert the orders list to use the Storefront API with pagination.

File

@@ -1,222 +1,184 @@
-import {
- Link,
- useLoaderData,
- useNavigation,
- useSearchParams,
-} from 'react-router';
+import {Link, useLoaderData, data, redirect} from 'react-router';
import type {Route} from './+types/account.orders._index';
-import {useRef} from 'react';
import {
Money,
getPaginationVariables,
- flattenConnection,
} from '@shopify/hydrogen';
-import {
- buildOrderSearchQuery,
- parseOrderFilters,
- ORDER_FILTER_FIELDS,
- type OrderFilterParams,
-} from '~/lib/orderFilters';
-import {CUSTOMER_ORDERS_QUERY} from '~/graphql/customer-account/CustomerOrdersQuery';
import type {
CustomerOrdersFragment,
OrderItemFragment,
-} from 'customer-accountapi.generated';
+} from 'storefrontapi.generated';
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
-type OrdersLoaderData = {
- customer: CustomerOrdersFragment;
- filters: OrderFilterParams;
-};
-
export const meta: Route.MetaFunction = () => {
return [{title: 'Orders'}];

Anchor to Step 13: Build customer profile pageStep 13: Build customer profile page

Convert the customer profile page to use Storefront API queries.

File

@@ -1,12 +1,12 @@
-import type {CustomerFragment} from 'customer-accountapi.generated';
-import type {CustomerUpdateInput} from '@shopify/hydrogen/customer-account-api-types';
-import {CUSTOMER_UPDATE_MUTATION} from '~/graphql/customer-account/CustomerUpdateMutation';
+import type {CustomerFragment} from 'storefrontapi.generated';
+import type {CustomerUpdateInput} from '@shopify/hydrogen/storefront-api-types';
import {
- data,
Form,
useActionData,
useNavigation,
useOutletContext,
+ data,
+ redirect,
} from 'react-router';
import type {Route} from './+types/account.profile';
@@ -20,62 +20,79 @@ export const meta: Route.MetaFunction = () => {
};
export async function loader({context}: Route.LoaderArgs) {
- context.customerAccount.handleAuthStatus();
-
+ const customerAccessToken = await context.session.get('customerAccessToken');
+ if (!customerAccessToken) {
+ return redirect('/account/login');
+ }
return {};
}
export async function action({request, context}: Route.ActionArgs) {
- const {customerAccount} = context;
+ const {session, storefront} = context;
if (request.method !== 'PUT') {
return data({error: 'Method not allowed'}, {status: 405});

Anchor to Step 14: Update account layout for session authStep 14: Update account layout for session auth

Convert the account layout to use session-based authentication.

File

@@ -1,45 +1,105 @@
import {
- data as remixData,
Form,
NavLink,
Outlet,
useLoaderData,
+ data,
+ redirect,
} from 'react-router';
import type {Route} from './+types/account';
-import {CUSTOMER_DETAILS_QUERY} from '~/graphql/customer-account/CustomerDetailsQuery';
+import type {CustomerFragment} from 'storefrontapi.generated';
export function shouldRevalidate() {
return true;
}
-export async function loader({context}: Route.LoaderArgs) {
- const {customerAccount} = context;
- const {data, errors} = await customerAccount.query(
- CUSTOMER_DETAILS_QUERY,
- {
- variables: {
- language: customerAccount.i18n.language,
- },
- },
- );
+export const headers: Route.HeadersFunction = ({loaderHeaders}) => loaderHeaders;
- if (errors?.length || !data?.customer) {
- throw new Error('Customer not found');
+export async function loader({request, context}: Route.LoaderArgs) {
+ const {session, storefront} = context;
+ const {pathname} = new URL(request.url);
+ const customerAccessToken = await session.get('customerAccessToken');

Anchor to Step 15: Create login pageStep 15: Create login page

Replace the Customer Account API login with the Storefront API form.

File

@@ -1,7 +1,139 @@
+import {Form, Link, useActionData, data, redirect} from 'react-router';
import type {Route} from './+types/account_.login';
-export async function loader({request, context}: Route.LoaderArgs) {
- return context.customerAccount.login({
- countryCode: context.storefront.i18n.country,
- });
+type ActionResponse = {
+ error: string | null;
+};
+
+export const meta: Route.MetaFunction = () => {
+ return [{title: 'Login'}];
+};
+
+export async function loader({context}: Route.LoaderArgs) {
+ if (await context.session.get('customerAccessToken')) {
+ return redirect('/account');
+ }
+ return {};
}
+
+export async function action({request, context}: Route.ActionArgs) {
+ const {session, storefront} = context;
+
+ if (request.method !== 'POST') {
+ return data({error: 'Method not allowed'}, {status: 405});
+ }
+
+ try {
+ const form = await request.formData();
+ const email = String(form.has('email') ? form.get('email') : '');
+ const password = String(form.has('password') ? form.get('password') : '');
+ const validInputs = Boolean(email && password);
+

Anchor to Step 16: Handle logout and session cleanupStep 16: Handle logout and session cleanup

Replace the Customer Account API logout with a session cleanup.

@@ -1,11 +1,25 @@
-import {redirect} from 'react-router';
+import {data, redirect} from 'react-router';
import type {Route} from './+types/account_.logout';
-// if we don't implement this, /account/logout will get caught by account.$.tsx to do login
+export const meta: Route.MetaFunction = () => {
+ return [{title: 'Logout'}];
+};
+
export async function loader() {
+ return redirect('/account/login');
+}
+
+export async function action({request, context}: Route.ActionArgs) {
+ const {session} = context;
+ session.unset('customerAccessToken');
+
+ if (request.method !== 'POST') {
+ return data({error: 'Method not allowed'}, {status: 405});
+ }
+
return redirect('/');
}
-export async function action({context}: Route.ActionArgs) {
- return context.customerAccount.logout();
-}
+export default function Logout() {
+ return null;
+}
\ No newline at end of file

After applying this recipe:

  1. Run npm run codegen to generate GraphQL types for the Storefront API queries.
  2. Run npm run dev to start the development server.
  3. Test the authentication flow:
    • Visit /account/register to create a new account
    • Check your email for the activation link
    • Visit /account/login to sign in
    • Browse /account/orders to view order history
    • Visit /account/addresses to manage addresses
  4. Configure email templates in your Shopify admin:
    • Go to Settings > Notifications
    • Customize the Customer account activation email
    • Customize the Customer account password reset email
  5. Consider implementing:
    • "Remember me" functionality with longer session expiry
    • Social login integration
    • Two-factor authentication
    • Customer profile fields (phone, marketing preferences)

Was this page helpful?