@wix/agent-skills 1.19.2 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
  3. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
  4. package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
  5. package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
  6. package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
  7. package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
  8. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
  9. package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
  10. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
  11. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
  12. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
  13. package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
  14. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
  15. package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
  16. package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
  17. package/skills/wix-headless/references/CAPABILITIES.md +2 -2
  18. package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
  19. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
  20. package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
  21. package/skills/wix-headless-fast/SKILL.md +200 -34
  22. package/skills/wix-headless-fast/entry/skill.md +10 -2
  23. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  24. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  25. package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
  26. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  27. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  28. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  29. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  30. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  31. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  32. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  33. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  34. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  35. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  36. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  37. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  38. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  39. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  40. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  41. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  42. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  43. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  44. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  45. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  46. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  47. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  48. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  49. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  50. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  51. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  52. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  53. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  54. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  55. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  56. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  57. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  58. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  59. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  60. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  61. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  62. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  63. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  64. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  65. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  66. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  67. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  68. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  69. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  70. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  71. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  72. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  73. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  74. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  75. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  76. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  77. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  78. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  79. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  80. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  81. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  82. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  83. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  84. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  85. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  86. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  87. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  88. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  89. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  90. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  91. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  92. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  93. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  94. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  95. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  96. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  97. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  98. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  99. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  100. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  101. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  102. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  103. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  104. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  105. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  106. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  107. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  108. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  109. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  110. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  111. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  112. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  113. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  114. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  115. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  116. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  117. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  118. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  119. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  120. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  121. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  122. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  123. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  124. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  125. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  126. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  127. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  128. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  129. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  130. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  131. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  132. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  133. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  134. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  135. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  136. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  137. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  138. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  139. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  140. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  141. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  142. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  143. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  144. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  145. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  146. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  147. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
  148. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  149. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  150. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  151. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  152. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
  153. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
  154. package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
  155. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  156. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
  157. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
  158. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
  159. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  160. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
  161. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  162. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
  163. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  164. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  165. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
  166. package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
  167. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  168. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  169. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
  170. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
  171. package/skills/wix-manage/SKILL.md +4 -1
  172. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  173. package/skills/wix-manage/references/events/create-wix-event.md +46 -0
  174. package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
  175. package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
  176. package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
  177. package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
  178. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
  179. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
  180. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  181. package/skills/wix-vibe-headless/platforms/base44.md +19 -4
  182. package/skills/wix-vibe-headless/platforms/generic.md +6 -0
  183. package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
  184. package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
  185. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
  186. package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
  187. package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
  188. package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
  189. package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
  190. package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
  191. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
  192. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
  193. package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
  194. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
  195. package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
  196. package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
  197. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
  198. package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
  199. package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
  200. package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
  201. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
  202. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
  203. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
  204. package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
  205. package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
  206. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
  207. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
  208. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
  209. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
  210. package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
  211. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
  212. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
  213. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  214. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  215. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,289 @@
1
+ // The shop listing as a framework-free store — the logic behind useShop, usable from React
2
+ // (useShop wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
3
+ // render), or as the specification for a port. Same shape as cart-store.ts: state, actions,
4
+ // subscribe/getState, emit after every change.
5
+ //
6
+ // Category, sort, price/stock/search filters, option facets, and cursor paging are all applied
7
+ // by Wix across the whole catalog (searchCatalog), never to a page already in hand. A changed
8
+ // selection starts a fresh cursor chain; a late response from a superseded query is dropped.
9
+ //
10
+ // SSR-friendly: seed with `initialProducts`/`initialCategories` and they render at once; `start()`
11
+ // then revalidates the first page to open the cursor chain, replacing the seed without a
12
+ // skeleton flash. `initialCategoryId` scopes the first query (a /category/[slug] page).
13
+ //
14
+ // URL state: sort, filters, facet choices (and a live category switch on /shop) are read from the
15
+ // query string by `start()` and written back with replaceState after every change — a filtered
16
+ // gallery is a link a shopper can share, reload, and step back to. Paging stays out of the URL.
17
+ //
18
+ // One store per mounted listing (a page can hold a shop and a featured rail): createShopStore(),
19
+ // not a singleton. The cart store is a singleton because the cart is one per visitor.
20
+ import { CATALOG_SORTS, fetchCategories, fetchFacetData, searchCatalog, type CatalogSort } from "./catalog";
21
+ import type { Category, Facet, PriceRange, ProductSummary } from "./types";
22
+
23
+ export const SORTS = CATALOG_SORTS;
24
+
25
+ export interface ShopFilters {
26
+ minPrice?: number | string;
27
+ maxPrice?: number | string;
28
+ inStockOnly?: boolean;
29
+ /** Name search, max 100 chars. */
30
+ search?: string;
31
+ }
32
+
33
+ export interface ShopStoreOptions {
34
+ initialProducts?: ProductSummary[];
35
+ initialCategories?: Category[];
36
+ /** Scope the listing to one category from the first render (a /category/[slug] page). */
37
+ initialCategoryId?: string | null;
38
+ pageSize?: number;
39
+ /** Mirror the selection into the query string (default true; off for a rail that isn't the page's subject). */
40
+ syncUrl?: boolean;
41
+ }
42
+
43
+ /** Everything a listing surface renders from. Read it with getState() or through a subscription. */
44
+ export interface ShopState {
45
+ /** null while the first load is in flight — render skeletons, not an empty state. */
46
+ products: ProductSummary[] | null;
47
+ /** Matching products across the whole catalog for the current selection; null until known. */
48
+ total: number | null;
49
+ categories: Category[];
50
+ /** null = "all products". */
51
+ activeCategoryId: string | null;
52
+ sort: CatalogSort;
53
+ filters: ShopFilters;
54
+ /** The filterable options of the current scope (Color, Size…), from the catalog itself. */
55
+ facets: Facet[];
56
+ /** Lowest and highest product price in the scope — the price slider's bounds; null until known or when equal. */
57
+ priceRange: PriceRange | null;
58
+ /** Selected facet choice ids — products carrying ANY of them match. */
59
+ selectedChoiceIds: string[];
60
+ /** True when any filter or facet is active. */
61
+ hasActiveFilters: boolean;
62
+ loading: boolean;
63
+ error: string | null;
64
+ hasMore: boolean;
65
+ loadingMore: boolean;
66
+ }
67
+
68
+ export interface ShopStore {
69
+ getState(): ShopState;
70
+ subscribe(listener: () => void): () => void;
71
+ /** Adopt the URL, fetch categories/facets, open the cursor chain. Call once when mounted (a browser). */
72
+ start(): void;
73
+ /** Stop reacting; drop late responses. */
74
+ stop(): void;
75
+ setActiveCategoryId(id: string | null): void;
76
+ setSort(sort: CatalogSort): void;
77
+ setFilters(filters: ShopFilters): void;
78
+ toggleChoice(choiceId: string): void;
79
+ /** Clears price/stock/search filters and facet selections (keeps category and sort). */
80
+ clearFilters(): void;
81
+ retry(): void;
82
+ loadMore(): Promise<void>;
83
+ }
84
+
85
+ const URL_KEYS = { sort: "sort", min: "min", max: "max", stock: "stock", q: "q", choice: "choice", category: "category" } as const;
86
+
87
+ function readUrlState(): { sort?: CatalogSort; filters: ShopFilters; choiceIds: string[]; categoryId?: string | null } | null {
88
+ if (typeof window === "undefined") return null;
89
+ const p = new URLSearchParams(window.location.search);
90
+ const sort = p.get(URL_KEYS.sort);
91
+ const filters: ShopFilters = {};
92
+ if (p.get(URL_KEYS.min)) filters.minPrice = p.get(URL_KEYS.min)!;
93
+ if (p.get(URL_KEYS.max)) filters.maxPrice = p.get(URL_KEYS.max)!;
94
+ if (p.get(URL_KEYS.stock) === "1") filters.inStockOnly = true;
95
+ if (p.get(URL_KEYS.q)) filters.search = p.get(URL_KEYS.q)!;
96
+ return {
97
+ sort: sort && sort in CATALOG_SORTS ? (sort as CatalogSort) : undefined,
98
+ filters,
99
+ choiceIds: p.getAll(URL_KEYS.choice),
100
+ categoryId: p.has(URL_KEYS.category) ? p.get(URL_KEYS.category) : undefined,
101
+ };
102
+ }
103
+
104
+ function writeUrlState(s: { sort: CatalogSort; filters: ShopFilters; selectedChoiceIds: string[]; activeCategoryId: string | null }, initialCategoryId: string | null): void {
105
+ if (typeof window === "undefined") return;
106
+ const p = new URLSearchParams(window.location.search);
107
+ for (const k of Object.values(URL_KEYS)) p.delete(k);
108
+ if (s.sort !== "featured") p.set(URL_KEYS.sort, s.sort);
109
+ if (s.filters.minPrice != null && s.filters.minPrice !== "") p.set(URL_KEYS.min, String(s.filters.minPrice));
110
+ if (s.filters.maxPrice != null && s.filters.maxPrice !== "") p.set(URL_KEYS.max, String(s.filters.maxPrice));
111
+ if (s.filters.inStockOnly) p.set(URL_KEYS.stock, "1");
112
+ if (s.filters.search?.trim()) p.set(URL_KEYS.q, s.filters.search.trim());
113
+ for (const id of s.selectedChoiceIds) p.append(URL_KEYS.choice, id);
114
+ if (s.activeCategoryId !== initialCategoryId) p.set(URL_KEYS.category, s.activeCategoryId ?? "");
115
+ const qs = p.toString();
116
+ const next = `${window.location.pathname}${qs ? `?${qs}` : ""}${window.location.hash}`;
117
+ if (next !== `${window.location.pathname}${window.location.search}${window.location.hash}`) window.history.replaceState(window.history.state, "", next);
118
+ }
119
+
120
+ export function createShopStore({ initialProducts, initialCategories, initialCategoryId = null, pageSize = 24, syncUrl = true }: ShopStoreOptions = {}): ShopStore {
121
+ // selection
122
+ let activeCategoryId: string | null = initialCategoryId;
123
+ let sort: CatalogSort = "featured";
124
+ let filters: ShopFilters = {};
125
+ let selectedChoiceIds: string[] = [];
126
+ let attempt = 0;
127
+ // data
128
+ let categories: Category[] = initialCategories ?? [];
129
+ let facets: Facet[] = [];
130
+ let priceRange: PriceRange | null = null;
131
+ // the current page; `key` is the selection it answers to (null = the SSR seed, no query yet)
132
+ let pageKey: string | null = null;
133
+ let products: ProductSummary[] | null = initialProducts ?? null;
134
+ let total: number | null = null;
135
+ let cursor: string | null = null;
136
+ let error: string | null = null;
137
+ let loadingMore = false;
138
+ // control
139
+ let started = false;
140
+ let generation = 0;
141
+ let pendingMore: object | null = null;
142
+ const listeners = new Set<() => void>();
143
+
144
+ const selectionKey = () =>
145
+ JSON.stringify([pageSize, activeCategoryId, sort, filters.minPrice ?? null, filters.maxPrice ?? null, !!filters.inStockOnly, filters.search ?? "", [...selectedChoiceIds].sort(), attempt]);
146
+
147
+ let snapshot: ShopState | null = null;
148
+ const emit = () => {
149
+ snapshot = null;
150
+ for (const fn of listeners) fn();
151
+ };
152
+
153
+ function getState(): ShopState {
154
+ if (snapshot) return snapshot;
155
+ const key = selectionKey();
156
+ const current = pageKey === key;
157
+ const seeded = pageKey === null && products !== null; // SSR seed, first query in flight
158
+ const hasActiveFilters =
159
+ selectedChoiceIds.length > 0 ||
160
+ !!filters.inStockOnly ||
161
+ (filters.minPrice != null && filters.minPrice !== "") ||
162
+ (filters.maxPrice != null && filters.maxPrice !== "") ||
163
+ !!(filters.search && filters.search.trim());
164
+ snapshot = {
165
+ products: current || seeded ? products : null,
166
+ total: current ? total : null,
167
+ categories,
168
+ activeCategoryId,
169
+ sort,
170
+ filters,
171
+ facets,
172
+ priceRange,
173
+ selectedChoiceIds,
174
+ hasActiveFilters,
175
+ // Seeded content on screen is not "loading" — skeletons are only for a genuinely empty wait.
176
+ loading: (!current && !seeded) || (current && products === null),
177
+ error: current ? error : null,
178
+ hasMore: current && !!cursor,
179
+ loadingMore: current && loadingMore,
180
+ };
181
+ return snapshot;
182
+ }
183
+
184
+ // Facets follow the category scope (a Size facet in "Donuts" is meaningless).
185
+ let facetScope: string | null | undefined;
186
+ function loadFacets(): void {
187
+ if (facetScope === activeCategoryId) return;
188
+ facetScope = activeCategoryId;
189
+ const scope = activeCategoryId;
190
+ fetchFacetData({ categoryId: scope }).then((d) => {
191
+ if (!started || scope !== activeCategoryId) return;
192
+ facets = d.facets;
193
+ priceRange = d.priceRange;
194
+ emit();
195
+ });
196
+ }
197
+
198
+ // Run the query for the current selection. Keeps the seed (or the previous page) on screen while
199
+ // it runs; only a selection change with no seed shows skeletons.
200
+ function query(): void {
201
+ if (!started) return;
202
+ const key = selectionKey();
203
+ const id = ++generation;
204
+ pendingMore = null;
205
+ if (pageKey !== null) products = null;
206
+ pageKey = key; total = null; cursor = null; error = null; loadingMore = false;
207
+ emit();
208
+ const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search, choiceIds] = JSON.parse(key);
209
+ searchCatalog({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search, choiceIds })
210
+ .then((res) => {
211
+ if (generation !== id || selectionKey() !== key) return; // superseded — drop it
212
+ products = res.products; total = res.total; cursor = res.nextCursor; error = null;
213
+ emit();
214
+ })
215
+ .catch((e) => {
216
+ if (generation !== id || selectionKey() !== key) return;
217
+ products = []; total = null; cursor = null;
218
+ error = e instanceof Error ? e.message : "Couldn't load products.";
219
+ emit();
220
+ });
221
+ }
222
+
223
+ function changed(): void {
224
+ if (syncUrl) writeUrlState({ sort, filters, selectedChoiceIds, activeCategoryId }, initialCategoryId);
225
+ loadFacets();
226
+ // A new filters object with the same values keeps the page (same selection, same query).
227
+ if (selectionKey() === pageKey) emit();
228
+ else query();
229
+ }
230
+
231
+ return {
232
+ getState,
233
+ subscribe(fn) {
234
+ listeners.add(fn);
235
+ return () => listeners.delete(fn);
236
+ },
237
+ start() {
238
+ if (started) return;
239
+ started = true;
240
+ // Adopt the URL's selection once (the server rendered the defaults).
241
+ const u = syncUrl ? readUrlState() : null;
242
+ if (u) {
243
+ if (u.sort) sort = u.sort;
244
+ if (Object.keys(u.filters).length) filters = u.filters;
245
+ if (u.choiceIds.length) selectedChoiceIds = u.choiceIds;
246
+ if (u.categoryId !== undefined && u.categoryId !== initialCategoryId) activeCategoryId = u.categoryId || null;
247
+ }
248
+ if (!initialCategories) {
249
+ fetchCategories().then((list) => { if (started) { categories = list; emit(); } }).catch(() => { if (started) { categories = []; emit(); } });
250
+ }
251
+ loadFacets();
252
+ query();
253
+ },
254
+ stop() {
255
+ started = false;
256
+ generation++;
257
+ },
258
+ setActiveCategoryId(id) { if (id === activeCategoryId) return; activeCategoryId = id; changed(); },
259
+ setSort(next) { if (next === sort) return; sort = next; changed(); },
260
+ setFilters(next) { filters = next; changed(); },
261
+ toggleChoice(choiceId) {
262
+ selectedChoiceIds = selectedChoiceIds.includes(choiceId) ? selectedChoiceIds.filter((x) => x !== choiceId) : [...selectedChoiceIds, choiceId];
263
+ changed();
264
+ },
265
+ clearFilters() { filters = {}; selectedChoiceIds = []; changed(); },
266
+ retry() { attempt++; changed(); },
267
+ async loadMore() {
268
+ const key = selectionKey();
269
+ if (pageKey !== key || !cursor || pendingMore) return;
270
+ const request = { generation, key };
271
+ pendingMore = request; // blocks repeated clicks before a re-render
272
+ const isCurrent = () => generation === request.generation && selectionKey() === key;
273
+ loadingMore = true; error = null; emit();
274
+ try {
275
+ const res = await searchCatalog({ limit: pageSize, cursor });
276
+ if (isCurrent()) {
277
+ const seen = new Set((products ?? []).map((p) => p.id));
278
+ products = [...(products ?? []), ...res.products.filter((p) => (seen.has(p.id) ? false : (seen.add(p.id), true)))];
279
+ cursor = res.nextCursor;
280
+ }
281
+ } catch (e) {
282
+ if (isCurrent()) error = e instanceof Error ? e.message : "Couldn't load more products.";
283
+ } finally {
284
+ if (pendingMore === request) pendingMore = null;
285
+ if (isCurrent()) { loadingMore = false; emit(); }
286
+ }
287
+ },
288
+ };
289
+ }
@@ -10,14 +10,24 @@ export interface ProductSummary {
10
10
  id: string;
11
11
  slug: string;
12
12
  name: string;
13
- /** Lowest price, formatted with currency symbol (e.g. "€34.99"). */
13
+ /**
14
+ * The price the buyer pays for the cheapest variant, formatted (an automatic discount already
15
+ * applied). When `price !== maxPrice` the product is a RANGE — render "price – maxPrice".
16
+ */
14
17
  price: string;
15
- /** Highest price, formatted — differs from `price` when variants are priced differently. */
18
+ /** Highest variant price, formatted — differs from `price` when variants are priced differently. */
16
19
  maxPrice: string;
17
- /** Strikethrough "was" price, formatted; null when not on sale. */
20
+ /**
21
+ * Struck "was" price, formatted; null when not on sale — and ALWAYS null for a range (a lone
22
+ * struck minimum beside a range claims a saving that may not apply to the variant picked).
23
+ */
18
24
  compareAtPrice: string | null;
19
- /** Merchant-set badge ("New", "Best Seller"); null when none. */
25
+ /** The primary merchant ribbon ("New", "Best Seller"); null when none. Same as ribbons[0]. */
20
26
  ribbon: string | null;
27
+ /** EVERY merchant ribbon, primary first ("New", "Sale") — render all of them, one shared style. */
28
+ ribbons: string[];
29
+ /** The cheapest variant's id — what a direct add sends for a product with no options; null when unknown. */
30
+ minPriceVariantId: string | null;
21
31
  availability: Availability;
22
32
  /** OUT_OF_STOCK but pre-orderable — label "Pre-order", not "Sold out". */
23
33
  preorder: boolean;
@@ -27,6 +37,11 @@ export interface ProductSummary {
27
37
  hoverImageUrl: string;
28
38
  /** e.g. "2 colors · 3 sizes"; "" for a single-variant product. */
29
39
  optionsSummary: string;
40
+ /**
41
+ * Hex colors of a color option's visible choices, catalog order — render as small dots on the
42
+ * tile (a preview, not a picker: selection happens in QuickAdd or on the PDP). [] when none.
43
+ */
44
+ swatches: string[];
30
45
  /** True when the product can be added to the cart with no choices (single variant, in stock). */
31
46
  quickAddable: boolean;
32
47
  }
@@ -60,15 +75,21 @@ export interface ProductVariant {
60
75
  variantId: string;
61
76
  /** The option selections this variant answers to: optionName -> choiceName. */
62
77
  choices: Record<string, string>;
78
+ /** The price the buyer pays, formatted — an automatic discount beats the regular price. */
63
79
  price: string;
80
+ /** Struck "was" price, formatted; null unless it is real and higher than `price`. */
64
81
  compareAtPrice: string | null;
65
82
  inStock: boolean;
83
+ /** Out of stock but pre-orderable — still buyable (the add carries preOrderRequested). */
84
+ preorderEnabled: boolean;
66
85
  }
67
86
 
68
87
  /** A product as the detail page needs it. */
69
88
  export interface ProductDetail extends ProductSummary {
70
89
  /** Product description as an HTML string — render with innerHTML, not as text. */
71
90
  descriptionHtml: string;
91
+ /** Merchant info sections (materials, shipping, care…) — title + HTML; render as sections or accordions. */
92
+ infoSections: { title: string; html: string }[];
72
93
  /** Every gallery image as a resolved https URL, main image first, de-duplicated. */
73
94
  gallery: string[];
74
95
  options: ProductOption[];
@@ -80,6 +101,36 @@ export interface Category {
80
101
  id: string;
81
102
  slug: string;
82
103
  name: string;
104
+ /** Plain-text description when the merchant wrote one; "" otherwise. */
105
+ description: string;
106
+ }
107
+
108
+ export interface FacetChoice {
109
+ /** The option choice id — what searchCatalog({ choiceIds }) filters on. */
110
+ id: string;
111
+ name: string;
112
+ /** Hex color for a swatch facet; null for a text facet. */
113
+ colorCode: string | null;
114
+ }
115
+
116
+ /** One filterable option across the catalog (or a category): "Color" with its choices. */
117
+ export interface Facet {
118
+ name: string;
119
+ isColor: boolean;
120
+ choices: FacetChoice[];
121
+ }
122
+
123
+ /** The catalog's (or category's) lowest and highest product price, as numbers in site currency — the slider's bounds. */
124
+ export interface PriceRange {
125
+ min: number;
126
+ max: number;
127
+ currency: string;
128
+ }
129
+
130
+ /** What the filter panel needs beyond the product page: the facets and the price bounds of the scope. */
131
+ export interface FacetData {
132
+ facets: Facet[];
133
+ priceRange: PriceRange | null;
83
134
  }
84
135
 
85
136
  export interface CartLine {
@@ -97,6 +148,11 @@ export interface CartLine {
97
148
  descriptionLines: string[];
98
149
  /** Not IN_STOCK → the line can't be checked out as-is. */
99
150
  status: string;
151
+ /**
152
+ * The recurring plan's terms for a subscription line — "Monthly plan · every month · 12 payments";
153
+ * "" for a one-time purchase. A subscription line must read as one in the cart.
154
+ */
155
+ subscription: string;
100
156
  }
101
157
 
102
158
  export interface Cart {
@@ -105,5 +161,7 @@ export interface Cart {
105
161
  itemCount: number;
106
162
  /** Formatted subtotal (after discounts) — from the cart estimate, not hand-summed. */
107
163
  subtotal: string;
164
+ /** Formatted CART-level discount (a coupon or cart rule) — "" when none; item discounts are already in subtotal. */
165
+ discount: string;
108
166
  currency: string;
109
167
  }
@@ -1,12 +1,18 @@
1
1
  ---
2
2
  // The site chrome for every storefront page — THIS file (header/footer/nav) plus the @theme
3
3
  // token block in src/styles/global.css are what you brand. Keep:
4
- // - the <slot name="seo-tags" /> in <head> (the product page's owner-editable SEO lands there)
4
+ // - the <slot name="seo-tags" /> in <head> (the product page's owner-editable SEO lands there,
5
+ // including the <title> — which is why ours is printed only when the slot is empty)
5
6
  // - the global.css import (Tailwind + the design tokens every shipped component reads)
6
7
  // - one <CartButton> in the header and one <CartDrawer> per page (both client:only —
7
8
  // they read browser cart state and must not server-render)
9
+ // - category links in the nav (or your own menu/footer) — every category page reachable by
10
+ // a real link (`/category/<slug>`), from the live tree, so a category the owner adds later
11
+ // shows up with no code change
8
12
  import CartButton from "../components/storefront/CartButton";
9
13
  import CartDrawer from "../components/storefront/CartDrawer";
14
+ import { fetchCategories } from "../wix/storefront/catalog";
15
+ import type { Category } from "../wix/storefront/types";
10
16
  import "../styles/global.css";
11
17
 
12
18
  interface Props {
@@ -14,6 +20,15 @@ interface Props {
14
20
  description?: string;
15
21
  }
16
22
  const { title, description } = Astro.props;
23
+
24
+ // Non-fatal: the chrome renders without the links if the read fails. Six in the header keeps a
25
+ // large tree from crowding it — put the rest in a menu or the footer, but keep them linked.
26
+ let navCategories: Category[] = [];
27
+ try {
28
+ navCategories = (await fetchCategories()).slice(0, 6);
29
+ } catch {
30
+ /* the shop link still reaches everything */
31
+ }
17
32
  ---
18
33
 
19
34
  <!doctype html>
@@ -21,7 +36,8 @@ const { title, description } = Astro.props;
21
36
  <head>
22
37
  <meta charset="UTF-8" />
23
38
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
- <title>{title}</title>
39
+ {/* The product page's <SEO.Tags> renders the <title>; print ours only when no page filled the slot — two titles otherwise. */}
40
+ {!Astro.slots.has("seo-tags") && <title>{title}</title>}
25
41
  {description && <meta name="description" content={description} />}
26
42
  <slot name="seo-tags" />
27
43
  </head>
@@ -31,6 +47,9 @@ const { title, description } = Astro.props;
31
47
  <a href="/" class="text-base font-bold tracking-tight text-foreground no-underline">Brand Name</a>
32
48
  <nav class="flex items-center gap-6 text-sm">
33
49
  <a href="/shop" class="text-foreground no-underline transition-colors hover:text-muted-foreground">Shop</a>
50
+ {navCategories.map((c) => (
51
+ <a href={`/category/${c.slug}`} class="hidden text-foreground no-underline transition-colors hover:text-muted-foreground md:inline">{c.name}</a>
52
+ ))}
34
53
  <CartButton client:only="react" />
35
54
  </nav>
36
55
  </div>
@@ -0,0 +1,132 @@
1
+ // Cart + checkout over REST (Wix eCom Cart V2) — the twin of app/wix/storefront/cart.ts. Same
2
+ // exports, same Cart/CartLine DTOs; rules and mappers from cart-core (the SAME file the SDK
3
+ // transport uses, deployed flat next to this one). The request shapes are
4
+ // exact and rewriting them is how carts break. Failures are loud: out-of-stock lines, an empty cart
5
+ // at checkout, a missing required selection all throw — surface the message to the buyer.
6
+ // All calls run with the visitor token: the cart is the token's (see ../shared/client).
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md
8
+ // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
9
+ import { WixApiError, wixRequest } from "./client.js";
10
+ import { imgSrc } from "./media.js";
11
+ import { WIX_STORES_APP_ID, addOptions, assertAdded, assertCheckoutable, rawId, summaryTotals, toCart } from "./cart-core.js";
12
+ import type { Raw } from "./catalog-core.js";
13
+ import type { Cart } from "./types.js";
14
+
15
+ const CART = "/ecom/v2/carts/current";
16
+
17
+ export interface AddToCartExtras {
18
+ /** Choice modifiers: modifier key → chosen choice key (options.options — MODIFIERS only, never Size/Color). */
19
+ modifierChoices?: Record<string, string>;
20
+ /** Free-text modifiers: freeTextSettings.key → the buyer's text. */
21
+ customTextFields?: Record<string, string>;
22
+ /** A chosen recurring plan; omit for a one-time purchase. */
23
+ subscriptionOptionId?: string;
24
+ /** The resolved variant is out of stock but pre-orderable → preOrderRequested. */
25
+ preorder?: boolean;
26
+ }
27
+
28
+ // A line's attributes.image comes back absent right after an add (verified live) — join the
29
+ // products' main images by catalogItemId, once per product, cached for the session.
30
+ // POST /stores/v3/products/query { query: { filter: { id: { $in: [...] } } } }
31
+ const lineImageCache = new Map<string, string>();
32
+ async function fillLineImages(cart: Cart, raws: Raw[]): Promise<void> {
33
+ const wanted = new Map<string, number[]>();
34
+ raws.forEach((raw, i) => {
35
+ if (cart.lines[i].imageUrl) return;
36
+ const pid = raw.source?.catalogReference?.catalogItemId;
37
+ if (!pid) return;
38
+ const cached = lineImageCache.get(pid);
39
+ if (cached !== undefined) { cart.lines[i].imageUrl = cached; return; }
40
+ wanted.set(pid, [...(wanted.get(pid) ?? []), i]);
41
+ });
42
+ if (!wanted.size) return;
43
+ try {
44
+ const res = await wixRequest<Raw>("/stores/v3/products/query", { body: { query: { filter: { id: { $in: [...wanted.keys()] } }, cursorPaging: { limit: 100 } } } });
45
+ for (const p of (res?.products ?? []) as Raw[]) {
46
+ const url = imgSrc(p.media?.main, 300, 300);
47
+ lineImageCache.set(rawId(p), url);
48
+ for (const i of wanted.get(rawId(p)) ?? []) cart.lines[i].imageUrl = url;
49
+ }
50
+ } catch {
51
+ /* images are a nicety — the cart stays correct without them */
52
+ }
53
+ }
54
+
55
+ async function readCartWithSubtotal(raw: Raw | null): Promise<Cart> {
56
+ // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.
57
+ if (!raw?.lineItems?.length) return toCart(raw, "", "", imgSrc);
58
+ // The after-discount subtotal and the CART-level discount come from the estimate, never from
59
+ // hand-summing lines. Its delivery/tax/fees read "0" when nothing was calculated — never render
60
+ // those as "Free"; shipping and tax resolve at checkout. POST /ecom/v2/carts/current/estimate
61
+ let totals = { subtotal: "", discount: "" };
62
+ try {
63
+ totals = summaryTotals(await wixRequest<Raw>(`${CART}/estimate`, { body: {} }), raw);
64
+ } catch {
65
+ /* the estimate is a display nicety — the cart itself is still valid */
66
+ }
67
+ const cart = toCart(raw, totals.subtotal, totals.discount, imgSrc);
68
+ await fillLineImages(cart, raw.lineItems as Raw[]);
69
+ return cart;
70
+ }
71
+
72
+ // GET /ecom/v2/carts/current — a 404 (CURRENT_CART_NOT_FOUND / OWNED_CART_NOT_FOUND) means no cart yet.
73
+ async function getCurrentCartRaw(): Promise<Raw | null> {
74
+ try {
75
+ return (await wixRequest<Raw>(CART, { method: "GET" }))?.cart ?? null;
76
+ } catch (e) {
77
+ if (e instanceof WixApiError && e.status === 404) return null;
78
+ throw e;
79
+ }
80
+ }
81
+
82
+ /** The visitor's current cart. An empty Cart (not an error) when none exists yet. */
83
+ export async function fetchCart(): Promise<Cart> {
84
+ return readCartWithSubtotal(await getCurrentCartRaw());
85
+ }
86
+
87
+ /**
88
+ * Add a product. `variantId` is REQUIRED for any product with options — resolve it first
89
+ * (resolveVariant); a refused add still returns 200, so the returned line is checked.
90
+ * POST /ecom/v2/carts/current/add-line-items { catalogItems: [{ quantity, catalogReference: { appId, catalogItemId, options } }] }
91
+ */
92
+ export async function addToCart(productId: string, variantId?: string | null, quantity = 1, extras: AddToCartExtras = {}): Promise<Cart> {
93
+ const options = addOptions({ variantId, ...extras });
94
+ const res = await wixRequest<Raw>(`${CART}/add-line-items`, {
95
+ body: { catalogItems: [{ quantity, catalogReference: { catalogItemId: productId, appId: WIX_STORES_APP_ID, ...(Object.keys(options).length ? { options } : {}) } }] },
96
+ });
97
+ assertAdded(res?.cart, productId, variantId);
98
+ return readCartWithSubtotal(res?.cart ?? null);
99
+ }
100
+
101
+ /**
102
+ * Change a line's quantity. `lineItemId` is CartLine.lineItemId, never the product id; a flat
103
+ * { id, quantity } is a 400. POST /ecom/v2/carts/current/update-line-items { lineItems: [{ lineItemId, quantity: { newQuantity } }] }
104
+ */
105
+ export async function updateQuantity(lineItemId: string, quantity: number): Promise<Cart> {
106
+ const res = await wixRequest<Raw>(`${CART}/update-line-items`, { body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] } });
107
+ return readCartWithSubtotal(res?.cart ?? null);
108
+ }
109
+
110
+ /** Remove a line by CartLine.lineItemId. POST /ecom/v2/carts/current/remove-line-items { lineItemIds } */
111
+ export async function removeLine(lineItemId: string): Promise<Cart> {
112
+ const res = await wixRequest<Raw>(`${CART}/remove-line-items`, { body: { lineItemIds: [lineItemId] } });
113
+ return readCartWithSubtotal(res?.cart ?? null);
114
+ }
115
+
116
+ /**
117
+ * Start the Wix-hosted checkout and return the URL to navigate the FULL document to. The cart's id
118
+ * IS the checkout id. `origin` must be the site's real https origin as registered on the OAuth
119
+ * app's allowed domains (browser: window.location.origin; server: the public https host, never
120
+ * the request's internal origin) — an unlisted or http origin 403s on return.
121
+ * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } }
122
+ */
123
+ export async function checkoutUrl(origin: string = typeof window !== "undefined" ? window.location.origin : ""): Promise<string> {
124
+ const raw = await getCurrentCartRaw();
125
+ assertCheckoutable(raw);
126
+ const res = await wixRequest<Raw>("/headless/v1/redirect-session", {
127
+ body: { ecomCheckout: { checkoutId: rawId(raw) }, callbacks: origin ? { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` } : {} },
128
+ });
129
+ const url = res?.redirectSession?.fullUrl;
130
+ if (!url) throw new Error("Checkout couldn't start: no redirect URL returned.");
131
+ return url;
132
+ }