Using the Customer Account API with Hydrogen
This tutorial shows how to create a login button on a Hydrogen storefront that lets a customer authenticate using the Customer Account API. If the login succeeds, then the site displays a welcome message with their email address.
Anchor to What you'll learnWhat you'll learn
After you've completed this tutorial, you'll be able to authenticate a Customer and make Customer Account API queries within a Hydrogen storefront.
Anchor to RequirementsRequirements
- You have completed the Getting started with the Customer Account API guide.
- You have completed Getting started with Hydrogen guide.
- You've installed the Hydrogen or Headless sales channel.
Anchor to LimitationsLimitations
- Multipass currently doesn't support Customer Account API. If you require single sign-on from an external website in your storefront, then you should use the code in our multipass example, which uses the legacy customer account flow.
- mock.shop, which provides example product data, doesn't support the Customer Account API. You must use a production store's credentials to work with the Customer Account API.
Anchor to Step 1: Set up a public domain for local developmentStep 1: Set up a public domain for local development
Customer Account API authentication doesn't support the use of localhost
due to security concerns. For development purposes, use a tunnelling service, such as ngrok.
In this step, you'll learn how to use ngrok to set up a public HTTPS domain that connects to your local Hydrogen application.
Anchor to Set up ngrokSet up ngrok
Install and run ngrok in your development environment.
- Set up an ngrok account.
- In your ngrok settings, add a static domain.
- Install the ngrok CLI.
- In a terminal, start ngrok using the following command:
Terminal
Anchor to Add your ngrok domain to the content security policyAdd your ngrok domain to the content security policy
Modify your Hydrogen app's content security policy to allow the development domain as a connect-src
. Your content security policy is typically located in /app/entry.server.tsx
.
/app/entry.server.tsx
Anchor to Step 2: Set up the environmentStep 2: Set up the environment
Configure the necessary Customer Account API settings in the Shopify admin so you can send the initial authentication request to Shopify.
Anchor to Open the Customer Account API settingsOpen the Customer Account API settings
- In the Shopify admin, open the Hydrogen sales channel.
- Click the storefront you're adding the customer account API functionality for.
- Click Storefront settings.
- Click Customer Account API to open the API settings.
Anchor to Update the application setupUpdate the application setup
For the Customer Account API to recognize your domain as a valid authentication host, edit your Customer Account API settings.
-
Under Application setup, click Edit
✎
to edit the endpoints. -
Under Callback URI(s), click Add Callback URI, and add your ngrok domain, with
/account/authorize
appended:https://<your-ngrok-domain>.app/account/authorizeThis is the URI your application will redirect to to continue the OAuth process after a successful customer login.
-
Under JavaScript origin(s), click Add origin, and then add your ngrok domain.
-
Under Logout URI, click Add Logout URI, and then add your ngrok domain.
If you don't see JavaScript origin(s) and Logout URI options, you'll need to switch to a Public client type. You can find this option at the top of the Customer Account API settings.
Anchor to Set up the environment variablesSet up the environment variables
There is only one environment variable needed to set up Customer Account API in your application:
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID
: A token prefixed with shp_
that represents a client secret used in all authentication requests. You can retrieve the token by navigating to the Customer Account API settings page > Customer Account API Credentials
Anchor to Production storefrontProduction storefront
When deploying to Oxygen, these variables are automatically created and used in your production environment.
Anchor to Local developmentLocal development
When developing Hydrogen locally, store your environment variables in an .env
file. You can automatically download the required variables with Shopify CLI:
- Run
npx shopify hydrogen link
in your Hydrogen project to link it to your Shopify store. - Run
npx shopify hydrogen env pull
to download your environment variables and write them to your local.env
file.
Anchor to Step 3: Create the Customer Account API clientStep 3: Create the Customer Account API client
The Skeleton template version 2024.7.5 and higher has a Customer Account API client by default and you can skip this step. Check package.json
to see your Skeleton template version.
If you need to manually create a Customer Account API client, then complete the following steps:
Create a new Customer Account API client in your server
file using the createCustomerAccountClient
utility.
Pass the new client to createCartHandler
to ensure that the logged-in customer is persisted from your store through to checkout.
Pass the new client to the application's context
so the utility can be accessed throughout the application.
The Customer Account API client uses the latest version of the API by default. If you need to use a specific version, then you can specify the version when you create the client.
Create a Customer Account API client
server.js
import * as remixBuild from '@remix-run/dev/server-build';
import {
createCartHandler,
storefrontRedirect,
createCustomerAccountClient,
} from '@shopify/hydrogen';
import {
createRequestHandler,
} from '@shopify/remix-oxygen';
import {AppSession} from '~/lib/session';
export default {
async fetch(
request,
env,
executionContext,
) {
try {
const waitUntil = executionContext.waitUntil.bind(executionContext);
const session = await AppSession.init(request, [env.SESSION_SECRET]);
const customerAccount = createCustomerAccountClient({
waitUntil,
request,
session,
customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
shopId: env.SHOP_ID,
});
const cart = createCartHandler({
customerAccount,
// additional options here
});
const handleRequest = createRequestHandler({
getLoadContext: () => ({
customerAccount,
}),
// additional options here
});
const response = await handleRequest(request);
if (response.status === 404) {
return storefrontRedirect({request, response, storefront});
}
return response;
} catch (error) {
console.error(error);
return new Response('An unexpected error occurred', {status: 500});
}
},
};
import * as remixBuild from '@remix-run/dev/server-build';
import {
createCartHandler,
storefrontRedirect,
createCustomerAccountClient,
} from '@shopify/hydrogen';
import {
createRequestHandler,
type AppLoadContext,
} from '@shopify/remix-oxygen';
import {AppSession} from '~/lib/session';
export default {
async fetch(
request: Request,
env: Env,
executionContext: ExecutionContext,
): Promise<Response> {
try {
const waitUntil = executionContext.waitUntil.bind(executionContext);
const session = await AppSession.init(request, [env.SESSION_SECRET]);
const customerAccount = createCustomerAccountClient({
waitUntil,
request,
session,
customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
shopId: env.SHOP_ID,
});
const cart = createCartHandler({
customerAccount,
// additional options here
});
const handleRequest = createRequestHandler({
getLoadContext: () => ({
customerAccount,
}),
// additional options here
});
const response = await handleRequest(request);
if (response.status === 404) {
return storefrontRedirect({request, response, storefront});
}
return response;
} catch (error) {
console.error(error);
return new Response('An unexpected error occurred', {status: 500});
}
},
};
createCustomerAccountClient
expects a session, implemented based on HydrogenSession
, to persist auth tokens and the customer's logged-in state.
You can view an example of a Hydrogen session in the Hydrogen GitHub repo.
Anchor to Step 4: Create auth routesStep 4: Create auth routes
Your application requires three routes for customer login and logout operations.
The default routes are as follows:
/account/login
: A route that redirects the user to a Shopify login./account/authorize
: A route that authorizes the customer after they log in./account/logout
: A route that logs the customer out.
If you chose to scaffold routes when creating your app, then your app already has the required routes in place. To generate a set of standard routes, including basic account-related functionality, run npx shopify hydrogen setup
.
Anchor to Create the login routeCreate the login route
Follow the following steps to create a customer login route in your Hydrogen storefront:
- In the
routes
folder, create a new file calledaccount_.login.[js|ts]
. - In the new file, add the
context.customerAccount.login()
function in the loader.
This function is responsible for redirecting users to Shopify to log in.
Create a login route
account_.login.js
export async function loader({context}) {
return context.customerAccount.login();
}
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
export async function loader({context}: LoaderFunctionArgs) {
return context.customerAccount.login();
}
Note the use of underscore in account_.login.ts
. This is to ensure that no layout is rendered when this route is accessed.
If you need to override the default behavior or change the login route location, then you can implement a customAuthStatusHandler
. Review an example implementation.
Follow the following steps to create an auth route in your Hydrogen storefront:
- In the
routes
folder, create a new file calledaccount_.authorize.[js|ts]
. - In the new file, add the
context.customerAccount.authorize()
function in the loader.
After a successful login, Shopify redirects to this authorize route. It continues the OAuth process, exchanges the access token, and persists the result to your application session.
If you choose to place this route somewhere else in the application, then use the authUrl
option with a relative url, and add the full public domain auth path in the Callback URI of the Customer Account API application setup
Create the authorize route
account_.authorize.js
export async function loader({context}) {
return context.customerAccount.authorize();
}
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
export async function loader({context}: LoaderFunctionArgs) {
return context.customerAccount.authorize();
}
At the end of this authorization step, the application redirects back to the page that initiated the login.
Use the customAuthStatusHandler
option to change this behavior.
Anchor to Create the logout routeCreate the logout route
Follow the following steps to create a logout route in your Hydrogen storefront:
- In the
routes
folder, create a new file calledaccount_.logout.[js|ts]
. - In the new file, add the
context.customerAccount.logout()
function in the action. Avoid including this function in the loader.
The logout action should be triggered by a user event, like clicking a logout button, not when a component is being loaded that can occur by page load or prefetching. This is because logging out is a significant action that can disrupt the user's workflow, so it should only happen when the user explicitly requests it.
Create a logout route
account_.logout.js
export async function action({context}) {
return context.customerAccount.logout();
}
import {type ActionFunctionArgs} from '@shopify/remix-oxygen';
export async function action({context}: ActionFunctionArgs) {
return context.customerAccount.logout();
}
You can set up a redirect that takes place after the logout step using the admin
setting in the application setup step.
Anchor to Step 5: Query the Customer Account APIStep 5: Query the Customer Account API
After you've set up your auth routes, you can start querying the Customer Account API.
In this step, you'll create a new account
route that queries for a logged in customer's first and last name.
-
In the
routes
folder, create a new file calledaccount.[jsx|tsx]
. -
Add the following code.
This code fetches the customer's first and last name from their account. If the customer isn't logged in, calling
query()
will trigger an automatic redirect to the login page, and redirect back to current page at the end of the auth process.
Query customer data
account.jsx
import {Form, useLoaderData} from '@remix-run/react';
import {json} from '@shopify/remix-oxygen';
export async function loader({context}) {
const {data, errors} = await context.customerAccount.query(`#graphql
query getCustomer {
customer {
firstName
lastName
}
}
`);
if (errors?.length || !data?.customer) {
throw new Error('Customer not found');
}
return json(
{customer: data.customer},
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Set-Cookie': await context.session.commit(),
},
},
);
}
export default function () {
const {customer} = useLoaderData();
return customer ? (
<>
<b>
Welcome {customer.firstName} {customer.lastName}
</b>
<Form method="post" action="/logout">
<button>Logout</button>
</Form>
</>
) : null;
}
import {Form, useLoaderData} from '@remix-run/react';
import {type LoaderFunctionArgs, json} from '@shopify/remix-oxygen';
export async function loader({context}: LoaderFunctionArgs) {
const {data, errors} = await context.customerAccount.query<{
customer: {firstName: string; lastName: string};
}>(`#graphql
query getCustomer {
customer {
firstName
lastName
}
}
`);
if (errors?.length || !data?.customer) {
throw new Error('Customer not found');
}
return json(
{customer: data.customer},
{
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Set-Cookie': await context.session.commit(),
},
},
);
}
export default function () {
const {customer} = useLoaderData<typeof loader>();
return customer ? (
<>
<b>
Welcome {customer.firstName} {customer.lastName}
</b>
<Form method="post" action="/logout">
<button>Logout</button>
</Form>
</>
) : null;
}
You need to commit the session at the end of a loader/action with any customer logged in check.
A logged in check can trigger an access token refresh, which won't persist in your application unless session is committed to the Set-Cookie
header.
The query
and mutate
functions follow GraphQL standards and return both data
and errors
objects. The errors
object typically returns GraphQL errors such as a syntax error for querying for unknown field name.
Most of the time, the existence of the errors
object means that the query isn't successful and there is nothing to show. However, during a mutation, it is possible to receive partial data while still experiencing errors.
You should always handle errors gracefully, either by showing a message to the user, or re-throwing them for your application's ErrorBoundary
to catch.
Never cache Customer Account API data or store personally identifiable information (PII). Caching this type of data causes risk of unauthorized access or data breaches, compromising user privacy and security.
Anchor to Step 6: Check the customer logged in stateStep 6: Check the customer logged in state
You can check whether a visitor is logged in without triggering an automatic login redirect. For instance, you might want to conditionally display a Log in or Account details link in a menu.
- In the root file of your application, add
customerAccount.isLoggedIn()
in the loader. - Return this promise using Remix's deferred data loading pattern. This allows the user interface to render before the login check is complete.
Note that you need to commit()
the session at the end of any loader or action that checks a customer's logged-in state. This is because a logged-in check can trigger an access token refresh, which won't persist in your application unless the session is committed to the Set-Cookie
header.
Check customer logged-in state
root.jsx
import {defer} from '@shopify/remix-oxygen';
import {Await, NavLink, useLoaderData} from '@remix-run/react';
import {Suspense} from 'react';
export async function loader({context}) {
const isLoggedInPromise = context.customerAccount.isLoggedIn();
return defer(
{isLoggedInPromise},
{
headers: {
'Set-Cookie': await context.session.commit(),
},
},
);
}
export default function App() {
const {isLoggedInPromise} = useLoaderData();
return (
<html lang="en">
<body>
<header className="header">
<NavLink prefetch="intent" to="/account">
<Suspense fallback="Sign in">
<Await resolve={isLoggedInPromise} errorElement="Sign in">
{(isLoggedIn) => (isLoggedIn ? 'Account' : 'Sign in')}
</Await>
</Suspense>
</NavLink>
</header>
{/* Rest of the application */}
</body>
</html>
);
}
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Await, NavLink, useLoaderData} from '@remix-run/react';
import {Suspense} from 'react';
export async function loader({context}: LoaderFunctionArgs) {
const isLoggedInPromise = customerAccount.isLoggedIn();
return defer(
{isLoggedInPromise},
{
headers: {
'Set-Cookie': await context.session.commit(),
},
},
);
}
export default function App() {
const {isLoggedInPromise} = useLoaderData<typeof loader>();
return (
<html lang="en">
<body>
<header className="header">
<NavLink prefetch="intent" to="/account">
<Suspense fallback="Sign in">
<Await resolve={isLoggedInPromise} errorElement="Sign in">
{(isLoggedIn) => (isLoggedIn ? 'Account' : 'Sign in')}
</Await>
</Suspense>
</NavLink>
</header>
{/* Rest of the application */}
</body>
</html>
);
}
Anchor to Step 7: Associate a customer with a cartStep 7: Associate a customer with a cart
You can associate a customer with a cart by obtaining a Storefront API CustomerAccessToken
.
-
Update the Customer Account API client to use the
unstableB2b
option.lib/context.ts
const hydrogenContext = createHydrogenContext({...,customerAccount: {unstableB2b: true,},}); -
Access the
CustomerAccessToken
and pass it to thecart.updateBuyerIdentity
function.File
export async function action({context}) {const {cart, customerAccount} = context;const buyer = await customerAccount.UNSTABLE_getBuyer();await cart.updateBuyerIdentity({customerAccessToken: buyer.customerAccessToken,})}
The CustomerAccessToken
returned by Customer Account Client can only be used to update the buyer identity of a cart. It cannot be used with a Storefront API customer
query.
Anchor to Next stepsNext steps
- Explore the GraphQL Customer Account API reference.
- Explore the
createCustomerAccountClient
reference, including additional examples. - Explore an end-to-end Customer Account API implementation example in the default Hydrogen template.
- Learn how to Manage customer accounts with the Customer Account API