create Customer Account Clientutility
The function creates a GraphQL client for querying the Customer Account API. It also provides methods to authenticate and check if the user is logged in.
Anchor to createcustomeraccountclient(options)createCustomerAccountClient(options)
- Anchor to customerAccountIdcustomerAccountIdstringrequired
Unique UUID prefixed with
associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. Mock.shop doesn't automatically supply customerAccountId. Use
npx shopify hydrogen env pull
to link your store credentials.- Anchor to requestrequestrequired
The object for the current Request. It should be provided by your platform.
- Anchor to sessionsessionHydrogenSessionrequired
The client requires a session to persist the auth and refresh token. By default Hydrogen ships with cookie session storage, but you can use another session storage implementation.
- Anchor to shopIdshopIdstringrequired
The shop id. Mock.shop doesn't automatically supply shopId. Use
npx shopify hydrogen env pull
to link your store credentials- string
The oauth authorize path. Defaults to
.
- Anchor to authUrlauthUrlstring
This is the route in your app that authorizes the customer after logging in. Make sure to call
customer.authorize()
within the loader on this route. It defaults to.
- Anchor to customAuthStatusHandlercustomAuthStatusHandler() =>
Use this method to overwrite the default logged-out redirect behavior. The default handler throws a redirect to
with current path as
query param.
- Anchor to customerApiVersioncustomerApiVersionstring
Override the version of the API
- Anchor to defaultRedirectPathdefaultRedirectPathstring
The path to redirect to after login. Defaults to
/account
.- Anchor to languagelanguageLanguageCode
Localization data.
- Anchor to logErrorslogErrorsboolean | ((error?: Error) => boolean)
Whether it should print GraphQL errors automatically. Defaults to true
- Anchor to loginPathloginPathstring
The path to login. Defaults to
.
- Anchor to unstableB2bunstableB2bboolean
Deprecated.
is now stable. Please remove.
- Anchor to waitUntilwaitUntilWaitUntil
The waitUntil function is used to keep the current request/response lifecycle alive even after a response has been sent. It should be provided by your platform.
CustomerAccountOptions
- authorizePath
The oauth authorize path. Defaults to `/account/authorize`.
string
- authUrl
This is the route in your app that authorizes the customer after logging in. Make sure to call `customer.authorize()` within the loader on this route. It defaults to `/account/authorize`.
string
- customAuthStatusHandler
Use this method to overwrite the default logged-out redirect behavior. The default handler [throws a redirect](https://remix.run/docs/en/main/utils/redirect#:~:text=!session) to `/account/login` with current path as `return_to` query param.
() => DataFunctionValue
- customerAccountId
Unique UUID prefixed with `shp_` associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. Mock.shop doesn't automatically supply customerAccountId. Use `npx shopify hydrogen env pull` to link your store credentials.
string
- customerApiVersion
Override the version of the API
string
- defaultRedirectPath
The path to redirect to after login. Defaults to `/account`.
string
- language
Localization data.
LanguageCode
- logErrors
Whether it should print GraphQL errors automatically. Defaults to true
boolean | ((error?: Error) => boolean)
- loginPath
The path to login. Defaults to `/account/login`.
string
- request
The object for the current Request. It should be provided by your platform.
CrossRuntimeRequest
- session
The client requires a session to persist the auth and refresh token. By default Hydrogen ships with cookie session storage, but you can use [another session storage](https://remix.run/docs/en/main/utils/sessions) implementation.
HydrogenSession
- shopId
The shop id. Mock.shop doesn't automatically supply shopId. Use `npx shopify hydrogen env pull` to link your store credentials
string
- unstableB2b
Deprecated. `unstableB2b` is now stable. Please remove.
boolean
- waitUntil
The waitUntil function is used to keep the current request/response lifecycle alive even after a response has been sent. It should be provided by your platform.
WaitUntil
{
/** The client requires a session to persist the auth and refresh token. By default Hydrogen ships with cookie session storage, but you can use [another session storage](https://remix.run/docs/en/main/utils/sessions) implementation. */
session: HydrogenSession;
/** Unique UUID prefixed with `shp_` associated with the application, this should be visible in the customer account api settings in the Hydrogen admin channel. Mock.shop doesn't automatically supply customerAccountId. Use `npx shopify hydrogen env pull` to link your store credentials. */
customerAccountId: string;
/** The shop id. Mock.shop doesn't automatically supply shopId. Use `npx shopify hydrogen env pull` to link your store credentials */
shopId: string;
/** Override the version of the API */
customerApiVersion?: string;
/** The object for the current Request. It should be provided by your platform. */
request: CrossRuntimeRequest;
/** The waitUntil function is used to keep the current request/response lifecycle alive even after a response has been sent. It should be provided by your platform. */
waitUntil?: WaitUntil;
/** This is the route in your app that authorizes the customer after logging in. Make sure to call `customer.authorize()` within the loader on this route. It defaults to `/account/authorize`. */
authUrl?: string;
/** Use this method to overwrite the default logged-out redirect behavior. The default handler [throws a redirect](https://remix.run/docs/en/main/utils/redirect#:~:text=!session) to `/account/login` with current path as `return_to` query param. */
customAuthStatusHandler?: () => DataFunctionValue;
/** Whether it should print GraphQL errors automatically. Defaults to true */
logErrors?: boolean | ((error?: Error) => boolean);
/** The path to redirect to after login. Defaults to `/account`. */
defaultRedirectPath?: string;
/** The path to login. Defaults to `/account/login`. */
loginPath?: string;
/** The oauth authorize path. Defaults to `/account/authorize`. */
authorizePath?: string;
/** Deprecated. `unstableB2b` is now stable. Please remove. */
unstableB2b?: boolean;
/** Localization data. */
language?: LanguageCode;
}
DataFunctionValue
Response | NonNullable<unknown> | null
CrossRuntimeRequest
- headers
{ [key: string]: any; get?: (key: string) => string; }
- method
string
- url
string
{
url?: string;
method?: string;
headers: {
get?: (key: string) => string | null | undefined;
[key: string]: any;
};
}
Anchor to returnsReturns
- () => Promise<Response>
On successful login, the customer redirects back to your app. This function validates the OAuth response and exchanges the authorization code for an access token and refresh token. It also persists the tokens on your session. This function should be called and returned from the Remix loader configured as the redirect URI within the Customer Account API settings in admin.
- Anchor to getAccessTokengetAccessToken() => Promise<string>
Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed.
- Anchor to getApiUrlgetApiUrl() => string
Creates the fully-qualified URL to your store's GraphQL endpoint.
- Anchor to handleAuthStatushandleAuthStatus() => void |
Check for a not logged in customer and redirect customer to login page. The redirect can be overwritten with
option.
- Anchor to isLoggedInisLoggedIn() => Promise<boolean>
Returns if the customer is logged in. It also checks if the access token is expired and refreshes it if needed.
- Anchor to loginlogin(options?: ) => Promise<Response>
Start the OAuth login flow. This function should be called and returned from a Remix action. It redirects the customer to a Shopify login domain. It also defined the final path the customer lands on at the end of the oAuth flow with the value of the
query param. (This is automatically setup unless
option is in use)
- Anchor to logoutlogout(options?: ) => Promise<Response>
Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.
- Anchor to mutatemutate<TData = any>(mutation: string, options: ) => Promise<TData>
Execute a GraphQL mutation against the Customer Account API. This method execute
ahead of mutation.
- Anchor to queryquery<TData = any>(query: string, options: ) => Promise<TData>
Execute a GraphQL query against the Customer Account API. This method execute
ahead of query.
CustomerAccountForDocs
Below are types meant for documentation only. Ensure it stay in sync with the type above.
- authorize
On successful login, the customer redirects back to your app. This function validates the OAuth response and exchanges the authorization code for an access token and refresh token. It also persists the tokens on your session. This function should be called and returned from the Remix loader configured as the redirect URI within the Customer Account API settings in admin.
() => Promise<Response>
- getAccessToken
Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed.
() => Promise<string>
- getApiUrl
Creates the fully-qualified URL to your store's GraphQL endpoint.
() => string
- handleAuthStatus
Check for a not logged in customer and redirect customer to login page. The redirect can be overwritten with `customAuthStatusHandler` option.
() => void | DataFunctionValue
- isLoggedIn
Returns if the customer is logged in. It also checks if the access token is expired and refreshes it if needed.
() => Promise<boolean>
- login
Start the OAuth login flow. This function should be called and returned from a Remix action. It redirects the customer to a Shopify login domain. It also defined the final path the customer lands on at the end of the oAuth flow with the value of the `return_to` query param. (This is automatically setup unless `customAuthStatusHandler` option is in use)
(options?: LoginOptions) => Promise<Response>
- logout
Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.
(options?: LogoutOptions) => Promise<Response>
- mutate
Execute a GraphQL mutation against the Customer Account API. This method execute `handleAuthStatus()` ahead of mutation.
<TData = any>(mutation: string, options: CustomerAccountQueryOptionsForDocs) => Promise<TData>
- query
Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query.
<TData = any>(query: string, options: CustomerAccountQueryOptionsForDocs) => Promise<TData>
{
/** Start the OAuth login flow. This function should be called and returned from a Remix action.
* It redirects the customer to a Shopify login domain. It also defined the final path the customer
* lands on at the end of the oAuth flow with the value of the `return_to` query param. (This is
* automatically setup unless `customAuthStatusHandler` option is in use)
*
* @param options.uiLocales - The displayed language of the login page. Only support for the following languages:
* `en`, `fr`, `cs`, `da`, `de`, `es`, `fi`, `it`, `ja`, `ko`, `nb`, `nl`, `pl`, `pt-BR`, `pt-PT`,
* `sv`, `th`, `tr`, `vi`, `zh-CN`, `zh-TW`. If supplied any other language code, it will default to `en`.
* */
login?: (options?: LoginOptions) => Promise<Response>;
/** On successful login, the customer redirects back to your app. This function validates the OAuth response and exchanges the authorization code for an access token and refresh token. It also persists the tokens on your session. This function should be called and returned from the Remix loader configured as the redirect URI within the Customer Account API settings in admin. */
authorize?: () => Promise<Response>;
/** Returns if the customer is logged in. It also checks if the access token is expired and refreshes it if needed. */
isLoggedIn?: () => Promise<boolean>;
/** Check for a not logged in customer and redirect customer to login page. The redirect can be overwritten with `customAuthStatusHandler` option. */
handleAuthStatus?: () => void | DataFunctionValue;
/** Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed. */
getAccessToken?: () => Promise<string | undefined>;
/** Creates the fully-qualified URL to your store's GraphQL endpoint.*/
getApiUrl?: () => string;
/** Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.
*
* @param options.postLogoutRedirectUri - The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev.
* @param options.headers - These will be passed along to the logout redirect. You can use these to set/clear cookies on logout, like the cart.
* @param options.keepSession - If true, custom data in the session will not be cleared on logout.
* */
logout?: (options?: LogoutOptions) => Promise<Response>;
/** Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. */
query?: <TData = any>(
query: string,
options: CustomerAccountQueryOptionsForDocs,
) => Promise<TData>;
/** Execute a GraphQL mutation against the Customer Account API. This method execute `handleAuthStatus()` ahead of mutation. */
mutate?: <TData = any>(
mutation: string,
options: CustomerAccountQueryOptionsForDocs,
) => Promise<TData>;
}
DataFunctionValue
Response | NonNullable<unknown> | null
LoginOptions
- uiLocales
LanguageCode
{
uiLocales?: LanguageCode;
}
LogoutOptions
- headers
Add custom headers to the logout redirect.
HeadersInit
- keepSession
If true, custom data in the session will not be cleared on logout.
boolean
- postLogoutRedirectUri
The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev.
string
{
/** The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev. */
postLogoutRedirectUri?: string;
/** Add custom headers to the logout redirect. */
headers?: HeadersInit;
/** If true, custom data in the session will not be cleared on logout. */
keepSession?: boolean;
}
CustomerAccountQueryOptionsForDocs
- variables
The variables for the GraphQL statement.
Record<string, unknown>
{
/** The variables for the GraphQL statement. */
variables?: Record<string, unknown>;
}
Example code
examples
Example code
description
I am the default example
JavaScript
import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, } from '@shopify/remix-oxygen'; export default { async fetch(request, env, executionContext) { const session = await AppSession.init(request, [env.SESSION_SECRET]); /* 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 token for your store */ customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_ID, /* Shop Id */ shopId: env.SHOP_ID, request, session, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, 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); } }
TypeScript
import { createCustomerAccountClient, type HydrogenSession, } from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; export default { async fetch( request: Request, env: Record<string, string>, executionContext: ExecutionContext, ) { const session = await AppSession.init(request, [env.SESSION_SECRET]); /* 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, /* Shop Id */ shopId: env.SHOP_ID, request, session, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, 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, ) {} static async init(request: Request, secrets: string[]) { 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: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } 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); } }
Anchor to examplesExamples
Examples of how to opt out of default logged-out redirect
Anchor to example-customized-logged-out-behavior-for-the-entire-applicationCustomized logged-out behavior for the entire application
Anchor to example-exampleExample
Throw error instead of redirect
Example
examples
Example
description
Throw error instead of redirect
JavaScript
import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/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, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, 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, isRouteErrorResponse, useLocation, } from 'react-router'; export async function loader({context}) { const {data} = await context.customerAccount.query(`#graphql query getCustomer { customer { firstName lastName } } `); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( <a href={`/account/login?${new URLSearchParams({ return_to: location.pathname, }).toString()}`} > Login </a> ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return ( <div style={{marginTop: 24}}> {customer ? ( <> <div style={{marginBottom: 24}}> <b> Welcome {customer.firstName} {customer.lastName} </b> </div> </> ) : null} </div> ); }
TypeScript
import { createCustomerAccountClient, type HydrogenSession, } from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch( request: Request, env: Record<string, string>, 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, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, mode: process.env.NODE_ENV, /* Inject the customer account client in the Remix context */ getLoadContext: () => ({session, 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, ) {} static async init(request: Request, secrets: string[]) { 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: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } 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 'react-router' { /** * Declare local additions to the Remix loader context. */ interface AppLoadContext { customerAccount: CustomerAccount; session: AppSession; } // TODO: remove this once we've migrated to `Route.LoaderArgs` instead for our loaders interface LoaderFunctionArgs { context: AppLoadContext; } // TODO: remove this once we've migrated to `Route.ActionArgs` instead for our actions interface ActionFunctionArgs { context: AppLoadContext; } /** * Declare local additions to the Remix session data. */ interface SessionData extends HydrogenSessionData {} } ///////////////////////////////// // In a route import { useLoaderData, useRouteError, isRouteErrorResponse, useLocation, } from 'react-router'; import {type LoaderFunctionArgs} from '@shopify/remix-oxygen'; export async function loader({context}: LoaderFunctionArgs) { const {data} = await context.customerAccount.query<{ customer: {firstName: string; lastName: string}; }>(`#graphql query getCustomer { customer { firstName lastName } } `); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( <a href={`/account/login?${new URLSearchParams({ return_to: location.pathname, }).toString()}`} > Login </a> ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData<typeof loader>(); return ( <div style={{marginTop: 24}}> {customer ? ( <> <div style={{marginBottom: 24}}> <b> Welcome {customer.firstName} {customer.lastName} </b> </div> </> ) : null} </div> ); }
Anchor to example-opt-out-of-logged-out-behavior-for-a-single-routeOpt out of logged-out behavior for a single route
Anchor to example-exampleExample
Handle logged-out ahead of query
Example
examples
Example
description
Handle logged-out ahead of query
JavaScript
import {createCustomerAccountClient} from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/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, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, 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, isRouteErrorResponse, useLocation, } from 'react-router'; export async function loader({context}) { if (!(await context.customerAccount.isLoggedIn())) { throw new Response('Customer is not login', { status: 401, }); } const {data} = await context.customerAccount.query( `#graphql query getCustomer { customer { firstName lastName } } `, ); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( <a href={`/account/login?${new URLSearchParams({ return_to: location.pathname, }).toString()}`} > Login </a> ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData(); return ( <div style={{marginTop: 24}}> {customer ? ( <> <div style={{marginBottom: 24}}> <b> Welcome {customer.firstName} {customer.lastName} </b> </div> </> ) : null} </div> ); }
TypeScript
import { createCustomerAccountClient, type HydrogenSession, } from '@shopify/hydrogen'; // @ts-expect-error import * as reactRouterBuild from 'virtual:react-router/server-build'; import { createRequestHandler, createCookieSessionStorage, type SessionStorage, type Session, } from '@shopify/remix-oxygen'; // In server.ts export default { async fetch( request: Request, env: Record<string, string>, 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, /* Shop Id */ shopId: env.SHOP_ID, request, session, customAuthStatusHandler, }); const handleRequest = createRequestHandler({ build: reactRouterBuild, 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, ) {} static async init(request: Request, secrets: string[]) { 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: string) { return this.session.get(key); } destroy() { return this.sessionStorage.destroySession(this.session); } flash(key: string, value: any) { this.session.flash(key, value); } 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 'react-router'; import {type LoaderFunctionArgs} from '@shopify/remix-oxygen'; export async function loader({context}: LoaderFunctionArgs) { if (!(await context.customerAccount.isLoggedIn())) { throw new Response('Customer is not login', { status: 401, }); } const {data} = await context.customerAccount.query( `#graphql query getCustomer { customer { firstName lastName } } `, ); return {customer: data.customer}; } export function ErrorBoundary() { const error = useRouteError(); const location = useLocation(); if (isRouteErrorResponse(error)) { if (error.status == 401) { return ( <a href={`/account/login?${new URLSearchParams({ return_to: location.pathname, }).toString()}`} > Login </a> ); } } } // this should be an default export export function Route() { const {customer} = useLoaderData<typeof loader>(); return ( <div style={{marginTop: 24}}> {customer ? ( <> <div style={{marginBottom: 24}}> <b> Welcome {customer.firstName} {customer.lastName} </b> </div> </> ) : null} </div> ); }