From 5152b16d616ad729fddb77892fe6304f10985dd1 Mon Sep 17 00:00:00 2001 From: "amirhosein.ashourloo" Date: Sun, 6 Sep 2026 15:01:22 +0330 Subject: [PATCH] Revert to guest-cart architecture, drop in-site auth/checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per api.meshkee.com/docs/website/AI_PROMPT.md's hard rules (which an earlier pass on this branch missed): login, register, OTP, the real cart, addresses, and payment are the customer dashboard's job (customer.sanihome.ir), not this storefront's. This site should only ever run a local guest mini-cart and hand off to the dashboard. - Remove: AuthContext, LoginModal, lib/auth.ts, lib/cart.ts (the real /businesses/{id}/cart integration), and the /checkout page entirely. - Add lib/guestCart.ts: localStorage + shared cookie (meshkee-guest-cart, Domain=.sanihome.ir) cart, storeItemVariantId keyed, matching the spec's line shape and ~3500-char cookie-size cutoff. - CartContext is now a thin React wrapper over the guest cart (no server round-trip, so add/update/remove are synchronous); CartPopup's "Continue" builds the exact customer.sanihome.ir/checkout/cart (or /login?redirect=...) URL from the spec, base64url-encoding the cart and detecting an existing dashboard session via the shared meshkee_customer_access_token cookie. - ProductCard and ProductPurchasePanel add to the guest cart directly; no login gate on add-to-cart anymore. - Comments panel is a plain guest name/email/text form again (the comments API never required auth — gating it on the now-removed login system was wrong regardless). - CONTEXT.md rewritten to document the guest-cart contract explicitly and flag this exact mistake so it doesn't get reintroduced. Co-Authored-By: Claude Sonnet 5 --- CONTEXT.md | 150 +++++----- src/app/checkout/page.tsx | 192 ------------- src/app/layout.tsx | 2 +- src/app/products/[slug]/page.tsx | 3 + src/app/products/page.tsx | 1 + src/app/store-items/[id]/page.tsx | 3 + src/components/auth/LoginModal.tsx | 262 ------------------ src/components/cart/CartButton.tsx | 5 +- src/components/cart/CartPopup.tsx | 48 ++-- src/components/home/BrandGroupCarousels.tsx | 1 + src/components/home/SpecialSaleCarousel.tsx | 1 + src/components/home/TopSellingCarousel.tsx | 1 + .../product/ProductPurchasePanel.tsx | 45 ++- src/components/product/ProductTabs.tsx | 72 ++--- src/components/product/SimilarProducts.tsx | 1 + src/components/ui/ProductCard.tsx | 36 +-- src/context/AuthContext.tsx | 147 ---------- src/context/CartContext.tsx | 117 +++----- src/context/Providers.tsx | 15 +- src/lib/auth.ts | 211 -------------- src/lib/cart.ts | 171 ------------ src/lib/guestCart.ts | 126 +++++++++ src/lib/meshkee.ts | 7 +- 23 files changed, 351 insertions(+), 1266 deletions(-) delete mode 100644 src/app/checkout/page.tsx delete mode 100644 src/components/auth/LoginModal.tsx delete mode 100644 src/context/AuthContext.tsx delete mode 100644 src/lib/auth.ts delete mode 100644 src/lib/cart.ts create mode 100644 src/lib/guestCart.ts diff --git a/CONTEXT.md b/CONTEXT.md index 782dfe6..64cd20c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -51,81 +51,72 @@ before touching App Router APIs you're not 100% sure of. because different endpoints shape the "same" concept differently (see Data quirks below). Prefer extending an existing `normalize*`/type over adding a parallel one. -- **`auth.ts`** — client-only (uses `localStorage`) Meshkee customer auth: - register/login/OTP/refresh. Access tokens last **~15 minutes** — this is - short enough that you *will* hit expiry during normal use, so every - authenticated call must self-heal (see Auth below), not just fail. -- **`cart.ts`** — cart/checkout, needs a Bearer token + `businessId` - (`GET /tenants/{domain}` and `.../website/business-info` both return the - same numeric id as their `id` field — `getBusinessInfo().id` is enough, - no separate tenant-resolve call needed). +- **`guestCart.ts`** — the storefront's entire cart story (see next + section). No token, no backend call, no `businessId` — just + `localStorage` + a shared cookie. -## Auth +## Auth, cart & checkout: this site does none of it -Real in-site login (not a redirect to `customer.sanihome.ir` — that link -still exists in the header/footer as a legacy "ورود" pointer but is -independent of this system). `AuthProvider` (`src/context/AuthContext.tsx`) -+ `LoginModal` (`src/components/auth/LoginModal.tsx`), mounted once in -`src/context/Providers.tsx` inside the root layout. +**This was built wrong once already** — an earlier pass added real in-site +login (password/OTP), a real backend cart (`/businesses/{businessId}/cart`), +and a `/checkout` page. That's explicitly **not** how Meshkee storefronts +work, per `https://api.meshkee.com/docs/website/AI_PROMPT.md`'s hard rules: +login, register, OTP, the real cart, addresses, and payment all belong to +**`https://customer.`** (the customer dashboard) — a *separate app*, +not a page on this site. If you're about to add a `/login`, `/register`, +`/checkout`, `/cart`, or `/account` route, or call `/auth/*` or +`/businesses/{id}/cart*` from this codebase: **stop, re-read the AI_PROMPT.md +"Shopping cart on the storefront" and "Shared login" sections first.** -- Login methods: password (`POST /auth/login`) and SMS OTP - (`POST /auth/send-otp` → `POST /auth/login-otp`). Register: - `POST /auth/register` (requires `domain: "sanihome.ir"`). -- **Confirmed live response shape** (undocumented in OpenAPI, verified by - actually registering a test account): `{ accessToken, refreshToken, user }` - from register/login/login-otp/refresh alike. `GET /auth/me` returns `{ user }`. -- **Password login requires a verified phone number** - (`cellNumber is not verified` 401) — a freshly-registered account can still - use the tokens *from that register response* immediately, but if the - session is lost before verifying, password login is blocked until OTP - verification. There's no verify-OTP UI built yet (see Known gaps). -- **Token refresh race**: `AuthContext` and `CartContext` each independently - notice a 401 and try to refresh. Without coordination this fires two - concurrent `/auth/refresh` calls and the second one fails (likely - refresh-token rotation server-side) — observed as 500/400 errors. Fixed - with an in-flight-promise singleton in `auth.ts`'s `refreshAccessToken()` - — **do not** add another independent refresh call anywhere; always go - through that function. -- `cart.ts` functions take no token parameter — they read the freshest token - from storage internally and self-refresh-and-retry once on 401. Gate UI on - `useAuth().user` (session identity), not on a captured `accessToken` value. - -## Cart & checkout - -`CartProvider` (`src/context/CartContext.tsx`) fetches the real backend cart -whenever `user` changes and exposes `addItem/updateItem/removeItem/refresh`. -`CartButton` + `CartPopup` (`src/components/cart/`) live in the header via -`Providers`. `/checkout` (`src/app/checkout/page.tsx`) posts to -`POST /businesses/{businessId}/cart/checkout` with `payment.type` of `cash` -or `transfer` only — no online payment gateway (Zarinpal/Mellat/etc.) is -wired up, by explicit scope decision (see Known gaps). - -**Important:** a successful checkout clears the cart server-side. Any flow -that calls `checkout()` must also call `useCart().refresh()` afterward, or -the header badge and cart state go stale until next reload. - -**Which store-item variant does "add to cart" target?** -- On the product/store-item **detail** pages, `ProductPurchasePanel` always - resolves a `matchedVariant` from `storeVariants` (even for a single-SKU - product with no selectable variations — it just uses the one variant) and - adds `matchedVariant.id`. -- On **cards** (`ProductCard`), only sources that already carry a specific - variant id can quick-add: `Product.variantId`, populated in - `normalizeProduct` from `raw.variants[0].id` (store-specials, brand-groups). - Cards from the plain `/products` list endpoint have no variant info, so the - button renders as a link to the detail page instead of a cart action — - never fabricate a variant id to force a quick-add. +- **Detecting a logged-in shopper**: read the `meshkee_customer_access_token` + cookie (written by the customer dashboard with `Domain=.sanihome.ir`, so + it's readable here too) — see `isCustomerLoggedIn()` in `guestCart.ts`. + Never build a login UI or call `/auth/login` from this site. +- **Guest cart** (`guestCart.ts`): lines are + `{ id, name, slug, price, originalPrice?, image, quantity }` where `id` is + always a `storeItemVariantId` — never a product id or store-item id. + Persisted to `localStorage` (key `meshkee-guest-cart`) **and** a cookie of + the same name (`Domain=.sanihome.ir`, skipped if the encoded JSON is + >~3500 chars — rely on the URL param instead). `CartProvider` + (`src/context/CartContext.tsx`) just wraps this in React state; there is + no server round-trip, so `addItem`/`updateItem`/`removeItem` are + synchronous. +- **"Continue" in the mini-cart** (`CartPopup`) builds a URL with + `continueToCheckoutUrl()`: the guest cart, base64url-encoded (exact + algorithm from the spec) into a `guestCart` query param, pointed at + `https://customer.sanihome.ir/checkout/cart?guestCart=...` if the shopper + looks logged in, or `.../login?redirect=...` (wrapping that same cart URL) + if not. The dashboard is responsible for decoding it and syncing it into + the real server cart after login — this site's job ends at building that + URL correctly. +- **Which variant does "add to cart" target?** + - On the product/store-item **detail** pages, `ProductPurchasePanel` + resolves a `matchedVariant` from `storeVariants` (even for a single-SKU + product with no selectable variations — it just uses the one variant) + and adds `matchedVariant.id`. + - On **cards** (`ProductCard`), only sources that already carry a specific + variant id can quick-add: `Product.variantId`, populated in + `normalizeProduct` from `raw.variants[0].id` (store-specials, + brand-groups). Cards from the plain `/products` list endpoint have no + variant info, so the button links to the detail page instead — never + fabricate a variant id to force a quick-add. + - The spec's fuller flow (`GET /store-items/by-product/{productId}`, + then: hide the button if no in-stock variant exists, force a picker + when more than one is in stock) isn't separately re-implemented per + card — the existing variation-picker UI on the detail page already + satisfies "shopper must choose before adding." ## Comments -`GET/POST /tenants/{domain}/comments` needs no auth on Meshkee's side -(`authorName`/`authorEmail` are plain body fields) — the login requirement is -a **product decision by this site**, not an API constraint. Gate the comment -form in `ProductTabs`' comments panel on `useAuth().user`; when posting, use -the logged-in user's name/email. `GET /comments` only returns **approved** -comments, so a freshly-posted comment won't reappear on refetch until an -admin approves it — the UI appends it optimistically to local state instead -of re-fetching, which is correct, not a bug. +`GET/POST /tenants/{domain}/comments` needs no auth on Meshkee's side — +`authorName`/`authorEmail` are plain body fields, same as the contact form. +`ProductTabs`' comments panel is a guest name+email(optional)+text form, no +login involved (this also isn't gated on the dashboard cookie — keep it +that way; there's no reason to require a customer-dashboard session just to +leave a comment). `GET /comments` only returns **approved** comments, so a +freshly-posted comment won't reappear on refetch until an admin approves it +— the UI appends it optimistically to local state instead of re-fetching, +which is correct, not a bug. ## Data quirks (verified against the live catalog) @@ -213,18 +204,17 @@ else the pre-existing UI unchanged). | `/products/id/[id]` | Redirect-only resolver, see "Group-source id vs productId" above | | `/store-items`, `/store-items/[id]` | Variant-level browsing/detail, see above | | `/blog`, `/blog/[slug]` | Blog listing/detail | -| `/checkout` | Cart → order, cash/transfer only | | `/contact` | Contact form → `POST /contact-submissions`, toast + reset on success | +No `/login`, `/register`, `/checkout`, `/cart`, or `/account` route exists +here on purpose — see "Auth, cart & checkout" above. + ## Known gaps / explicit scope decisions -- No OTP-verification UI for a registered-but-unverified phone number — if a - returning shopper's session is lost pre-verification, password login fails - with a clear error but there's no in-app way to complete verification yet - (OTP *login* still works as a workaround). -- No online payment gateway integration (Zarinpal/Mellat/etc.) — checkout - only supports cash-on-delivery and bank transfer, by explicit user choice - over building the bank-redirect + return-URL callback flow. -- A throwaway test customer account exists in the live business database - from building/verifying this integration: `+989120000001` / "Test Dev". - Safe to delete from the Meshkee admin panel. +- Login, registration, OTP, the real cart, addresses, and payment are all + out of scope for this codebase by design — they live on + `customer.sanihome.ir`. Don't reintroduce them here. +- A throwaway test customer account was created against the live business + database while an earlier (since-reverted) pass tested a real backend + cart: `+989120000001` / "Test Dev". Safe to delete from the Meshkee admin + panel — it's not used by anything in this codebase anymore. diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx deleted file mode 100644 index a4b2943..0000000 --- a/src/app/checkout/page.tsx +++ /dev/null @@ -1,192 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Breadcrumb } from "@/components/ui/Breadcrumb"; -import { EmptyState } from "@/components/ui/EmptyState"; -import { useAuth } from "@/context/AuthContext"; -import { useCart } from "@/context/CartContext"; -import { checkout, type Order } from "@/lib/cart"; -import { formatPrice } from "@/data/sample"; - -type PaymentType = "cash" | "transfer"; - -const inputClass = - "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus:border-red"; - -export default function CheckoutPage() { - const { user, openLoginModal } = useAuth(); - const { cart, businessId, refresh } = useCart(); - const [province, setProvince] = useState(""); - const [city, setCity] = useState(""); - const [address, setAddress] = useState(""); - const [postalCode, setPostalCode] = useState(""); - const [paymentType, setPaymentType] = useState("cash"); - const [transferRefNumber, setTransferRefNumber] = useState(""); - const [customerNotes, setCustomerNotes] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [order, setOrder] = useState(null); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setBusy(true); - setError(null); - const res = await checkout(businessId, { - shippingAddress: { province, city, address, postalCode: postalCode || undefined }, - customerNotes: customerNotes || undefined, - payment: - paymentType === "transfer" - ? { type: "transfer", transferRefNumber: transferRefNumber || undefined } - : { type: "cash" }, - }); - setBusy(false); - if (!res.ok) { - setError(res.message); - return; - } - setOrder(res.data); - refresh(); - }; - - return ( -
- -

تسویه حساب

- - {!user ? ( -
- برای تسویه حساب باید وارد حساب کاربری خود شوید. - -
- ) : order ? ( -
-

سفارش شما با موفقیت ثبت شد ✓

-

شماره سفارش: {order.orderNumber}

-

- مبلغ کل: {formatPrice(order.total)} تومان -

- - بازگشت به فروشگاه ← - -
- ) : !cart || cart.items.length === 0 ? ( - - ) : ( -
-
-
-

آدرس ارسال

-
- setProvince(e.target.value)} - className={inputClass} - /> - setCity(e.target.value)} - className={inputClass} - /> -
-