import "server-only"; import { customerAccountConfig } from "@shared/config"; import { createCustomerAccountClient, gql } from "@shopify/hydrogen/customer-account"; import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { getCustomerAccessToken } from "@/lib/customer-account"; import { isCustomerAccountsAvailable } from "@/lib/storefront-config"; import { toURLSearchParams } from "@/lib/url-params"; export const metadata: Metadata = { title: "Account", }; const CUSTOMER_QUERY = gql(` query CurrentCustomer { customer { firstName lastName emailAddress { emailAddress } } } `); type AccountCustomer = { firstName?: string | null; lastName?: string | null; emailAddress?: { emailAddress?: string | null } | null; }; type AccountPageProps = { searchParams: Promise>; }; /** * `/account` — core Customer Accounts surface (name + email only). * * No `export const dynamic` / `export const fetchCache`: under * `cacheComponents: true`, `force-dynamic` is not allowed. The page relies on * the `AppShell` dynamic context (`await connection()` in the root layout's * shell), which opts the whole subtree into dynamic rendering — any * `headers()`/`cookies()` read inside (e.g. `getCustomerAccessToken()` → * `createCurrentRequest()` → `headers()`) is then automatically dynamic, the * same convention `cart/page.tsx` follows. * * Token refresh is delegated to the `/account/refresh` handler (intercepted in * `proxy.ts`): the page only **reads** the token via `getAccessToken` * (read-only, no mutation, no render-time commit — the proxy runs before RSC * render and cannot commit mutations that occur during render). When no usable * token is present, the page `redirect()`s to `/account/refresh?return_to=…`; * the handler refreshes, commits the cookie on its response, and redirects * back to `/account`, which now reads a valid token. */ export default async function AccountPage({ searchParams }: AccountPageProps) { const available = isCustomerAccountsAvailable(); // sync — no await const urlSearchParams = toURLSearchParams(await searchParams); const loginFailed = urlSearchParams.get("login") === "failed"; const refreshAttempted = urlSearchParams.get("refreshed") === "1"; if (!available) return ; const { accessToken, requestContext } = await getCustomerAccessToken(); if (!accessToken && !loginFailed && !refreshAttempted) { // Delegate the refresh to the `/account/refresh` handler instead of calling // `getOrRefreshAccessToken` here: this page only *reads* the token during // RSC render, and the proxy runs before render — it cannot commit a cookie // mutation that happens during render. Refreshing in-page would break that // no-render-commit invariant. The handler refreshes, sets the cookie on its // own response, and redirects back with `refreshed=1`. redirect(`/account/refresh?return_to=${encodeURIComponent("/account?refreshed=1")}`); } if (!accessToken) return ; // Build the client per-call (Next has no RR "context" equivalent; per-call // construction from `headers()` is the native Next approach, matching // `getStorefrontClient`). const customerAccount = createCustomerAccountClient({ shopId: customerAccountConfig.shopId, requestContext, }); // Keep the try/catch so a network/timeout failure surfaces as a friendly // error card instead of an unhandled 500 in RSC render. let customer: AccountCustomer | undefined; let error: string | undefined; try { const { data, errors } = await customerAccount.graphql(CUSTOMER_QUERY, { accessToken }); customer = errors ? undefined : data.customer; error = errors?.[0]?.message; } catch { error = "Customer Account API request failed. Try again later."; } return ; } type AccountShellProps = | { notice: "real-store" } | { loginFailed: boolean } | { customer?: AccountCustomer | null; error?: string }; function AccountShell(props: AccountShellProps) { return (

Account

Sign in with Shopify Customer Accounts to view your basic account identity.

{"notice" in props && } {"loginFailed" in props && } {"customer" in props && (props.error ? ( ) : ( ))}
); } function RealStoreNotice() { return (

Customer Accounts require a real store

This example is running against mock.shop, which has no Customer Account API. Set your store credentials in .env.local and run{" "} yarn https:dev to enable login.

); } function LoginPanel({ loginFailed }: { loginFailed: boolean }) { return (

Sign in

Use your customer account to view your name and email for this store.

{loginFailed ? (

We could not complete your login. Try signing in again.

) : null} {/* Plain `` — `/account/login` is handler-intercepted in `proxy.ts`. */} Log in
); } function CustomerAccountError({ message }: { message: string }) { return (

{message}

); } function CustomerCard({ customer }: { customer?: AccountCustomer | null }) { const name = [customer?.firstName, customer?.lastName].filter(Boolean).join(" ") || "Customer"; return (

Customer identity

{name}

{customer?.emailAddress?.emailAddress ? (

{customer.emailAddress.emailAddress}

) : null}
); }