Skip to content

Add wishlist support - #184

Open
Abdul-Haseeb-Kamboh wants to merge 9 commits into
spree:mainfrom
Abdul-Haseeb-Kamboh:feat/wishlist-support
Open

Add wishlist support#184
Abdul-Haseeb-Kamboh wants to merge 9 commits into
spree:mainfrom
Abdul-Haseeb-Kamboh:feat/wishlist-support

Conversation

@Abdul-Haseeb-Kamboh

@Abdul-Haseeb-Kamboh Abdul-Haseeb-Kamboh commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Add wishlist functionality to the storefront.

Problem

The Spree API and SDK already support wishlists, but the storefront did not provide a user interface for managing wishlist items.

Solution

Implemented wishlist support including:

  • Wishlist state management
  • Add/remove wishlist actions
  • Wishlist page
  • Empty wishlist state
  • Integration with existing authentication flow

Testing

Tested locally and verified:

  • Products can be added to wishlist
  • Products can be removed from wishlist
  • Wishlist items are displayed correctly
  • Empty wishlist state works as expected

Fixes #136

Summary by CodeRabbit

  • New Features
    • Added wishlist support across the storefront (product cards, product detail pages) with add/remove, badge count, and signed-in toggle behavior.
    • Added an account wishlist page with refresh, add-to-cart, remove, empty/error states, and guest sign-in prompts that return with a redirect.
    • Added a top-level /wishlist route that redirects to the localized account wishlist.
  • UI/Localization
    • Extended i18n copy for wishlist across EN/DE/ES/FR/PL, including header/footer/account labels and heart/button text.
  • Tests
    • Added Playwright E2E coverage for guest and authenticated wishlist flows.
    • Added/expanded Vitest coverage for wishlist button, context, and data operations.

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@Abdul-Haseeb-Kamboh is attempting to deploy a commit to the Spree Commerce Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds wishlist persistence, client state, storefront controls, account pages, navigation links, localized messages, unit tests, and end-to-end coverage for adding, viewing, and removing wishlist items.

Changes

Wishlist feature

Layer / File(s) Summary
Wishlist data and state
src/lib/data/wishlist.ts, src/contexts/WishlistContext.tsx, src/lib/data/__tests__/wishlist.test.ts, src/contexts/__tests__/WishlistContext.test.tsx
Adds authenticated wishlist retrieval and mutations, enrichment, cache revalidation, context state, selectors, error handling, and tests.
Wishlist controls and page
src/components/wishlist/*, src/components/products/ProductCard.tsx, src/app/.../products/[slug]/ProductDetails.tsx
Adds wishlist toggling, product-card and product-detail controls, wishlist rendering, cart actions, removal, and loading, sign-in, and empty states with component tests.
Routes, provider wiring, and navigation
src/app/.../layout.tsx, src/app/.../account/*, src/app/.../wishlist/page.tsx, src/components/layout/*, e2e/wishlist.spec.ts
Wires the provider, adds account and redirect routes, exposes navigation links and badges, and tests guest and authenticated flows.
Wishlist translations
messages/{de,en,es,fr,pl}.json
Adds wishlist labels and messages across five locales.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Shopper
  participant ProductDetails
  participant WishlistButton
  participant WishlistContext
  participant wishlistData
  Shopper->>ProductDetails: Select product variant
  ProductDetails->>WishlistButton: Render selected variantId
  Shopper->>WishlistButton: Click add to wishlist
  WishlistButton->>WishlistContext: addItem(variantId, 1)
  WishlistContext->>wishlistData: addWishlistItem()
  wishlistData-->>WishlistContext: Updated wishlist
  WishlistContext-->>WishlistButton: Updated membership and count
  WishlistButton-->>Shopper: Show active wishlist control
Loading

Poem

A bunny taps the heart so bright,
Saves a snack for later tonight.
Pages bloom and counters glow,
Wishes hop from left to right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding wishlist support.
Linked Issues check ✅ Passed The PR adds wishlist UI, state, actions, and pages, matching the issue's request to add storefront wishlist support.
Out of Scope Changes check ✅ Passed The changes are centered on wishlist UI, data, localization, and tests, with no clearly unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 5

🧹 Nitpick comments (6)
src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx (2)

1-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the new dynamic routes’ rendering contracts explicit.

As per coding guidelines:

  • src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx#L1-L5: add generateMetadata and explicitly choose dynamic = "force-dynamic" or generateStaticParams.
  • src/app/[country]/[locale]/(storefront)/wishlist/page.tsx#L3-L14: explicitly choose dynamic = "force-dynamic" or generateStaticParams for the redirect route.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[country]/[locale]/(storefront)/account/wishlist/page.tsx around
lines 1 - 5, Update
src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx lines 1-5 to
add generateMetadata and explicitly configure either dynamic = "force-dynamic"
or generateStaticParams. Update
src/app/[country]/[locale]/(storefront)/wishlist/page.tsx lines 3-14 to
explicitly choose dynamic = "force-dynamic" or generateStaticParams for the
redirect route.

Source: Coding guidelines


3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add explicit return types to the new functions.

As per coding guidelines, these new functions should not rely on inferred return types.

  • src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx#L3-L5: annotate AccountWishlistPage.
  • src/app/[country]/[locale]/(storefront)/wishlist/page.tsx#L10-L15: annotate the async redirect page as Promise<never>.
  • src/components/layout/WishlistNavButton.tsx#L11-L29: annotate the component return type.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[country]/[locale]/(storefront)/account/wishlist/page.tsx around
lines 3 - 5, Annotate the return types of the three new functions: set
AccountWishlistPage in
src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx to its
explicit component return type, set the async redirect page in
src/app/[country]/[locale]/(storefront)/wishlist/page.tsx to
Promise&lt;never&gt;, and annotate the component return type in
src/components/layout/WishlistNavButton.tsx.

Source: Coding guidelines

src/components/wishlist/__tests__/WishlistButton.test.tsx (1)

59-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the authenticated remove click path.

This test only asserts the "Remove from wishlist" label renders; it never clicks the button to verify removeItemByVariant is actually invoked, unlike the parallel add-path test at lines 78-84.

✅ Suggested addition
   it("shows remove state for variants already in wishlist", () => {
     mockHasVariant.mockReturnValue(true);
     render(<WishlistButton variantId="var_1" />);
     expect(
       screen.getByRole("button", { name: "Remove from wishlist" }),
     ).toBeInTheDocument();
   });
+
+  it("removes variant when authenticated and already in wishlist", async () => {
+    mockHasVariant.mockReturnValue(true);
+    render(<WishlistButton variantId="var_1" />);
+
+    fireEvent.click(screen.getByRole("button", { name: "Remove from wishlist" }));
+
+    expect(mockRemoveItemByVariant).toHaveBeenCalledWith("var_1");
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/wishlist/__tests__/WishlistButton.test.tsx` around lines 59 -
65, Add click coverage to the existing “shows remove state for variants already
in wishlist” test by clicking the rendered Remove from wishlist button and
asserting the authenticated remove handler, removeItemByVariant, is called with
the expected variant identifier, matching the parallel add-path test.
src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx (1)

99-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate variant-id fallback logic.

selectedVariantId (114-117) recomputes the exact same fallback chain (selectedVariant?.id || product.default_variant?.id || product.default_variant_id) already inlined in handleAddToCart (100-103). Hoist to a single derived value and reuse it in both places to avoid the two copies drifting apart.

♻️ Suggested change
+  const selectedVariantId =
+    selectedVariant?.id ||
+    product.default_variant?.id ||
+    product.default_variant_id;
+
   const handleAddToCart = async () => {
-    const variantId =
-      selectedVariant?.id ||
-      product.default_variant?.id ||
-      product.default_variant_id;
+    const variantId = selectedVariantId;
     if (!variantId) {
       throw new Error("No variant selected");
     }
     ...
   };
-
-  const selectedVariantId =
-    selectedVariant?.id ||
-    product.default_variant?.id ||
-    product.default_variant_id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx
around lines 99 - 117, Hoist the shared variant ID fallback chain into a single
derived value near handleAddToCart, then reuse that value for the addItem call
and selectedVariantId-dependent logic. Remove the duplicated inline computation
while preserving the existing no-variant error behavior.
src/contexts/WishlistContext.tsx (1)

101-103: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

router.refresh() on every wishlist mutation may be unnecessary overhead.

mutateWishlist already sets local wishlist state from the mutation response, and all wishlist consumers (WishlistButton, ProductCard, WishlistPageContent) read from this context, not from server-rendered data. Calling router.refresh() after every add/remove forces a full RSC data refetch for the current route on every toggle, which is likely redundant unless some server component elsewhere depends on wishlist state.

♻️ Suggested change
         setWishlist(result.wishlist ?? null);
-        router.refresh();
         return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/WishlistContext.tsx` around lines 101 - 103, Remove the
router.refresh() call from mutateWishlist after updating local wishlist state
with result.wishlist, while preserving the existing state update and successful
return behavior. Remove any now-unused router dependency associated with this
call.
src/lib/data/wishlist.ts (1)

73-73: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Remove the no-op wishlist invalidation updateTag("wishlist") doesn’t invalidate anything today because the wishlist reads in this file aren’t cached or tagged. Either add cacheTag("wishlist") to the read path or drop these calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/data/wishlist.ts` at line 73, Remove the no-op updateTag("wishlist")
calls from the wishlist mutation flow, since the reads in this file are not
cached or tagged. Do not add cache tagging unless you also update the
corresponding read path to use cacheTag("wishlist").
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/wishlist.spec.ts`:
- Around line 12-14: Add explicit Promise<void> return types to the async test
callbacks in the wishlist.spec.ts tests, including “guest is redirected to
account when clicking wishlist on PDP” and the other flagged tests at lines
32–34 and 70, while preserving their existing behavior.

In `@src/app/`[country]/[locale]/(storefront)/account/wishlist/page.tsx:
- Around line 3-5: Update the account route protection around
AccountWishlistPage so unauthenticated users can reach WishlistPageContent and
its guest sign-in state, preserving the wishlist path and redirect query for
sign-in; exempt this wishlist route from the generic account-layout redirect
rather than sending guests to /account.

In `@src/components/layout/WishlistNavButton.tsx`:
- Around line 18-24: Update the Link aria-label in WishlistNavButton to expose
itemCount to assistive technology using a localized label that includes the
count, or associate a localized screen-reader-only count with the link while
preserving the visible badge.

In `@src/components/wishlist/WishlistPageContent.tsx`:
- Around line 174-202: Remove the unrelated updating flag from the disabled
condition of the Add to Cart Button in WishlistPageContent. Keep the button
disabled only while another variant is being added or when
item.variant.purchasable is false, preserving the existing addingVariantId
behavior.

In `@src/contexts/__tests__/WishlistContext.test.tsx`:
- Around line 38-53: The wishlist fixtures are incorrectly cast as never,
causing downstream object spreads to fail type checking. In
src/contexts/__tests__/WishlistContext.test.tsx at lines 38-53, cast
wishlistFixture and updatedWishlist around line 81 as unknown as Wishlist; in
src/lib/data/__tests__/wishlist.test.ts at lines 34-67, cast wishlistFixture as
unknown as Wishlist, preserving the existing fixture data.

---

Nitpick comments:
In `@src/app/`[country]/[locale]/(storefront)/account/wishlist/page.tsx:
- Around line 1-5: Update
src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx lines 1-5 to
add generateMetadata and explicitly configure either dynamic = "force-dynamic"
or generateStaticParams. Update
src/app/[country]/[locale]/(storefront)/wishlist/page.tsx lines 3-14 to
explicitly choose dynamic = "force-dynamic" or generateStaticParams for the
redirect route.
- Around line 3-5: Annotate the return types of the three new functions: set
AccountWishlistPage in
src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx to its
explicit component return type, set the async redirect page in
src/app/[country]/[locale]/(storefront)/wishlist/page.tsx to
Promise&lt;never&gt;, and annotate the component return type in
src/components/layout/WishlistNavButton.tsx.

In `@src/app/`[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx:
- Around line 99-117: Hoist the shared variant ID fallback chain into a single
derived value near handleAddToCart, then reuse that value for the addItem call
and selectedVariantId-dependent logic. Remove the duplicated inline computation
while preserving the existing no-variant error behavior.

In `@src/components/wishlist/__tests__/WishlistButton.test.tsx`:
- Around line 59-65: Add click coverage to the existing “shows remove state for
variants already in wishlist” test by clicking the rendered Remove from wishlist
button and asserting the authenticated remove handler, removeItemByVariant, is
called with the expected variant identifier, matching the parallel add-path
test.

In `@src/contexts/WishlistContext.tsx`:
- Around line 101-103: Remove the router.refresh() call from mutateWishlist
after updating local wishlist state with result.wishlist, while preserving the
existing state update and successful return behavior. Remove any now-unused
router dependency associated with this call.

In `@src/lib/data/wishlist.ts`:
- Line 73: Remove the no-op updateTag("wishlist") calls from the wishlist
mutation flow, since the reads in this file are not cached or tagged. Do not add
cache tagging unless you also update the corresponding read path to use
cacheTag("wishlist").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 775ce688-6fea-4c57-8ca9-c1d8093f5bec

📥 Commits

Reviewing files that changed from the base of the PR and between 1b54481 and d3932fe.

📒 Files selected for processing (26)
  • e2e/wishlist.spec.ts
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/pl.json
  • src/app/[country]/[locale]/(storefront)/account/layout.tsx
  • src/app/[country]/[locale]/(storefront)/account/page.tsx
  • src/app/[country]/[locale]/(storefront)/account/wishlist/page.tsx
  • src/app/[country]/[locale]/(storefront)/products/[slug]/ProductDetails.tsx
  • src/app/[country]/[locale]/(storefront)/wishlist/page.tsx
  • src/app/[country]/[locale]/layout.tsx
  • src/components/layout/Footer.tsx
  • src/components/layout/Header.tsx
  • src/components/layout/MobileMenu.tsx
  • src/components/layout/WishlistNavButton.tsx
  • src/components/products/ProductCard.tsx
  • src/components/products/__tests__/ProductCard.test.tsx
  • src/components/wishlist/WishlistButton.tsx
  • src/components/wishlist/WishlistPageContent.tsx
  • src/components/wishlist/__tests__/WishlistButton.test.tsx
  • src/contexts/WishlistContext.tsx
  • src/contexts/__tests__/WishlistContext.test.tsx
  • src/lib/data/__tests__/wishlist.test.ts
  • src/lib/data/index.ts
  • src/lib/data/wishlist.ts

Comment thread e2e/wishlist.spec.ts Outdated
Comment on lines +12 to +14
test("guest is redirected to account when clicking wishlist on PDP", async ({
page,
}) => {

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add explicit return types to the async test functions.

As per coding guidelines, “Always define explicit return types for functions.”

Proposed fix
-test("guest is redirected to account when clicking wishlist on PDP", async ({
+test("guest is redirected to account when clicking wishlist on PDP", async ({
   page,
-}) => {
+}): Promise<void> => {
@@
-test("authenticated user can add and remove a wishlist item", async ({
+test("authenticated user can add and remove a wishlist item", async ({
   page,
-}) => {
+}): Promise<void> => {
@@
-async function registerUser(page: Page, email: string, password: string) {
+async function registerUser(
+  page: Page,
+  email: string,
+  password: string,
+): Promise<void> {

Also applies to: 32-34, 70-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/wishlist.spec.ts` around lines 12 - 14, Add explicit Promise<void> return
types to the async test callbacks in the wishlist.spec.ts tests, including
“guest is redirected to account when clicking wishlist on PDP” and the other
flagged tests at lines 32–34 and 70, while preserving their existing behavior.

Source: Coding guidelines

Comment on lines +3 to +5
export default function AccountWishlistPage() {
return <WishlistPageContent />;
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the guest wishlist flow.

This route is nested under the protected account layout, which redirects unauthenticated /account/* paths to /account. As a result, WishlistPageContent’s guest sign-in state and its redirect query are never reached; guests clicking /wishlist, the header link, or footer link land on the generic account page and lose their destination. Exempt this route from the layout redirect or preserve the wishlist URL when redirecting to sign-in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[country]/[locale]/(storefront)/account/wishlist/page.tsx around
lines 3 - 5, Update the account route protection around AccountWishlistPage so
unauthenticated users can reach WishlistPageContent and its guest sign-in state,
preserving the wishlist path and redirect query for sign-in; exempt this
wishlist route from the generic account-layout redirect rather than sending
guests to /account.

Comment thread src/components/layout/WishlistNavButton.tsx
Comment thread src/components/wishlist/WishlistPageContent.tsx
Comment thread src/contexts/__tests__/WishlistContext.test.tsx Outdated
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
storefront Ready Ready Preview, Comment Jul 25, 2026 5:50pm

Request Review

Comment on lines +106 to +125
const imageUrl =
variant.thumbnail_url ||
variant.option_values?.find((value) => Boolean(value.image_url))
?.image_url ||
variant.images?.[0]?.styles?.product ||
variant.images?.[0]?.styles?.large ||
variant.images?.[0]?.styles?.small ||
variant.images?.[0]?.url ||
variant.images?.[0]?.original_url ||
wishlistItem.thumbnail_url ||
itemProduct?.thumbnail_url ||
itemProductAttributes?.thumbnail_url ||
itemProduct?.images?.[0]?.styles?.product ||
itemProduct?.images?.[0]?.styles?.large ||
itemProduct?.images?.[0]?.styles?.small ||
itemProduct?.images?.[0]?.url ||
itemProduct?.images?.[0]?.original_url ||
product?.thumbnail_url ||
productAttributes?.thumbnail_url ||
null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Abdul-Haseeb-Kamboh

  1. There's no images property, only media
  2. Also you should just ust primary_media
  3. Also you should just check variant.thumbnail_url || itemProduct.thumbnail_url no need for such extended finders

Comment thread src/lib/data/wishlist.ts Outdated
import { getClient, isAuthError, withAuthRefresh } from "@/lib/spree";
import { actionResult } from "./utils";

type WishlistItemWithVariantProductId = NonNullable<

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment thread src/lib/data/wishlist.ts Outdated
thumbnail_url?: string | null;
};

type ProductWithPrimaryMedia = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Abdul-Haseeb-Kamboh same here, also you don't need to fetch the image from primary_media - thumbnail_url is just fine

@damianlegawiec

Copy link
Copy Markdown
Member

Lets wait for Spree 5.6.1, including this improvement - spree/spree@f62ec82

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.

Support wishlists

2 participants