Move Storefront queries onto @shopify/hydrogen preview client

Replaces the hand-rolled shopifyFetch wrapper and raw query strings with
the hydrogen preview package (0.0.0-preview-116d5d7-20260730141607), the
same build pinned in hydrogen-preview/.

- services/shopify/client: createStorefrontClient({type: "public"}) with a
  static request context. Two module-scoped clients — uncached for carts,
  products and customers; revalidate-3600 for shop policies — using the
  client's custom fetch option to carry Next's caching hints.
- graphql/*: every document wrapped in gql(), fragments composed via the
  second argument instead of string interpolation, and $country/$language
  declared with @inContext so hydrogen injects them.
- shopifyFetch keeps its {query, variables} call shape so call sites are
  unchanged, but is now generic over the document, so data is inferred. It
  re-raises GraphQL errors, which hydrogen returns rather than throws.

Env var names, the 2025-07 API version pin, and the client-side fetching
architecture are unchanged.

Turning on type coverage surfaced real defects, not just annotations:

- hydrogen gql check caught $discountCodes: [String!] used where the field
  requires [String!]!.
- Cart and customer mutation payloads are nullable and were dereferenced
  unconditionally, so a failed mutation threw a TypeError. Adds a shared
  unwrapCartPayload helper (collapsing five copies of the same userErrors
  check) and explicit null handling in the customer service, so a null
  payload reads as an error rather than success with no errors.
- Search results are a Product | Page | Article union; adds __typename to
  the queries and narrows on it.
- Widens nullable fields (altText, image, customer.email, totalTaxAmount)
  in the domain interfaces and in the structural duplicates some
  components declare locally.

Adds a typecheck script (tsc --noEmit && hydrogen gql check); it passes
with 0 errors. Two deprecation warnings are left alone as acting on them
would change behaviour: ProductOption.values and CartCost.totalTaxAmount.

Verified against mock.shop: build prerenders the policy pages through the
cached client, and in the browser the product grid, search with facets,
collection filter round-trip, add-to-cart and quantity update all work.
Customer account flows are untested — they need real credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JaxgqPbFxLsSuLPZom2kdC
This commit is contained in:
Rami Bitar
2026-08-08 10:24:38 -04:00
co-authored by Claude Opus 5
parent 95b191eeb8
commit f8c3ef9e0e
21 changed files with 468 additions and 215 deletions
+18 -3
View File
@@ -18,7 +18,8 @@ import {
export const CUSTOMER_TOKEN_COOKIE = 'customerAccessToken';
export interface CustomerUserError {
code?: string;
/** One of `CustomerErrorCode`; kept as a string so new codes don't break. */
code?: string | null;
field?: string[] | null;
message: string;
}
@@ -57,7 +58,7 @@ export interface CustomerOrder {
export interface Customer {
id: string;
email: string;
email: string | null;
firstName?: string | null;
lastName?: string | null;
phone?: string | null;
@@ -84,6 +85,13 @@ const firstMessage = (errors: CustomerUserError[]) =>
export { firstMessage as customerErrorMessage };
// Shopify returns a null mutation payload when the mutation could not run at
// all. That is not a success, so it surfaces as a generic error rather than an
// empty error list, which callers read as "it worked".
const MUTATION_FAILED: CustomerUserError[] = [
{ message: 'Something went wrong. Please try again.' },
];
export async function createCustomer(input: {
email: string;
password: string;
@@ -96,7 +104,10 @@ export async function createCustomer(input: {
variables: { input },
});
return { errors: response.data.customerCreate.customerUserErrors ?? [] };
const result = response.data.customerCreate;
if (!result) return { errors: MUTATION_FAILED };
return { errors: result.customerUserErrors ?? [] };
}
export async function login(
@@ -109,6 +120,7 @@ export async function login(
});
const result = response.data.customerAccessTokenCreate;
if (!result) return { token: null, errors: MUTATION_FAILED };
return {
token: result.customerAccessToken ?? null,
@@ -152,6 +164,7 @@ export async function resetPassword(
});
const result = response.data.customerReset;
if (!result) return { token: null, errors: MUTATION_FAILED };
return {
token: result.customerAccessToken ?? null,
@@ -170,6 +183,7 @@ export async function activateAccount(
});
const result = response.data.customerActivate;
if (!result) return { token: null, errors: MUTATION_FAILED };
return {
token: result.customerAccessToken ?? null,
@@ -210,6 +224,7 @@ export async function updateCustomer(
});
const result = response.data.customerUpdate;
if (!result) return { customer: null, errors: MUTATION_FAILED };
return {
customer: result.customer ?? null,