Skip to content

Complete migration to @spree/next package - #7

Merged
damianlegawiec merged 5 commits into
mainfrom
damianlegawiec/spree-next-migration
Feb 11, 2026
Merged

Complete migration to @spree/next package#7
damianlegawiec merged 5 commits into
mainfrom
damianlegawiec/spree-next-migration

Conversation

@damianlegawiec

@damianlegawiec damianlegawiec commented Feb 10, 2026

Copy link
Copy Markdown
Member

Summary

Migrate all server-side data layer from direct @spree/sdk usage to @spree/next, which provides Server Actions for authentication, cart, checkout, and customer operations with built-in cookie-based auth and token refresh.

Changes

  • Add @spree/next@0.1.1 as dependency and configure transpilePackages
  • Delete src/lib/spree.ts (client initialization now internal)
  • Delete src/lib/data/auth-request.ts (token refresh now internal)
  • Rewrite 12 data layer files to delegate to @spree/next Server Actions
  • Fix React 19 hook ordering and nested form issues in checkout components

Test plan

  • ✅ TypeScript compilation (npx tsc --noEmit)
  • ✅ Biome linting (npm run check)
  • ✅ Next.js build (npm run build)
  • Manual testing: Verify cart, checkout, authentication flows work end-to-end

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Contact Information added to checkout (email input); improved shipping address flow allowing selection of saved addresses or entering a new address.
  • Bug Fixes

    • Checkout hook execution order corrected for improved stability.
  • Refactor

    • Centralized API handling and streamlined cart/checkout/payment flows.
    • Simplified authentication and cookie handling for more consistent behavior.

Migrate all server-side data layer from direct @spree/sdk usage to @spree/next, which provides Server Actions for authentication, cart, checkout, and customer operations with built-in cookie-based auth and token refresh.

Changes:
- Add @spree/next@0.1.1 as dependency and configure transpilePackages
- Delete src/lib/spree.ts (client initialization now internal to @spree/next)
- Delete src/lib/data/auth-request.ts (token refresh now internal to @spree/next)
- Simplify src/lib/data/cookies.ts to only isAuthenticated() check
- Rewrite all data layer files to delegate to @spree/next Server Actions
- Fix React 19 hook ordering in CheckoutPage (use() before other hooks)
- Fix nested form issue in AddressStep component

Result: Reduced data layer from 867 to 252 lines while maintaining all functionality.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@damianlegawiec has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 13 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Centralizes data access by replacing local Spree client and auth utilities with @spree/next wrappers across many data modules, adjusts public function signatures (removing options/token handling), restructures AddressStep UI, and adds @spree/next to dependencies and Next.js transpilePackages.

Changes

Cohort / File(s) Summary
Configuration & Dependency
next.config.ts, package.json
Added @spree/next dependency and added transpilePackages: ["@spree/next","@spree/sdk"] to Next config.
Auth & Client Removal
src/lib/spree.ts, src/lib/data/auth-request.ts, src/lib/data/cookies.ts
Removed server Spree client and local auth utilities (getSpreeClient, withAuthRefresh, token/cookie helpers); isAuthenticated simplified to read cookie.
Global data layer → @spree/next
src/lib/data/* (addresses.ts, cart.ts, checkout.ts, countries.ts, credit-cards.ts, customer.ts, gift-cards.ts, orders.ts, products.ts, store.ts, taxonomies.ts)
Replaced direct Spree client and auth flows with imports from @spree/next (list*/_get*/addItem/updateItem/etc.), simplified error shapes, removed per-call options/token handling, and adjusted several function signatures (many now accept generic Record or locale/currency options).
Checkout page hook order
src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
Moved use(params) invocation earlier to satisfy hook ordering and expose orderId before other hooks.
AddressStep UI restructure
src/components/checkout/AddressStep.tsx
Reorganized markup into fragment: consolidated sign-in prompt, added Contact Information section (email input), added Shipping Address section (AddressSelector vs AddressForm conditional), moved Submit area and preserved edit-modal wiring.
Context adjustments
src/contexts/CartContext.tsx
Cart mutation handlers now use { success, cart, error } results from data layer; success updates state and triggers UI effects, failures are logged rather than throwing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰✨ I hopped through code to tidy the trail,
Swapped old tokens for one tidy rail.
@spree/next now hums where clients played,
Addresses and carts in a cleaner parade.
A carrot for refactors — hip-hop hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Complete migration to @spree/next package' directly and accurately summarizes the primary change: migrating the codebase from direct @spree/sdk usage to the new @spree/next package.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch damianlegawiec/spree-next-migration

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/checkout/AddressStep.tsx (1)

155-161: ⚠️ Potential issue | 🟠 Major

ship_address_id is never sent when a saved address is selected.

The onSubmit prop accepts ship_address_id?: string (line 24), but handleSubmit always sends the full ship_address object. When an authenticated user picks an existing saved address via AddressSelector, it would be more correct (and efficient) to pass ship_address_id instead. Currently, the server receives a copy of the address fields rather than a reference to the saved address, which may create a duplicate address or bypass address-book linkage on the backend.

Proposed fix

You'd need to track whether a saved address was selected (e.g., store selectedSavedAddressId) and branch in handleSubmit:

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
+   // If a saved address is selected, send its ID instead of duplicating the data
+   if (selectedSavedAddressId && selectedSavedAddressId !== "new") {
+     onSubmit({
+       email,
+       ship_address_id: selectedSavedAddressId,
+     });
+     return;
+   }
    onSubmit({
      email,
      ship_address: formDataToAddress(shipAddress),
    });
  };

This requires lifting or sharing the selectedAddressId state from AddressSelector, or adding a callback prop.

src/lib/data/gift-cards.ts (1)

5-19: ⚠️ Potential issue | 🟡 Minor

Add explicit return types to both exported functions.

The coding guidelines require explicit return types for all functions. Both getGiftCards and getGiftCard lack them, which violates the strict TypeScript requirement.

For getGiftCards, the return type should be Promise<{ data: GiftCard[] }> based on the SDK and usage in page.tsx (line 207-208). For getGiftCard, it should be Promise<GiftCard | null> to match the error handling (returning null on failure).

Define these types explicitly, or import concrete types from @spree/next if available:

-export async function getGiftCards() {
+export async function getGiftCards(): Promise<{ data: unknown[] }> {
   try {
     return await listGiftCards();
   } catch {
     return { data: [] };
   }
 }

-export async function getGiftCard(id: string) {
+export async function getGiftCard(id: string): Promise<unknown | null> {
   try {
     return await _getGiftCard(id);
   } catch {
     return null;
   }
 }

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type."

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 14: Remove the invalid dependency entry "@spree/next": "^0.1.1" from
package.json (or replace it with the correct published package name if you
intended a different Spree package such as "@spree/dashboard" or
"@spree/storefront-api-v2-sdk"); verify there are no imports of "@spree/next" in
the codebase and then run npm install to ensure the install succeeds after
removing or correcting the package name.

In `@src/lib/data/cart.ts`:
- Around line 25-38: The three mutation helpers addToCart, updateCartItem, and
removeCartItem are inconsistent with associateCartWithUser: they throw on errors
instead of returning a structured { success, error } result and also risk
surfacing a fetch error after a successful mutation; wrap each function
(addToCart, updateCartItem, removeCartItem) in a try/catch that calls the
underlying mutation (addItem/updateItem/removeItem) and then attempts _getCart —
on success return { success: true, cart }, on mutation failure return { success:
false, error }, and if the mutation succeeds but _getCart fails return {
success: true, cart: null, error: fetchError } (or include both mutation and
fetch error fields) so callers get a consistent structured result like
associateCartWithUser.

In `@src/lib/data/credit-cards.ts`:
- Around line 8-14: Add an explicit return type to getCreditCards to match the
shape returned by listCreditCards (or a custom interface that includes at least
the data array) so callers get proper typing; update the function signature of
getCreditCards to declare that return type, import the appropriate type from the
Spree SDK if available (or define a local interface), and ensure the error
fallback ({ data: [] }) conforms to that type so the implementation and
signature remain consistent with listCreditCards.
🧹 Nitpick comments (13)
src/lib/data/customer.ts (2)

11-29: Missing explicit return types on exported functions.

Per coding guidelines, functions should have explicit return types. These Server Actions are part of the public API surface and would benefit from declared return types for type safety and documentation.

♻️ Add explicit return types
-export async function getCustomer() {
+export async function getCustomer(): ReturnType<typeof _getCustomer> {
   return _getCustomer();
 }

-export async function login(email: string, password: string) {
+export async function login(email: string, password: string): ReturnType<typeof _login> {
   return _login(email, password);
 }

-export async function register(
+export async function register(
   email: string,
   password: string,
   passwordConfirmation: string,
-) {
+): ReturnType<typeof _register> {
   return _register(email, password, passwordConfirmation);
 }

-export async function logout() {
+export async function logout(): ReturnType<typeof _logout> {
   return _logout();
 }

Alternatively, import and use the concrete types from @spree/next if available.

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".


31-44: Inconsistent error handling across functions.

updateCustomer wraps the call in try/catch and returns a { success, customer | error } result object, while getCustomer, login, register, and logout let exceptions propagate. If this is intentional (e.g., the others rely on error boundaries or @spree/next returning error states), a brief comment would help future maintainers understand the distinction.

src/components/checkout/AddressStep.tsx (2)

133-153: useEffect for state fetching triggered by state change — consider moving to event handler.

This useEffect fetches states whenever shipAddress.country_iso changes. The coding guideline says to avoid useEffect for data fetching triggered by state changes, preferring event handlers instead. The fetch could be initiated directly inside updateShipAddress when field === "country_iso".

Sketch
  const updateShipAddress = (field: keyof AddressFormData, value: string) => {
    setShipAddress((prev) => {
      const updated = { ...prev, [field]: value };
      if (field === "country_iso") {
        updated.state_abbr = "";
        updated.state_name = "";
      }
      return updated;
    });
+   if (field === "country_iso") {
+     if (!value) {
+       setShipStates([]);
+       return;
+     }
+     startTransitionShip(() => {
+       fetchStates(value).then(setShipStates);
+     });
+   }
  };

Then remove the useEffect at lines 133-153. Note: you'd still need to handle the initial load for order.ship_address.country_iso — a one-time fetch on mount (or computing initial states server-side) would cover that.

As per coding guidelines: "Avoid using useEffect for data fetching triggered by state changes; use event handlers or Server Actions instead".


633-841: Significant duplication between AddressForm and AddressSelector's inline form.

The local AddressForm component (lines 633-841) renders nearly identical fields to the form embedded inside AddressSelector (see relevant snippet, lines ~160-380). Both render the same first name, last name, company, address lines, city, country, state, zip, and phone fields with the same markup. Consider having AddressSelector consume AddressForm internally to eliminate the duplication.

src/lib/data/countries.ts (1)

5-10: Add explicit return types to exported functions.

getCountries() and getCountry() should declare their return types explicitly. With strict: true in tsconfig, this aligns with the project's TypeScript guidelines and guards against unintended type changes from @spree/next updates. See src/lib/data/cookies.ts for the pattern in use (e.g., Promise<boolean>).

export async function getCountries(options?: {
  locale?: string;
  currency?: string;
}): Promise</* return type from `@spree/next` */> {
  return listCountries(options);
}

export async function getCountry(
  iso: string,
  options?: { locale?: string; currency?: string },
): Promise</* return type from `@spree/next` */> {
  return _getCountry(iso, options);
}
src/lib/data/checkout.ts (2)

15-21: Consider adding explicit return types to exported Server Action functions.

Per coding guidelines, functions should have explicit return types. The inferred types work but explicit annotations improve discoverability and catch accidental return shape changes.

For example:

export async function getCheckoutOrder(orderId: string): Promise<StoreOrder | null> {

This applies to all 8 exported functions in this file.

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type"


67-84: selectShippingRate doesn't return the updated order, unlike other mutation functions.

Other mutation wrappers (applyCouponCode, removeCouponCode, advanceCheckout, completeOrder) return the updated order in the success response, but selectShippingRate only returns { success: true }. The caller in page.tsx (Line 289) works around this by re-fetching via getCheckoutOrder, but returning the order here would be more consistent and could eliminate that extra network call.

This is minor since the current approach works correctly.

src/lib/data/store.ts (1)

5-10: Missing error handling — inconsistent with other data layer wrappers.

Other data layer functions in this PR (e.g., getCheckoutOrder, getOrders, getOrder) wrap calls in try/catch and return safe fallbacks (null, { data: [] }). getStore lets exceptions propagate, which could cause unhandled errors in consuming components/pages.

Consider adding a try/catch with a sensible fallback, or at minimum document that callers must handle the thrown error.

♻️ Proposed fix
 export async function getStore(options?: {
   locale?: string;
   currency?: string;
 }) {
-  return _getStore(options);
+  try {
+    return await _getStore(options);
+  } catch {
+    return null;
+  }
 }
src/lib/data/orders.ts (1)

5-11: Add explicit type for params to align with SDK type safety guidelines.

The Record<string, unknown> parameter type is too loose. Per the coding guidelines, Spree SDK types should be used for type safety. If @spree/next's listOrders accepts specific parameters (like include, filter, per_page), use a typed interface or the SDK's own param type instead of Record<string, unknown>. Additionally, define an explicit return type for the function.

src/lib/data/taxonomies.ts (2)

10-38: Missing explicit return types on all exported functions.

The coding guidelines require explicit return types for functions. All four functions here rely on inferred return types, which makes the public API contract implicit and fragile — any change in @spree/next internals silently propagates to consumers.

Additionally, Record<string, unknown> is a significant type-safety regression from the previously structured parameter interfaces. Consider re-exporting or defining typed param interfaces that match the @spree/next expected shapes, so callers get autocomplete and compile-time validation.

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type" and "Import and use Spree SDK types (StoreProduct, StoreVariant, StoreOrder, StoreLineItem, PaginatedResponse) for type safety".


10-38: Extract the duplicated options type.

The { locale?: string; currency?: string } type is repeated verbatim four times in this file (and likely across other data-layer files like products.ts). Extract it into a shared type alias.

♻️ Suggested refactor
+type SpreeRequestOptions = { locale?: string; currency?: string };
+
 export async function getTaxonomies(
   params?: Record<string, unknown>,
-  options?: { locale?: string; currency?: string },
+  options?: SpreeRequestOptions,
 ) {

Apply similarly to all four functions (and consider placing the type in a shared module if used across files).

src/lib/data/products.ts (1)

10-38: Same type-safety concerns as taxonomies.ts.

Missing explicit return types and Record<string, unknown> params apply here as well. Notably, getProduct on Line 19 retains a more specific { includes?: string } param type — consider applying similar specificity to the other three functions for consistency and caller guidance.

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

src/lib/data/addresses.ts (1)

30-57: Add explicit discriminated-union return types for mutation functions.

createAddress and updateAddress return either { success: true, address } or { success: false, error }, but without an explicit return type annotation, consumers lose the benefit of TypeScript narrowing on success. This matters more here than on the read-only wrappers because callers will branch on the result.

♻️ Example for createAddress
+type MutationResult<T = unknown> =
+  | { success: true; address: T }
+  | { success: false; error: string };
+
-export async function createAddress(address: AddressParams) {
+export async function createAddress(address: AddressParams): Promise<MutationResult> {

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

Comment thread package.json
Comment thread src/lib/data/cart.ts
Comment thread src/lib/data/credit-cards.ts Outdated
…ditCards

Wrap addToCart, updateCartItem, removeCartItem in try/catch with structured
{ success, cart, error } returns for consistency with associateCartWithUser.
Update CartContext to handle the new structured return type. Add explicit
return type annotation to getCreditCards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/lib/data/cart.ts`:
- Around line 13-23: clearCart currently re-throws errors instead of matching
the structured { success, error } response used by other mutation helpers;
change clearCart to call _clearCart() inside a try/catch, return { success: true
} on success, and on failure catch the error and return { success: false, error:
String(err) } (or include err.message) so its signature and error-handling
mirror addToCart, updateCartItem, removeCartItem, and associateCartWithUser
while still delegating work to _clearCart.
🧹 Nitpick comments (4)
src/lib/data/credit-cards.ts (1)

17-28: Add an explicit return type to deleteCreditCard.

Per coding guidelines, all functions should have explicit return types. The two return branches have different shapes ({ success: true } vs { success: false, error: string }), so a discriminated union would also improve caller ergonomics.

Suggested fix
-export async function deleteCreditCard(id: string) {
+export async function deleteCreditCard(
+  id: string
+): Promise<{ success: true } | { success: false; error: string }> {

As per coding guidelines, **/*.ts{,x}: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

src/lib/data/cart.ts (2)

13-15: Missing explicit return type annotations on all exported functions.

The coding guideline requires explicit return types for functions. Every public function here relies on inference. Adding return types improves documentation and prevents accidental signature drift — especially important for Server Actions consumed across client boundaries.

For example:

-export async function addToCart(variantId: string, quantity: number) {
+export async function addToCart(variantId: string, quantity: number): Promise<
+  { success: true; cart: StoreOrder } | { success: false; error: string }
+> {

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type."

Also applies to: 17-19, 25-37, 39-51, 53-65, 67-78


25-37: Mutation-then-fetch can return a false failure when only the re-fetch fails.

If addItem/updateItem/removeItem succeeds but the subsequent _getCart() throws, the catch returns { success: false } even though the server-side mutation was applied. The caller (CartContext) won't update its state, leaving the UI stale until the next refresh.

A lightweight mitigation is to catch the re-fetch separately:

await addItem(variantId, quantity);
try {
  const cart = await _getCart();
  return { success: true as const, cart };
} catch {
  return { success: true as const, cart: null };
}

This way callers know the mutation succeeded and can trigger a refresh. Not blocking, but worth considering.

Also applies to: 39-51, 53-65

src/contexts/CartContext.tsx (1)

57-74: Consider useOptimistic for instant cart UI updates.

All three mutation handlers (addItem, updateItem, removeItem) set updating=true, wait for the full server round-trip, then update state. This means the user sees a loading state until the Server Action completes and returns the refreshed cart.

React 19's useOptimistic hook would let you apply the expected state change immediately (e.g., update quantity or remove item from the local cart) and reconcile when the server responds, providing a snappier UX. This is a good-to-have improvement and can be deferred.

As per coding guidelines: "Use useOptimistic hook for instant UI updates when performing async operations like cart updates."

Also applies to: 76-92, 94-110

Comment thread src/lib/data/cart.ts
damianlegawiec and others added 3 commits February 10, 2026 22:28
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set up Vitest with React Testing Library and jsdom for unit/integration
testing. Add 58 tests across 5 test files covering the data layer (cart,
checkout, customer), CartContext, and ProductCard component. Add GitHub
Actions CI workflow with parallel lint, typecheck, and test jobs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use node: protocol for path import, fix import ordering, and auto-format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants