freshyo/docs/user-ui-cart-management.md
2026-08-16 15:45:05 +05:30

13 KiB
Raw Blame History

Cart Management in apps/user-ui — Detailed Documentation

How the customer cart works end-to-end: local storage, React Query hooks, the two cart types (regular + flash), pricing, slots, coupons, checkout handoff, and all edge cases.


1. Architecture Overview

The cart is 100% client-side / local-first. There is no server cart — items live in local storage (SecureStore on native, localStorage on web via StorageServiceCasual), and are resolved against the cached product catalog + slot/availability data on the device.

AddToCartDialog / ProductCard / FloatingCartBar
        │  useCartStore.setAddedToCartProduct()  (trigger dialog)
        ▼
useAddToCart / useUpdateCartItem / useRemoveFromCart   (cart-query-hooks.tsx)
        │  mutations write to local storage
        ▼
StorageServiceCasual (key: "cart_items" | "flash_cart_items")
        ▲
useGetCart()  (query reads local storage + merges product/availability data)
        │
cart-page.tsx / floating-cart-bar.tsx / checkout-page.tsx

Key files

File Role
hooks/cart-query-hooks.tsx The core: local-storage CRUD + React Query hooks (useGetCart, useAddToCart, useUpdateCartItem, useRemoveFromCart, clearLocalCart).
src/store/cartStore.ts Zustand: addedToCartProduct — which product opened the AddToCart dialog.
src/store/flashCartStore.ts Zustand: addedFlashProduct — which product was routed to the flash flow.
src/store/centralProductStore.ts Zustand: cached products + productsById map (from useAllProducts).
src/store/centralSlotStore.ts Zustand: slots + productSlotsMap (per-product availability/out-of-stock/flash).
components/cart-page.tsx The full cart screen (regular + flash via prop).
components/floating-cart-bar.tsx Collapsible cart summary bar (count, total, free-delivery progress).
src/components/AddToCartDialog.tsx Slot/quantity/flash chooser when adding/updating an item.
components/checkout-page.tsx Reads cart via URL params (slots=, coupons=, deliveryPrice=) — the cart is passed by reference, not re-read.

2. Data Model

Local storage item (LocalCartItem)

{
  id: number,       // local incrementing id (max existing id + 1)
  skuId: number,    // the product/SKU id
  quantity: number,
  slotId: number,   // delivery slot id; 0 = flash/unspecified
  addedAt: string,  // ISO timestamp
}

Stored under cart_items (regular) or flash_cart_items (flash) via StorageServiceCasual (SecureStore native / localStorage web).

Computed cart (CartData)

{
  items: CartItem[],
  totalItems: number,      // item count (not quantity)
  totalAmount: number,     // Σ(price × quantity)
}

CartItem = local item + subtotal (computed from productBasic.price).

Product resolution

useGetCart builds items by looking up each skuId in productsById (central product store) and requiring productSlotsMap[skuId] to exist — if either is missing, the item is silently dropped from the cart view (not deleted from storage).


3. Flow: Add to Cart

  1. Trigger: ProductCard / home / store / search call useCartStore.setAddedToCartProduct({ productId, product }) → the AddToCartDialog opens (regular) OR useFlashCartStore.setAddedFlashProduct (flash).
  2. Dialog (AddToCartDialog.tsx):
    • Lists available slots for the product (from useSlots()productSlotIdsMap, filtered to future freezeTime).
    • Shows "1 hr Delivery" option if productSlotsMap[id].isFlashAvailable AND flash is enabled.
    • Quantity via Quantifier (step 1 — not the product's incrementStep!).
    • Pre-fills from existing cart item: if the product is already in the cart, it loads the existing quantity + slotId.
    • "Add to Cart" / "Update Item" (if already in cart) / "Remove" (if updating).
  3. Mutation (useAddToCart / useUpdateCartItem):
    • addToLocalCart: if skuId exists → merge quantity (add to existing) + optionally update slotId; else push new item.
    • Requires slotId (throws if null).
    • updateLocalCartItem: set quantity on the item (by local id).
    • removeFromLocalCart: filter out by local id.
  4. Flash special-case: choosing "1 hr Delivery" sets useFlashCartStore.setAddedFlashProduct(...) and navigates to the flash products page — it does NOT add to the regular cart via the dialog; the flash cart is a separate local list.

4. Flow: Cart Page

cart-page.tsx (isFlashDelivery prop selects cartType):

  • Loading: blocks on useGetCart loading + (regular only) getCartSlots loading.
  • Per-item:
    • Image, name, unit, Quantifier (step = product incrementStep).
    • Quantity to 0 → confirmation alert → remove.
    • Slot selection (regular only): BottomDropdown from trpc.user.cart.getCartSlots({ productIds }), filtered to future slots. Flash forces slotId = 0.
    • Price: regular uses product.price; flash uses product.flashPrice ?? product.price.
    • Availability logic per item:
      • productSlotInfo.isOutOfStock → unavailable "Out of Stock".
      • Flash: not flash-eligible → "Not available for flash delivery. Please remove".
      • Regular: no slots → "No delivery slots available".
  • Auto-slot effect: on slotsData/cartItems change, auto-selects each item's slot (keeps existing if still valid, else first upcoming; flash = 0).
  • Coupons: trpc.user.coupon.getEligible → eligible list; min-order + usage-limit filtering client-side; discount = percent (capped at maxValue) or flat (capped at total); applied coupon id passed to checkout via URL.
  • Bill: Item Total → Product Discount → Coupon Applied → Delivery Fee (0 if finalTotal >= threshold) → To Pay. Free-delivery nudge + "You saved ₹X" banner.
  • Checkout handoff: builds slots=<slotId>:<itemId,itemId>;... + coupons=<id> + deliveryPrice=<charge> URL params → navigates to /home/checkout or /flash-delivery/checkout.
  • Guard: if no available items → alert (all OOS / none flash-eligible / missing slots) and blocks checkout.

5. Flow: Floating Cart Bar

  • Reads useGetCart for the active cart type; shows count + totalCartValue + free-delivery progress (regular vs flash thresholds).
  • Expanded dialog: list with MiniQuantifier (0 = remove), per-item slot dropdown (regular), subtotal, "Go to cart" → cart page.
  • Slot-reassignment effect (runs once on mount): for regular items whose slotId is no longer in productSlotsMap[skuId].slots, it removes + re-adds the item with the nearest slot (removeFromCart.mutate + addToCartHook.addToCart). (Note: addToCart(item.skuId, item.quantity, nearestSlotId) — passing quantity as the quantity arg.)

6. Checkout integration

  • Checkout does not re-read local storage directly — it receives the cart items by URL params (slots=, coupons=, deliveryPrice=) from the cart page.
  • This means the checkout's item set/quantities/slots are frozen at navigation time; if the cart changes after, checkout uses the snapshot.

7. Edge Cases & Observations

Data / storage

  1. Missing product or slot data → item dropped from view (useGetCart returns null for items lacking productBasic or productAvailability). The item stays in storage but is invisible — potential "phantom cart" if product is later removed from catalog.
  2. Duplicate add merges quantity (same skuIdquantity +=). No separate line items.
  3. slotId: 0 sentinel for flash — 0 is not a real slot; code must treat 0 as "flash/unspecified".
  4. Local id generation = max(existing ids) + 1. Deleting the highest-id item then adding reuses ids (fine) — but concurrent adds (two rapid taps) could race since id is computed from the read-then-write sequence (no lock).
  5. Storage failures (StorageServiceCasual.setItem error) are caught by the service and return false — the mutation still resolves, but the write may not have persisted (silent data loss risk).

Slot / availability

  1. Auto-slot reassignment: if a cart item's saved slot becomes invalid, the cart page picks the first upcoming slot silently; the floating bar instead does remove+re-add. Two different behaviors for the same situation.
  2. Slot changes in cart page are local-only (selectedSlots state) — they're NOT written back to LocalCartItem.slotId until checkout builds the URL. (The useEffect reads cartData slotId but the dropdown only updates selectedSlots.) → If the user leaves the cart without checking out, the new slot choice is lost.
  3. AddToCartDialog requires a slot — if a product has no future slots, the dialog can't add it (button disabled). Good, but the flash path can bypass via "1 hr Delivery".
  4. Flash eligibility is from productSlotsMap[id].isFlashAvailable — but the flash cart page also checks flashEligibleProductIds (derived from products + productSlotsMap). Two sources that should agree.
  5. Out-of-stock mid-cart: if a product becomes OOS after adding, cart page shows "Out of Stock" and excludes it from totals/checkout, but does NOT auto-remove it.

Quantity / pricing

  1. Dialog step = 1 vs cart page step = product.incrementStep — inconsistent stepping between the two add/update UIs. (A product with 500g increment could be set to any integer in the dialog.)
  2. baseTotalPrice (coupon eligibility) uses product.price (not flash price) even in flash cart — coupon min-order check may be wrong for flash.
  3. totalPrice filters OOS items — good — but the Quantifier still shows them; quantity edits on OOS items are allowed.
  4. Flash pricing: flashPrice ?? price — if flashPrice is 0 or null, falls back to base price; if flashPrice is a valid string, used. (Floating bar + cart page consistent.)
  5. Discount cap for flat coupons: Math.min(flatDiscount, maxValue || totalPrice) — reasonable; percent capped at maxValue || Infinity.

Coupons

  1. Usage-limit coupons are filtered out entirely from the dropdown (.filter(coupon => coupon.ineligibilityReason !== "Usage limit exceeded")) — a used-up coupon disappears rather than showing "used up".
  2. Min-order coupons show disabled with reason — good.
  3. Selected coupon is only applied to available items' total — matches backend intent (coupon applies to non-OOS items).
  4. Coupon passed by id to checkout — checkout re-validates on the backend; client discount is a preview.

Checkout / navigation

  1. Cart is snapshotted into URL params — long carts → long URLs; quantities/slots frozen at navigation.
  2. availableItems.length === 0 guard produces specific alerts (all OOS / none flash-eligible / missing slots) — good UX.
  3. Partial availability: if only some items are available, checkout proceeds with only the available ones — the unavailable ones stay in the cart (not removed).
  4. Cart type switcher navigates between regular/flash flows, but does NOT move items — each cart type is independent.

Flash-specific

  1. Flash cart items have slotId: 0 — the cart page forces selectedSlots[item.id] = 0 for flash.
  2. Adding via "1 hr Delivery" in the dialog does NOT add to the regular cart — it sets addedFlashProduct and navigates to flash products; the actual add happens on the flash page. (If the user abandons the flash page, nothing was added.)
  3. isFlashDeliveryEnabled gate — flash option only shows if the const is true.

Concurrency / timing

  1. useGetCart enabled only when productsById is populated — before the central product store loads, the cart query is disabled (no stale empty cart flash). Good.
  2. refetchOnWindowFocus default true → returning to the app refreshes cart totals (from local storage) but prices come from cached products — price updates in the catalog reflect on next product refetch.
  3. Floating bar slot-reassignment runs once ([] deps) — on mount only; slot validity isn't re-checked on later slot-data changes.
  4. Remove-then-add in floating bar for invalid slots changes the local item id (new id) — any UI holding the old id could mismatch.

8. Potential improvements (suggestions, not required)

  • Persist slot changes: write selectedSlots back to LocalCartItem.slotId so slot choices survive leaving the cart.
  • Unify quantity step (dialog step=1 vs cart incrementStep).
  • Fix flash coupon min-order to use flash price.
  • Handle "phantom items": purge local cart entries whose product no longer exists (or show them as unavailable).
  • Serialize add-to-cart writes (mutex/queue) to avoid id races on rapid taps.
  • Surface storage write failures instead of silently succeeding.

Documentation based on code reading of cart-query-hooks.tsx, cartStore.ts, flashCartStore.ts, centralProductStore.ts, centralSlotStore.ts, cart-page.tsx, floating-cart-bar.tsx, AddToCartDialog.tsx, and checkout-page.tsx.