Skip to content

Conversation

@yuriassuncx
Copy link
Contributor

@yuriassuncx yuriassuncx commented Dec 4, 2025

What is this Contribution About?

Implementing Headless Account for Shopify on Deco.cx

Description

This PR adds Headless Account functionality for Shopify on Deco.cx, enabling user account management via API without relying on Shopify’s default interface.

Completed Tasks

New actions:

  • Create address (Create Address)
  • Set default address (Set Default Address)
  • Update address (Update Address)
  • Delete address (Delete Address)
  • Update customer info (Update Customer Info)
  • Send password reset email (Send Password Reset Email)

New Loader:

  • Get Addresses (Get Addresses)
  • List Customer Orders (List Customer Orders)

Loom

https://www.loom.com/share/2b07a3b412674d7d8adef2fb004b2b5e

Reference

Shopify Storefront GraphQL API - Postman

Summary by CodeRabbit

  • New Features
    • Address management: create, update, delete, and set a default customer address.
    • Password recovery: send password reset emails.
    • Customer profile edits: update name, email, and marketing preferences.
    • Order history browsing: view paginated order lists with cursor-based navigation.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 4, 2025

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.133.14 update
  • 🎉 for Minor 0.134.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

Walkthrough

Adds Shopify customer address CRUD actions, a password reset and user-update action, a paginated orders loader, corresponding GraphQL operations and generated types, and wires these into the manifest.

Changes

Cohort / File(s) Summary
Address Management Actions
shopify/actions/address/createAddress.ts, shopify/actions/address/deleteAddress.ts, shopify/actions/address/setDefaultAddress.ts, shopify/actions/address/updateAddress.ts
New server actions to create, delete, set default, and update customer addresses. Each reads customer access token from cookies, returns null when unauthenticated, executes typed storefront GraphQL mutation with required variables, and returns the mutation payload.
User Account Actions
shopify/actions/user/sendPasswordResetEmail.ts, shopify/actions/user/updateUser.ts
New actions: one sends a password-reset email via a storefront mutation; the other updates customer profile (email, names, acceptsMarketing) using customerAccessToken from cookies and returns the mutation payload or null when unauthenticated.
Order Pagination Loader
shopify/loaders/orders/list.ts
New loader that enforces authentication, derives cursor/page state from URL query and props, queries OrdersByCustomer, validates presence of pageInfo, and returns orders with pagination metadata including next/previous page URLs and currentPage.
GraphQL Operations & Types
shopify/utils/storefront/queries.ts, shopify/utils/storefront/storefront.graphql.gen.ts
Added GraphQL descriptors and generated TypeScript types for: FetchCustomerAddresses, OrdersByCustomer, UpdateCustomerInfo, SendPasswordResetEmail, CreateAddress, UpdateAddress, SetDefaultAddress, DeleteAddress.
Manifest Wiring
shopify/manifest.gen.ts
Updated manifest imports and maps to include new address actions, user actions (including sendPasswordResetEmail), and the new orders loader; reindexed existing loaders/actions accordingly.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Client
participant ServerAction as Server Action
participant CookieStore as getUserCookie
participant Storefront as Storefront Client
participant Shopify as Shopify GraphQL
Client->>ServerAction: HTTP request (props)
ServerAction->>CookieStore: read customerAccessToken(req.headers)
alt token missing
CookieStore-->>ServerAction: null
ServerAction-->>Client: null / 401
else token present
CookieStore-->>ServerAction: customerAccessToken
ServerAction->>Storefront: storefront.query(variables, mutation)
Storefront->>Shopify: GraphQL request
Shopify-->>Storefront: GraphQL response
Storefront-->>ServerAction: payload
ServerAction-->>Client: payload

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • Pay attention to: cookie extraction consistency across actions, correct GraphQL variable shapes vs. generated types, pagination cursor math and URL construction in orders/list.ts, and accurate manifest import/mapping indices.

Possibly related PRs

  • fix check #1418 — touches generated GraphQL types in shopify/utils/storefront/storefront.graphql.gen.ts and may overlap with type additions here.

Suggested reviewers

  • viktormarinho
  • guitavano

Poem

🐇 I hopped through code with ears held high,
New addresses, orders, and reset links to try.
Mutations and pages in a neat little row,
GraphQL carrots where customer data grow.
Cheers — a small hop for a smoother buy!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat(Shopify): headless account' clearly summarizes the main change: implementing headless account functionality for Shopify.
Description check ✅ Passed The PR description provides a clear overview of changes, lists completed tasks for actions and loaders, includes a Loom video link, and provides a reference link. However, it is missing the Issue Link and Demonstration Link sections from the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8486144 and 11f86dc.

📒 Files selected for processing (1)
  • shopify/utils/storefront/storefront.graphql.gen.ts (3 hunks)
🔇 Additional comments (8)
shopify/utils/storefront/storefront.graphql.gen.ts (8)

7912-7917: Customer addresses query types look consistent with Shopify schema

FetchCustomerAddressesQueryVariables and FetchCustomerAddressesQuery correctly model customer(customerAccessToken) { addresses { edges { node { … }}}} with the expected MailingAddress fields. The shape matches existing connection patterns in this file and should work cleanly with the “Get Addresses” loader.


7919-7926: OrdersByCustomer query typing is sound and pagination‑ready

OrdersByCustomerQueryVariables exposes first and after for pagination, and OrdersByCustomerQuery includes orders.totalCount, pageInfo, and nodes with lineItems.nodes.variant shaped as per Order/OrderLineItem. This should be sufficient for the “List Customer Orders” loader and avoids over‑fetching by relying on connection arguments at the call‑site.


7960-7967: UpdateCustomerInfo mutation types correctly surface both error channels

UpdateCustomerInfoMutationVariables and UpdateCustomerInfoMutation match customerUpdate(customerAccessToken, customer: CustomerUpdateInput) and return customer.id, customerUserErrors, and userErrors. This gives the caller access to both the newer and deprecated error arrays, which is helpful for robust error handling.


8018-8023: Password reset email mutation types align with customerRecover

SendPasswordResetEmailMutationVariables and SendPasswordResetEmailMutation correctly model customerRecover(email) and expose both customerUserErrors (with code) and generic userErrors. That’s the right info surface for distinguishing user vs generic failures in the headless flow.


8025-8032: CreateAddress mutation types are minimal and sufficient

CreateAddressMutationVariables/CreateAddressMutation map to customerAddressCreate(customerAccessToken, address) and return customerAddress.id plus customerUserErrors. Skipping the deprecated userErrors field is fine here as long as the caller uses customerUserErrors consistently.


8033-8041: UpdateAddress mutation types mirror the underlying payload correctly

UpdateAddressMutationVariables and UpdateAddressMutation properly cover customerAddressUpdate(id, customerAccessToken, address) and include customerAddress.id, customerUserErrors, and userErrors, matching CustomerAddressUpdatePayload. This should give enough detail for UI error reporting.


8042-8048: SetDefaultAddress mutation types correctly expose the new default

SetDefaultAddressMutationVariables/SetDefaultAddressMutation align with customerDefaultAddressUpdate(customerAccessToken, addressId) and return customer.defaultAddress.id plus customerUserErrors. That’s exactly what the caller needs to confirm the new default address.


8050-8056: DeleteAddress mutation types capture all relevant outputs

DeleteAddressMutationVariables and DeleteAddressMutation match customerAddressDelete(customerAccessToken, id) and expose deletedCustomerAddressId, customerUserErrors, and userErrors. This is consistent with the schema and supports both optimistic updates and robust error handling in the headless account flows.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
shopify/actions/address/updateAddress.ts (1)

9-60: Action wiring and mutation variables look correct; consider reusing a shared address shape

The token guard, destructuring, and storefront.query<CustomerAddressUpdatePayload, MutationCustomerAddressUpdateArgs> call all look correct and consistent with the UpdateAddress mutation.

Given createAddress and updateAddress share the same address fields (address/country/province/city/zip), consider extracting a shared AddressProps/MailingAddressProps type and reusing it across actions to keep them in sync if you add fields like address2, firstName, or phone later.

shopify/actions/address/createAddress.ts (1)

9-48: Create‑address logic is sound; consider consolidating address props

The action correctly:

  • Guards on customerAccessToken.
  • Maps addressaddress1 and forwards country/province/city/zip.
  • Uses storefront.query<CustomerAddressCreatePayload, MutationCustomerAddressCreateArgs> with the expected variables.

As with updateAddress, you might want a shared AddressProps/MailingAddressProps type reused between create/update to avoid the two drifting if you add more address fields later.

shopify/utils/storefront/queries.ts (2)

458-475: Consider adding pagination and more address fields.

The query hardcodes first: 10 and lacks pagination support. Customers with more than 10 addresses won't see all of them. Additionally, common address fields like address2, firstName, lastName, phone, and company are missing, which may limit the usefulness of the returned data.

Consider this enhancement:

 export const FetchCustomerAddresses = {
-  query: gql`query FetchCustomerAddresses($customerAccessToken: String!) {
+  query: gql`query FetchCustomerAddresses($customerAccessToken: String!, $first: Int = 10, $after: String) {
     customer(customerAccessToken: $customerAccessToken) {
-      addresses(first: 10) {
+      addresses(first: $first, after: $after) {
         edges {
           node {
             address1
+            address2
             city
+            company
             country
+            firstName
             id
+            lastName
+            phone
             province
             zip
           }
         }
+        pageInfo {
+          hasNextPage
+          endCursor
+        }
       }
     }
   }`,
 };

648-666: Minor inconsistency: missing userErrors field.

For consistency with UpdateAddress and DeleteAddress, consider adding userErrors to the response.

     customerAddressCreate(
       customerAccessToken: $customerAccessToken,
       address: $address
     ) {
       customerAddress {
         id
       }
       customerUserErrors {
         code
         message
       }
+      userErrors {
+        message
+      }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd8a9eb and 8486144.

📒 Files selected for processing (9)
  • shopify/actions/address/createAddress.ts (1 hunks)
  • shopify/actions/address/deleteAddress.ts (1 hunks)
  • shopify/actions/address/setDefaultAddress.ts (1 hunks)
  • shopify/actions/address/updateAddress.ts (1 hunks)
  • shopify/actions/user/sendPasswordResetEmail.ts (1 hunks)
  • shopify/actions/user/updateUser.ts (1 hunks)
  • shopify/loaders/orders/list.ts (1 hunks)
  • shopify/manifest.gen.ts (1 hunks)
  • shopify/utils/storefront/queries.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
shopify/actions/address/deleteAddress.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (2)
  • CustomerAddressDeletePayload (3200-3210)
  • MutationCustomerAddressDeleteArgs (5343-5346)
shopify/utils/storefront/queries.ts (1)
  • DeleteAddress (717-738)
shopify/actions/address/updateAddress.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (1)
  • CustomerAddressUpdatePayload (3213-3223)
shopify/utils/storefront/queries.ts (1)
  • UpdateAddress (668-693)
shopify/actions/user/updateUser.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (1)
  • CustomerUpdatePayload (3376-3392)
shopify/utils/storefront/queries.ts (1)
  • UpdateCustomerInfo (570-593)
shopify/loaders/orders/list.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (1)
  • QueryRootLocationsArgs (6669-6677)
shopify/utils/storefront/queries.ts (1)
  • OrdersByCustomer (477-533)
shopify/actions/user/sendPasswordResetEmail.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (2)
  • CustomerRecoverPayload (3306-3314)
  • MutationCustomerRecoverArgs (5371-5373)
shopify/utils/storefront/queries.ts (1)
  • SendPasswordResetEmail (634-646)
shopify/actions/address/createAddress.ts (2)
shopify/utils/storefront/storefront.graphql.gen.ts (1)
  • CustomerAddressCreatePayload (3187-3197)
shopify/utils/storefront/queries.ts (1)
  • CreateAddress (648-666)
🔇 Additional comments (11)
shopify/actions/address/deleteAddress.ts (1)

9-41: Delete action is correctly wired to the mutation

addressIdid mapping, token guard, and storefront.query<CustomerAddressDeletePayload, MutationCustomerAddressDeleteArgs> usage all look correct and consistent with DeleteAddress. No changes needed here.

shopify/actions/user/sendPasswordResetEmail.ts (1)

8-32: Password‑reset action is correctly implemented

The action cleanly forwards { email } into SendPasswordResetEmail and returns the typed CustomerRecoverPayload, letting the caller inspect customerUserErrors/userErrors. No changes needed here.

shopify/actions/address/setDefaultAddress.ts (1)

9-41: Default‑address mutation wiring looks good

Token handling, addressId mapping, and the typed storefront.query<CustomerDefaultAddressUpdatePayload, MutationCustomerDefaultAddressUpdateArgs> call all look consistent with the SetDefaultAddress mutation and the other address actions. No changes required.

shopify/manifest.gen.ts (1)

5-57: Generated manifest wiring for new actions/loaders looks consistent

The new imports and entries for:

  • address actions (createAddress, deleteAddress, setDefaultAddress, updateAddress),
  • cart/order/user actions, and
  • the orders/list loader plus proxy, shop, and user loaders

all look coherent and match the file paths used elsewhere in the PR. As this file is generator‑owned, just ensure it’s regenerated (not hand‑edited) whenever actions or loaders move.

shopify/actions/user/updateUser.ts (1)

9-43: Update customer mutation looks correct; tie Props to the generated input type for future‑proofing

The action is correctly implemented: token handling, UpdateCustomerInfo usage, and the typed storefront.query<CustomerUpdatePayload, MutationCustomerUpdateArgs> call all align with the GraphQL mutation.

To avoid drift if you later expand the payload, consider defining:

import type { CustomerUpdateInput } from "../../utils/storefront/storefront.graphql.gen.ts";

interface Props extends CustomerUpdateInput {
  // Optionally narrow to the subset you expose in the UI
}

and then using customer: props instead of a separate Props shape. That keeps this action automatically in sync with Shopify's CustomerUpdateInput.

shopify/utils/storefront/queries.ts (6)

477-533: LGTM!

Well-structured query with proper pagination support, relevant order fields, and reverse: true for showing newest orders first. The structure follows existing patterns in the file.


570-593: LGTM!

The mutation follows Shopify's standard patterns and includes both customerUserErrors and userErrors for comprehensive error handling.


634-646: LGTM!

Clean implementation of the password reset flow using Shopify's customerRecover mutation with proper error handling.


668-693: LGTM!

Complete mutation with proper error handling including both customerUserErrors and userErrors.


695-715: LGTM!

The mutation correctly returns the new default address ID for confirmation.


717-738: LGTM!

Well-implemented delete mutation returning deletedCustomerAddressId for client-side state management, with comprehensive error handling.

Comment on lines +72 to +129
const { count = 12, pageOffset = 1 } = props;
const pageParam = searchParams.get("page")
? Number(searchParams.get("page")) - pageOffset
: 0;

const page = props.page ?? pageParam;
const startCursor = props.startCursor ?? searchParams.get("startCursor") ??
"";
const endCursor = props.endCursor ?? searchParams.get("endCursor") ?? "";

const variables = {
customerAccessToken,
...(startCursor && { after: startCursor, first: count }),
...(endCursor && { before: endCursor, last: count }),
...(!startCursor && !endCursor && { first: count }),
};

const data = await storefront.query<
QueryRoot,
QueryRootCustomerArgs & QueryRootLocationsArgs
>({
...OrdersByCustomer,
variables,
});

const orders = data.customer?.orders?.nodes ?? [];
const pageInfo = data.customer?.orders?.pageInfo;

if (!pageInfo) {
throw new Error("Missing pageInfo from Shopify response");
}

const nextPage = new URLSearchParams(searchParams);
const previousPage = new URLSearchParams(searchParams);

if (pageInfo.hasNextPage) {
nextPage.set("page", (page + pageOffset + 1).toString());
nextPage.set("startCursor", pageInfo.endCursor ?? "");
nextPage.delete("endCursor");
}

if (pageInfo.hasPreviousPage) {
previousPage.set("page", (page + pageOffset - 1).toString());
previousPage.set("endCursor", pageInfo.startCursor ?? "");
previousPage.delete("startCursor");
}

const currentPage = Math.max(1, page + pageOffset);

return {
orders,
pageInfo: {
nextPage: pageInfo.hasNextPage ? `?${nextPage}` : undefined,
previousPage: pageInfo.hasPreviousPage ? `?${previousPage}` : undefined,
currentPage,
records: data.customer?.orders.totalCount ?? 0,
recordPerPage: count,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Previous‑page pagination is inconsistent with the GraphQL query; also harden page parsing

The loader is close, but two things are worth fixing:

  1. Backward pagination doesn’t currently match the OrdersByCustomer query
  • The query definition you’re using only declares $customerAccessToken, $first, and $after, and passes them into orders(first: $first, after: $after, reverse: true).

  • The loader, however, builds variables with before and last when going backwards:

    const variables = {
      customerAccessToken,
      ...(startCursor && { after: startCursor, first: count }),
      ...(endCursor && { before: endCursor, last: count }),
      ...(!startCursor && !endCursor && { first: count }),
    };
  • Because $before/$last are not declared or used in the query, a “previous page” request will not actually page backwards based on endCursor, and may even fall back to Shopify’s defaults.

To make previous‑page links behave correctly, you should either:

  • extend OrdersByCustomer to accept and forward $before/$last to the orders connection, and keep the current variable construction, e.g.:

    // shopify/utils/storefront/queries.ts (conceptual)
    query OrdersByCustomer(
      $customerAccessToken: String!,
      $first: Int,
      $after: String,
      $last: Int,
      $before: String
    ) {
      customer(customerAccessToken: $customerAccessToken) {
        orders(
          first: $first,
          after: $after,
          last: $last,
          before: $before,
          reverse: true
        ) {
          ...
        }
      }
    }

    and keep the loader’s variables as-is (ensuring you never send both forward and backward args at the same time), or

  • if you want only forward pagination for now, drop the before/last branch and previousPage computation until the query supports them, so the UI doesn’t expose a non‑working prev link.

Given the PR description mentions a customer order list with pagination, aligning the query and loader for real backward pagination is preferable.

  1. Guard against invalid page query parameters

If page is non‑numeric, Number(searchParams.get("page")) becomes NaN, which then propagates into currentPage and the page value written into nextPage/previousPage.

You can make this more robust with something like:

-  const pageParam = searchParams.get("page")
-    ? Number(searchParams.get("page")) - pageOffset
-    : 0;
+  const rawPage = searchParams.get("page");
+  const parsedPage = rawPage ? Number(rawPage) : 0;
+  const safePage = Number.isNaN(parsedPage) ? 0 : parsedPage;
+  const pageParam = safePage - pageOffset;

This keeps currentPage and the generated links sane even if someone passes a bad page value.

🤖 Prompt for AI Agents
In shopify/loaders/orders/list.ts around lines 72 to 129, the loader builds
backward-pagination variables (before/last) that the OrdersByCustomer GraphQL
query does not declare/use, and it does not robustly handle non-numeric page
query params; update to either align the query with before/last or remove the
backward branch, and validate page parsing. Fix: extend the OrdersByCustomer
query to accept $before and $last and forward them into orders (ensuring you
never set both forward and backward args simultaneously) so previousPage links
work, and/or if you choose not to implement backward paging yet remove the
before/last variable construction and previousPage URL generation; additionally,
defensively parse the page param by coercing Number(searchParams.get("page")) to
a safe integer with a fallback (e.g., isNaN -> 0 or default page value) before
using it to compute currentPage and link pages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant