Template
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
178 lines
3.6 KiB
TypeScript
178 lines
3.6 KiB
TypeScript
import { gql } from '@shopify/hydrogen';
|
|
|
|
// Cart Fragment for consistent cart data
|
|
const CartFragment = gql(`
|
|
fragment CartFragment on Cart {
|
|
id
|
|
checkoutUrl
|
|
totalQuantity
|
|
discountCodes {
|
|
code
|
|
applicable
|
|
}
|
|
cost {
|
|
subtotalAmount {
|
|
amount
|
|
currencyCode
|
|
}
|
|
totalAmount {
|
|
amount
|
|
currencyCode
|
|
}
|
|
totalTaxAmount {
|
|
amount
|
|
currencyCode
|
|
}
|
|
}
|
|
lines(first: 100) {
|
|
edges {
|
|
node {
|
|
id
|
|
quantity
|
|
cost {
|
|
totalAmount {
|
|
amount
|
|
currencyCode
|
|
}
|
|
}
|
|
merchandise {
|
|
... on ProductVariant {
|
|
id
|
|
title
|
|
selectedOptions {
|
|
name
|
|
value
|
|
}
|
|
price {
|
|
amount
|
|
currencyCode
|
|
}
|
|
image {
|
|
id
|
|
url
|
|
altText
|
|
width
|
|
height
|
|
}
|
|
product {
|
|
id
|
|
title
|
|
handle
|
|
vendor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
// Create a new cart
|
|
export const CREATE_CART_MUTATION = gql(
|
|
`
|
|
mutation CreateCart($lines: [CartLineInput!], $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cartCreate(input: { lines: $lines }) {
|
|
cart {
|
|
...CartFragment
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|
|
|
|
// Add lines to cart
|
|
export const ADD_CART_LINES_MUTATION = gql(
|
|
`
|
|
mutation AddCartLines($cartId: ID!, $lines: [CartLineInput!]!, $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
|
cart {
|
|
...CartFragment
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|
|
|
|
// Update cart lines
|
|
export const UPDATE_CART_LINES_MUTATION = gql(
|
|
`
|
|
mutation UpdateCartLines($cartId: ID!, $lines: [CartLineUpdateInput!]!, $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
|
cart {
|
|
...CartFragment
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|
|
|
|
// Remove lines from cart
|
|
export const REMOVE_CART_LINES_MUTATION = gql(
|
|
`
|
|
mutation RemoveCartLines($cartId: ID!, $lineIds: [ID!]!, $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
|
|
cart {
|
|
...CartFragment
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|
|
|
|
// Apply (or clear) discount codes on the cart
|
|
export const UPDATE_CART_DISCOUNT_CODES_MUTATION = gql(
|
|
`
|
|
mutation UpdateCartDiscountCodes($cartId: ID!, $discountCodes: [String!]!, $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) {
|
|
cart {
|
|
...CartFragment
|
|
}
|
|
userErrors {
|
|
field
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|
|
|
|
// Get cart by ID
|
|
export const GET_CART_QUERY = gql(
|
|
`
|
|
query GetCart($cartId: ID!, $country: CountryCode, $language: LanguageCode)
|
|
@inContext(country: $country, language: $language) {
|
|
cart(id: $cartId) {
|
|
...CartFragment
|
|
}
|
|
}
|
|
`,
|
|
[CartFragment]
|
|
);
|