Cart & Wishlist

Last updated on 2026-08-30

The cart and wishlist use a hybrid storage strategy: guests get localStorage, authenticated users get database persistence, and items merge automatically when a guest logs in.

Hybrid Cart

How It Works

The CartProvider in context/cart-context.tsx detects auth state and delegates to the appropriate storage:

Guest user:
  → Read/write localStorage ("ember-guest-cart")
  → Cart items are CartItem[] in React state

Authenticated user:
  → Read/write Supabase cart_items table
  → Server-side persistence via Supabase queries

Guest logs in:
  → Guest cart merges into server cart
  → Quantities take the higher value
  → localStorage is cleared

Cart Context API

const {
  items,           // CartItem[] — current cart items
  count,           // number — total item count
  subtotal,        // number — total price
  addItem,         // (product, quantity?, variants?) => void
  removeItem,      // (itemId) => void
  updateQuantity,  // (itemId, quantity) => void
  clear,           // () => void
  loading,         // boolean — initial load state
} = useCart()

Guest Cart (localStorage)

Guest cart items are stored as:

interface GuestCartItem {
  productId: string
  product: Product          // Full product object for display
  quantity: number
  selectedVariants: { type: string; value: string }[]
}

Item IDs follow the pattern guest-{productId} to distinguish from database UUIDs.

Authenticated Cart (Supabase)

For logged-in users, cart operations use Supabase directly:

  • Add item: Check if product exists in cart. If yes, increment quantity. If no, insert new row.
  • Update quantity: Update the quantity column on the cart item row.
  • Remove item: Delete the row by ID.
  • Clear cart: Delete all rows for the user.

The cart_items table schema:

cart_items (
  id uuid PRIMARY KEY,
  user_id uuid REFERENCES auth.users,
  product_id uuid REFERENCES products,
  quantity integer,
  variant_selections jsonb  -- [{type: "size", value: "M"}, ...]
)

Cart Merge on Login

When a guest user logs in, the mergeGuestCartToServer function runs:

  1. Read guest cart from localStorage
  2. Fetch existing server cart for dedup
  3. For each guest item:
    • If product already in server cart: take the higher quantity
    • If product not in server cart: insert it
  4. Clear localStorage

This happens automatically in the useEffect that watches auth state transitions.

Hybrid Wishlist

The wishlist follows the same pattern as the cart.

Wishlist Context API

const {
  items,           // WishlistItem[] — wishlisted products
  count,           // number — total count
  isInWishlist,    // (productId) => boolean
  toggle,          // (product) => void — add or remove
  remove,          // (productId) => void
  loading,         // boolean
} = useWishlist()

Guest Wishlist (localStorage)

Stored under "ember-guest-wishlist" with product ID and full product data.

Authenticated Wishlist (Supabase)

The wishlists table:

wishlists (
  id uuid PRIMARY KEY,
  user_id uuid REFERENCES auth.users,
  product_id uuid REFERENCES products,
  created_at timestamptz
)
-- Unique constraint on (user_id, product_id)

Toggle checks for existing row: if found, deletes it; if not, inserts.

Wishlist Merge on Login

Same as cart merge: guest wishlist items are inserted (with dedup) into the server wishlist, then localStorage is cleared.

Recently Viewed

The RecentlyViewedProvider in context/recently-viewed-context.tsx is client-side only (localStorage). It tracks the last 12 products viewed and is not affected by auth state.

Using the Cart in Checkout

The checkout page reads from useCart() to build the Stripe Checkout Session:

const { items } = useCart()

// Maps cart items to the API request format
const response = await fetch("/api/checkout", {
  method: "POST",
  body: JSON.stringify({
    items: items.map((item) => ({
      product_id: item.product.id,
      name: item.product.name,
      price: item.product.salePrice ?? item.product.price,
      quantity: item.quantity,
    })),
    email: user?.email,
  }),
})

After successful payment, the Stripe webhook clears the authenticated user's cart from the database.