@mohasinac/appkit 4.11.2 → 4.12.0

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 (246) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +40 -3
  2. package/dist/_internal/server/features/checkout/actions.js +232 -47
  3. package/dist/_internal/server/features/checkout/data.d.ts +10 -0
  4. package/dist/_internal/server/features/orders/adapters.js +20 -0
  5. package/dist/_internal/server/features/products/data.d.ts +12 -3
  6. package/dist/_internal/server/features/products/data.js +55 -4
  7. package/dist/_internal/server/features/products/index.d.ts +1 -1
  8. package/dist/_internal/server/features/products/index.js +1 -1
  9. package/dist/_internal/server/features/products/list-public.d.ts +47 -0
  10. package/dist/_internal/server/features/products/list-public.js +131 -6
  11. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  12. package/dist/_internal/server/features/reviews/data.js +3 -2
  13. package/dist/_internal/server/features/tester/visibility.js +3 -3
  14. package/dist/_internal/server/jobs/core/auctionSettlement.js +5 -2
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -1
  16. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
  17. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +2 -0
  18. package/dist/_internal/server/jobs/handlers/_helpers.js +4 -1
  19. package/dist/_internal/shared/checkout/lanes.d.ts +20 -0
  20. package/dist/_internal/shared/checkout/lanes.js +28 -0
  21. package/dist/_internal/shared/features/checkout/config.d.ts +4 -0
  22. package/dist/_internal/shared/features/checkout/config.js +8 -0
  23. package/dist/_internal/shared/features/promotions/schema.d.ts +0 -16
  24. package/dist/_internal/shared/features/promotions/schema.js +0 -2
  25. package/dist/_internal/shared/features/reviews/config.d.ts +8 -1
  26. package/dist/_internal/shared/features/reviews/config.js +8 -1
  27. package/dist/_internal/shared/fees/calculator.d.ts +34 -2
  28. package/dist/_internal/shared/fees/calculator.js +45 -1
  29. package/dist/_internal/shared/listing-types/_registry.d.ts +88 -1
  30. package/dist/_internal/shared/listing-types/_registry.js +63 -1
  31. package/dist/_internal/shared/listing-types/art/config.d.ts +59 -0
  32. package/dist/_internal/shared/listing-types/art/config.js +12 -0
  33. package/dist/_internal/shared/listing-types/auction/config.d.ts +53 -0
  34. package/dist/_internal/shared/listing-types/auction/config.js +17 -0
  35. package/dist/_internal/shared/listing-types/classified/config.d.ts +59 -0
  36. package/dist/_internal/shared/listing-types/classified/config.js +14 -0
  37. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +59 -0
  38. package/dist/_internal/shared/listing-types/digital-code/config.js +12 -0
  39. package/dist/_internal/shared/listing-types/live/config.d.ts +59 -0
  40. package/dist/_internal/shared/listing-types/live/config.js +14 -0
  41. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +110 -0
  42. package/dist/_internal/shared/listing-types/pre-order/config.js +14 -0
  43. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +44 -0
  44. package/dist/_internal/shared/listing-types/prize-draw/config.js +10 -0
  45. package/dist/_internal/shared/listing-types/standard/config.d.ts +59 -0
  46. package/dist/_internal/shared/listing-types/standard/config.js +9 -0
  47. package/dist/_internal/shared/listing-types/stickers/config.d.ts +59 -0
  48. package/dist/_internal/shared/listing-types/stickers/config.js +12 -0
  49. package/dist/client.d.ts +12 -2
  50. package/dist/client.js +8 -2
  51. package/dist/constants/api-endpoints.d.ts +6 -0
  52. package/dist/constants/api-endpoints.js +2 -0
  53. package/dist/constants/field-names.d.ts +12 -0
  54. package/dist/constants/field-names.js +12 -0
  55. package/dist/constants/table-keys.d.ts +2 -0
  56. package/dist/constants/table-keys.js +2 -0
  57. package/dist/features/account/components/UserOrdersView.js +27 -5
  58. package/dist/features/admin/components/AdminAddressesView.js +21 -11
  59. package/dist/features/admin/components/AdminArtView.d.ts +9 -0
  60. package/dist/features/admin/components/AdminArtView.js +11 -35
  61. package/dist/features/admin/components/AdminClassifiedView.d.ts +9 -0
  62. package/dist/features/admin/components/AdminClassifiedView.js +11 -35
  63. package/dist/features/admin/components/AdminCouponEditorView.js +1 -4
  64. package/dist/features/admin/components/AdminDigitalCodesView.d.ts +9 -0
  65. package/dist/features/admin/components/AdminDigitalCodesView.js +11 -42
  66. package/dist/features/admin/components/AdminLiveView.d.ts +9 -0
  67. package/dist/features/admin/components/AdminLiveView.js +11 -41
  68. package/dist/features/admin/components/AdminOrderEditorView.d.ts +8 -1
  69. package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
  70. package/dist/features/admin/components/AdminOrdersView.js +14 -1
  71. package/dist/features/admin/components/AdminPaymentMethodsView.js +18 -11
  72. package/dist/features/admin/components/AdminProductsView.js +34 -18
  73. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  74. package/dist/features/admin/components/AdminSiteSettingsView.js +4 -2
  75. package/dist/features/admin/components/AdminStickersView.d.ts +9 -0
  76. package/dist/features/admin/components/AdminStickersView.js +11 -35
  77. package/dist/features/admin/constants/filter-tabs.d.ts +26 -9
  78. package/dist/features/admin/constants/filter-tabs.js +22 -9
  79. package/dist/features/admin/hooks/useAdminListing.js +7 -2
  80. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  81. package/dist/features/admin/schemas/firestore.js +1 -0
  82. package/dist/features/auctions/actions/bid-actions.js +2 -2
  83. package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
  84. package/dist/features/auctions/components/AuctionBottomActions.js +1 -0
  85. package/dist/features/auctions/components/AuctionDetailPageView.js +13 -11
  86. package/dist/features/cart/components/CartDrawer.d.ts +9 -1
  87. package/dist/features/cart/components/CartDrawer.js +4 -3
  88. package/dist/features/cart/components/CartPriceBreakdown.d.ts +61 -0
  89. package/dist/features/cart/components/CartPriceBreakdown.js +25 -0
  90. package/dist/features/cart/components/StoreAddonsPicker.d.ts +47 -0
  91. package/dist/features/cart/components/StoreAddonsPicker.js +21 -0
  92. package/dist/features/cart/components/index.d.ts +4 -0
  93. package/dist/features/cart/components/index.js +2 -0
  94. package/dist/features/cart/repository/cart.repository.d.ts +20 -2
  95. package/dist/features/cart/repository/cart.repository.js +49 -0
  96. package/dist/features/cart/schemas/firestore.d.ts +26 -2
  97. package/dist/features/categories/components/BrandDetailTabs.js +31 -11
  98. package/dist/features/categories/components/CategoryDetailTabs.js +27 -7
  99. package/dist/features/categories/components/CategoryProductsListing.d.ts +9 -1
  100. package/dist/features/categories/components/CategoryProductsListing.js +5 -2
  101. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -1
  102. package/dist/features/contact/email.js +5 -1
  103. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +17 -16
  104. package/dist/features/homepage/components/FeaturedResultsSection.js +1 -1
  105. package/dist/features/homepage/components/SectionCarousel.js +1 -1
  106. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  107. package/dist/features/layout/AppLayoutShell.js +9 -2
  108. package/dist/features/layout/BottomActions.js +62 -6
  109. package/dist/features/layout/BottomActionsContext.d.ts +26 -0
  110. package/dist/features/layout/BottomActionsContext.js +18 -1
  111. package/dist/features/layout/hooks/useBottomActions.d.ts +12 -1
  112. package/dist/features/layout/hooks/useBottomActions.js +14 -1
  113. package/dist/features/layout/index.d.ts +1 -1
  114. package/dist/features/live/components/LiveItemDetailPageView.js +7 -6
  115. package/dist/features/orders/components/OrderAddonBadges.d.ts +36 -0
  116. package/dist/features/orders/components/OrderAddonBadges.js +28 -0
  117. package/dist/features/orders/components/index.d.ts +1 -0
  118. package/dist/features/orders/components/index.js +1 -0
  119. package/dist/features/orders/repository/orders.repository.d.ts +36 -0
  120. package/dist/features/orders/repository/orders.repository.js +18 -0
  121. package/dist/features/orders/types/index.d.ts +38 -0
  122. package/dist/features/pre-orders/components/PreOrderBottomActions.d.ts +1 -1
  123. package/dist/features/pre-orders/components/PreOrderBottomActions.js +3 -19
  124. package/dist/features/products/components/ArtStickersListView.js +5 -4
  125. package/dist/features/products/components/AuctionsIndexListing.js +1 -2
  126. package/dist/features/products/components/ListingBottomActions.d.ts +28 -0
  127. package/dist/features/products/components/ListingBottomActions.js +25 -0
  128. package/dist/features/products/components/PrizeDrawBottomActions.d.ts +1 -1
  129. package/dist/features/products/components/PrizeDrawBottomActions.js +3 -21
  130. package/dist/features/products/components/PrizeDrawsListingView.js +18 -33
  131. package/dist/features/products/components/ProductDetailActions.js +4 -0
  132. package/dist/features/products/components/ProductDetailPageView.js +7 -7
  133. package/dist/features/products/components/ProductsIndexListing.js +87 -11
  134. package/dist/features/products/components/ProductsIndexPageView.js +14 -2
  135. package/dist/features/products/config/listing-type-listing-config.d.ts +31 -0
  136. package/dist/features/products/config/listing-type-listing-config.js +74 -0
  137. package/dist/features/products/constants/listing-tabs.d.ts +43 -198
  138. package/dist/features/products/constants/listing-tabs.js +71 -58
  139. package/dist/features/products/constants/sieve.d.ts +104 -1
  140. package/dist/features/products/constants/sieve.js +27 -7
  141. package/dist/features/products/hooks/useProducts.js +14 -0
  142. package/dist/features/products/repository/products.repository.d.ts +45 -1
  143. package/dist/features/products/repository/products.repository.js +26 -3
  144. package/dist/features/products/types/index.d.ts +15 -0
  145. package/dist/features/products/utils/listing-badge-variant.d.ts +20 -1
  146. package/dist/features/products/utils/listing-badge-variant.js +26 -1
  147. package/dist/features/products/utils/listing-type.d.ts +24 -0
  148. package/dist/features/products/utils/listing-type.js +48 -0
  149. package/dist/features/promotions/actions/coupon-actions.d.ts +7 -0
  150. package/dist/features/promotions/actions/coupon-actions.js +12 -1
  151. package/dist/features/promotions/actions/coupon-stacking.d.ts +24 -0
  152. package/dist/features/promotions/actions/coupon-stacking.js +21 -0
  153. package/dist/features/promotions/actions/index.d.ts +1 -0
  154. package/dist/features/promotions/actions/index.js +1 -0
  155. package/dist/features/promotions/actions/seller-coupon-actions.d.ts +0 -1
  156. package/dist/features/promotions/actions/seller-coupon-actions.js +0 -1
  157. package/dist/features/promotions/components/CouponHelpDetails.d.ts +20 -0
  158. package/dist/features/promotions/components/CouponHelpDetails.js +17 -0
  159. package/dist/features/promotions/components/CouponsIndexListing.js +2 -1
  160. package/dist/features/promotions/components/index.d.ts +2 -0
  161. package/dist/features/promotions/components/index.js +1 -0
  162. package/dist/features/promotions/constants/coupon-help.d.ts +27 -0
  163. package/dist/features/promotions/constants/coupon-help.js +42 -0
  164. package/dist/features/promotions/repository/coupons.repository.d.ts +10 -1
  165. package/dist/features/promotions/repository/coupons.repository.js +22 -8
  166. package/dist/features/promotions/schemas/firestore.d.ts +0 -1
  167. package/dist/features/promotions/schemas/firestore.js +1 -1
  168. package/dist/features/promotions/schemas/index.d.ts +0 -20
  169. package/dist/features/promotions/schemas/index.js +0 -1
  170. package/dist/features/reviews/actions/review-actions.js +5 -1
  171. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -1
  172. package/dist/features/reviews/components/ReviewFilters.d.ts +7 -1
  173. package/dist/features/reviews/components/ReviewFilters.js +4 -3
  174. package/dist/features/reviews/components/ReviewsIndexListing.d.ts +6 -0
  175. package/dist/features/reviews/components/ReviewsIndexListing.js +9 -109
  176. package/dist/features/reviews/components/ReviewsListingPanel.d.ts +41 -0
  177. package/dist/features/reviews/components/ReviewsListingPanel.js +226 -0
  178. package/dist/features/reviews/components/index.d.ts +2 -0
  179. package/dist/features/reviews/components/index.js +1 -0
  180. package/dist/features/reviews/hooks/useReviews.d.ts +8 -0
  181. package/dist/features/reviews/hooks/useReviews.js +14 -6
  182. package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
  183. package/dist/features/reviews/repository/reviews.repository.js +77 -41
  184. package/dist/features/reviews/types/index.d.ts +8 -1
  185. package/dist/features/seller/components/SellerOrdersView.js +2 -1
  186. package/dist/features/seller/components/SellerProductsCards.js +2 -2
  187. package/dist/features/seller/components/SellerProductsView.js +21 -18
  188. package/dist/features/stores/actions/store-query-actions.js +1 -1
  189. package/dist/features/stores/components/StoreArtStickersPageView.d.ts +21 -0
  190. package/dist/features/stores/components/StoreArtStickersPageView.js +30 -0
  191. package/dist/features/stores/components/StoreAuctionsListing.js +5 -11
  192. package/dist/features/stores/components/StoreAuctionsPageView.d.ts +4 -1
  193. package/dist/features/stores/components/StoreAuctionsPageView.js +17 -17
  194. package/dist/features/stores/components/StoreClassifiedsListing.js +7 -0
  195. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -1
  196. package/dist/features/stores/components/StoreClassifiedsPageView.js +16 -12
  197. package/dist/features/stores/components/StoreDetailLayoutView.js +4 -3
  198. package/dist/features/stores/components/StoreDigitalCodesListing.js +5 -0
  199. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -1
  200. package/dist/features/stores/components/StoreDigitalCodesPageView.js +16 -12
  201. package/dist/features/stores/components/StoreLiveItemsListing.js +7 -0
  202. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -1
  203. package/dist/features/stores/components/StoreLiveItemsPageView.js +16 -12
  204. package/dist/features/stores/components/StorePreOrdersListing.js +6 -11
  205. package/dist/features/stores/components/StorePreOrdersPageView.d.ts +4 -1
  206. package/dist/features/stores/components/StorePreOrdersPageView.js +17 -17
  207. package/dist/features/stores/components/StorePrizeDrawsPageView.d.ts +4 -8
  208. package/dist/features/stores/components/StorePrizeDrawsPageView.js +17 -22
  209. package/dist/features/stores/components/StoreProductsListing.d.ts +7 -1
  210. package/dist/features/stores/components/StoreProductsListing.js +3 -2
  211. package/dist/features/stores/components/StoreProductsPageView.d.ts +1 -1
  212. package/dist/features/stores/components/StoreProductsPageView.js +16 -21
  213. package/dist/features/stores/components/StoreReviewsListing.d.ts +4 -0
  214. package/dist/features/stores/components/StoreReviewsListing.js +7 -108
  215. package/dist/features/stores/schemas/index.d.ts +2 -2
  216. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +3 -3
  217. package/dist/features/tester/seed-data/index.d.ts +1 -0
  218. package/dist/features/tester/seed-data/index.js +1 -0
  219. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +2 -0
  220. package/dist/features/tester/seed-data/offers-tester-seed-data.js +61 -0
  221. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +939 -1
  222. package/dist/index.d.ts +9 -3
  223. package/dist/index.js +11 -2
  224. package/dist/next/routing/route-map.d.ts +2 -0
  225. package/dist/next/routing/route-map.js +5 -0
  226. package/dist/react/hooks/useListingTypeFlags.d.ts +12 -19
  227. package/dist/react/hooks/useListingTypeFlags.js +4 -9
  228. package/dist/schemas/registry.d.ts +0 -12
  229. package/dist/seed/claimed-coupons-seed-data.js +3 -3
  230. package/dist/seed/coupons-seed-data.js +29 -10
  231. package/dist/seed/faq-seed-data.js +2 -2
  232. package/dist/seed/manifest.js +2 -2
  233. package/dist/seed/reviews-seed-data.js +52 -0
  234. package/dist/server-entry.d.ts +1 -0
  235. package/dist/server-entry.js +1 -0
  236. package/dist/styles.css +55 -39
  237. package/dist/tailwind-utilities.css +1 -1
  238. package/dist/ui/components/Checkbox.d.ts +10 -1
  239. package/dist/ui/components/Checkbox.js +14 -2
  240. package/dist/ui/components/Checkbox.style.css +11 -0
  241. package/dist/ui/components/FilterChipGroup.d.ts +21 -3
  242. package/dist/ui/components/FilterChipGroup.js +33 -4
  243. package/dist/ui/components/HorizontalScroller.d.ts +1 -2
  244. package/dist/ui/components/HorizontalScroller.js +9 -5
  245. package/dist/ui/components/HorizontalScroller.style.css +43 -38
  246. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  export { OrderCard, OrdersList } from "./OrdersList";
2
+ export { OrderAddonBadges, } from "./OrderAddonBadges";
2
3
  export { MarketplaceOrderCard, } from "./MarketplaceOrderCard";
3
4
  export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
4
5
  export { OrderSiblingPayments } from "./OrderSiblingPayments";
@@ -101,6 +101,22 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
101
101
  canFilter: boolean;
102
102
  canSort: boolean;
103
103
  };
104
+ whatsappNotifyAddon: {
105
+ canFilter: boolean;
106
+ canSort: boolean;
107
+ };
108
+ giftWrapAddon: {
109
+ canFilter: boolean;
110
+ canSort: boolean;
111
+ };
112
+ shipmentProtectionAddon: {
113
+ canFilter: boolean;
114
+ canSort: boolean;
115
+ };
116
+ couponCode: {
117
+ canFilter: boolean;
118
+ canSort: boolean;
119
+ };
104
120
  };
105
121
  listForSeller(productIds: string[], model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
106
122
  listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
@@ -175,6 +191,26 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
175
191
  canFilter: boolean;
176
192
  canSort: boolean;
177
193
  };
194
+ orderType: {
195
+ canFilter: boolean;
196
+ canSort: boolean;
197
+ };
198
+ whatsappNotifyAddon: {
199
+ canFilter: boolean;
200
+ canSort: boolean;
201
+ };
202
+ giftWrapAddon: {
203
+ canFilter: boolean;
204
+ canSort: boolean;
205
+ };
206
+ shipmentProtectionAddon: {
207
+ canFilter: boolean;
208
+ canSort: boolean;
209
+ };
210
+ couponCode: {
211
+ canFilter: boolean;
212
+ canSort: boolean;
213
+ };
178
214
  };
179
215
  listAll(model: SieveModel): Promise<FirebaseSieveResult<OrderDocument>>;
180
216
  /**
@@ -496,6 +496,13 @@ OrderRepository.SELLER_SIEVE_FIELDS = {
496
496
  orderDate: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
497
497
  createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
498
498
  contestable: { canFilter: true, canSort: false },
499
+ // Fulfilment flags. Filterable so "which of my orders need gift wrap?" is a
500
+ // query against the orders themselves — the whole point of recording the
501
+ // add-on on the order rather than keeping a side list of who asked for what.
502
+ whatsappNotifyAddon: { canFilter: true, canSort: false },
503
+ giftWrapAddon: { canFilter: true, canSort: false },
504
+ shipmentProtectionAddon: { canFilter: true, canSort: false },
505
+ couponCode: { canFilter: true, canSort: false },
499
506
  };
500
507
  OrderRepository.ADMIN_SIEVE_FIELDS = {
501
508
  id: { canFilter: true, canSort: false },
@@ -516,6 +523,17 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
516
523
  // S-SBUNI-RULES 2026-05-13 — refund + batch fields
517
524
  paymentBatchId: { canFilter: true, canSort: false },
518
525
  contestable: { canFilter: true, canSort: false },
526
+ // Lane segregation on /user/orders — "standard"/"auction"/"offer" etc.
527
+ // Never filtered as "standard" via Firestore == (see listForUser callers):
528
+ // orders written before this field existed have no value to match.
529
+ orderType: { canFilter: true, canSort: false },
530
+ // Same fulfilment flags as SELLER_SIEVE_FIELDS — admin needs to answer
531
+ // "who opted into WhatsApp updates?" when a status change goes out, without
532
+ // maintaining a parallel tracker.
533
+ whatsappNotifyAddon: { canFilter: true, canSort: false },
534
+ giftWrapAddon: { canFilter: true, canSort: false },
535
+ shipmentProtectionAddon: { canFilter: true, canSort: false },
536
+ couponCode: { canFilter: true, canSort: false },
519
537
  };
520
538
  const orderRepository = new OrderRepository();
521
539
  export { OrderRepository, orderRepository };
@@ -1,6 +1,8 @@
1
1
  import { type PaymentGateway, PaymentGatewayValues } from "../../payments/schemas";
2
2
  export { type PaymentGateway, PaymentGatewayValues };
3
3
  import type { ListingType } from "../../products/types/index";
4
+ import type { OrderType } from "../utils/order-splitter";
5
+ import type { AppliedOrderDiscount } from "../schemas/firestore";
4
6
  export type PaymentStatus = "pending" | "processing" | "paid" | "failed" | "refunded" | "partial_refund";
5
7
  export interface UserAddress {
6
8
  id: string;
@@ -57,6 +59,13 @@ export interface Order {
57
59
  address: UserAddress;
58
60
  orderStatus: OrderStatus;
59
61
  paymentStatus: PaymentStatus;
62
+ /**
63
+ * Which checkout lane produced this order. Absent on orders written before
64
+ * the field existed — treat missing as `"standard"`, never filter it out.
65
+ */
66
+ orderType?: OrderType;
67
+ /** Set when this order settled an accepted Make-an-Offer. */
68
+ offerId?: string;
60
69
  paymentGateway?: PaymentGateway;
61
70
  subtotal: number;
62
71
  shippingCost?: number;
@@ -64,7 +73,36 @@ export interface Order {
64
73
  tax?: number;
65
74
  total: number;
66
75
  currency: string;
76
+ /**
77
+ * First applied coupon's code only. Kept for back-compat; prefer
78
+ * `appliedDiscounts` when rendering, since a cart may stack one coupon per
79
+ * store plus one platform-wide coupon and this scalar shows only one of them.
80
+ */
67
81
  couponCode?: string;
82
+ /** Total rupee value of every discount applied to this order. */
83
+ couponDiscount?: number;
84
+ /** Every discount applied to this order — the full stack, not just the first. */
85
+ appliedDiscounts?: AppliedOrderDiscount[];
86
+ /**
87
+ * Paid add-ons the buyer opted into for THIS order (= this store).
88
+ *
89
+ * These are operational flags, not just billing records: `whatsappNotifyAddon`
90
+ * is what the status-change notifier checks to decide whether to send a
91
+ * WhatsApp message, and `giftWrapAddon`/`shipmentProtectionAddon` are what
92
+ * the packer needs to see before dispatch. They live on the order itself and
93
+ * are filterable (`orders` SIEVE_FIELDS) precisely so nobody has to keep a
94
+ * separate list of who asked for what.
95
+ */
96
+ whatsappNotifyAddon?: boolean;
97
+ whatsappNotifyFee?: number;
98
+ giftWrapAddon?: boolean;
99
+ giftWrapFee?: number;
100
+ /** The buyer's gift message — the packer has to physically include this. */
101
+ giftWrapMessage?: string;
102
+ shipmentProtectionAddon?: boolean;
103
+ shipmentProtectionFee?: number;
104
+ /** Buyer-facing platform commission apportioned to this order. */
105
+ platformFee?: number;
68
106
  trackingNumber?: string;
69
107
  shippingCarrier?: string;
70
108
  /** Carrier tracking page URL — rendered as an external link that opens in a new tab. No live-tracking API integration; this is just the passthrough URL a seller/admin entered. */
@@ -2,4 +2,4 @@ export interface PreOrderBottomActionsProps {
2
2
  price: number | null;
3
3
  currency: string;
4
4
  }
5
- export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): null;
5
+ export declare function PreOrderBottomActions({ price, currency, }: PreOrderBottomActionsProps): import("react").JSX.Element;
@@ -1,22 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../../products/constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "../../products/components/ListingBottomActions";
5
4
  export function PreOrderBottomActions({ price, currency, }) {
6
- useBottomActions({
7
- actions: [
8
- {
9
- id: ACTION_ID.RESERVE_NOW,
10
- label: "Reserve Now",
11
- variant: "primary",
12
- onClick: () => {
13
- document
14
- .getElementById("pre-order-buy-bar")
15
- ?.scrollIntoView({ behavior: "smooth" });
16
- },
17
- },
18
- ],
19
- infoLabel: price !== null ? formatCurrency(price, currency) : undefined,
20
- });
21
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "preorder", anchorId: "pre-order-buy-bar", price: price, currency: currency }));
22
6
  }
@@ -4,9 +4,8 @@ import { AdSlot } from "../../homepage/components/AdSlot";
4
4
  import { ProductsIndexListing } from "./ProductsIndexListing";
5
5
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
6
6
  import { sortBy } from "../../../constants/sort";
7
- import { ART_STICKERS_TYPE_FILTER_TABS } from "../constants/listing-tabs";
8
- import { listPublicProducts, parsePublicProductParams, } from "../../../_internal/server/features/products/list-public";
9
- const ART_STICKERS_LISTING_TYPES = ["art", "stickers"];
7
+ import { ART_STICKERS_TYPE_FILTER_TABS, ART_STICKERS_LISTING_TYPES, } from "../constants/listing-tabs";
8
+ import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
10
9
  const DEFAULT_PAGE_SIZE = 24;
11
10
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
12
11
  export async function ArtStickersListView({ searchParams = {} }) {
@@ -17,7 +16,9 @@ export async function ArtStickersListView({ searchParams = {} }) {
17
16
  listingTypes: ART_STICKERS_LISTING_TYPES,
18
17
  pageSize: DEFAULT_PAGE_SIZE,
19
18
  sorts: DEFAULT_SORT,
20
- hideSoldByDefault: true,
19
+ // Derived, so this page and ProductsIndexListing can never disagree
20
+ // about the default view (Root Cause #30).
21
+ ...defaultTogglesForListingTypes(ART_STICKERS_LISTING_TYPES),
21
22
  }));
22
23
  return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Art & Stickers" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Original art prints and sticker packs from independent sellers" })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: result ?? null, listingTypes: ART_STICKERS_LISTING_TYPES, typeTabs: ART_STICKERS_TYPE_FILTER_TABS, searchPlaceholder: "Search art & stickers..." }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
23
24
  }
@@ -24,7 +24,6 @@ const __O = {
24
24
  hidden: "overflow-hidden",
25
25
  };
26
26
  const DEFAULT_SORT = AUCTION_PUBLIC_SORT_OPTIONS[0].value;
27
- const AUCTION_SORT_OPTIONS = AUCTION_PUBLIC_SORT_OPTIONS;
28
27
  const FILTER_KEYS = [TABLE_KEYS.CATEGORY, TABLE_KEYS.BRAND, TABLE_KEYS.MIN_BID, TABLE_KEYS.MAX_BID, TABLE_KEYS.STORE_ID, TABLE_KEYS.DATE_FROM, TABLE_KEYS.DATE_TO];
29
28
  export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
30
29
  const table = useUrlTable({ defaults: { pageSize: "24", sort: DEFAULT_SORT } });
@@ -132,7 +131,7 @@ export function AuctionsIndexListing({ initialData, categorySlug, brandName }) {
132
131
  },
133
132
  },
134
133
  ] } } : {});
135
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
134
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search auctions...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: AUCTION_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: auctions.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
136
135
  { label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") },
137
136
  ] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
138
137
  {
@@ -0,0 +1,28 @@
1
+ import { MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
2
+ /**
3
+ * The sticky-CTA registrar shared by every listing type whose bottom bar is just
4
+ * "primary CTA that scrolls to the buy/contact panel, plus a price label".
5
+ *
6
+ * Pre-order, prize-draw, classified, digital-code and live all fit that shape, which put
7
+ * it well past the Rule of Three for a per-type copy. Auction and standard product keep
8
+ * their own registrars — the auction ticks a live countdown into `secondaryLabel`, and
9
+ * the product bar carries three buttons whose labels track cart/wishlist state.
10
+ *
11
+ * Renders nothing; it exists purely to register into `BottomActionsContext`.
12
+ */
13
+ export interface ListingBottomActionsProps {
14
+ /** Selects the CTA set from the ACTIONS registry (Rule #7 — never inline actions). */
15
+ listingType: keyof typeof MOBILE_PRIMARY_ACTIONS;
16
+ /** `id` of the element the CTA scrolls to. */
17
+ anchorId: string;
18
+ price: number | null;
19
+ currency: string;
20
+ /**
21
+ * Register nothing at all — for ended/closed/sold-out listings, matching how the
22
+ * auction and prize-draw registrars bail out.
23
+ */
24
+ unavailable?: boolean;
25
+ /** Appended after the formatted price, e.g. "per entry". */
26
+ infoSuffix?: string;
27
+ }
28
+ export declare function ListingBottomActions({ listingType, anchorId, price, currency, unavailable, infoSuffix, }: ListingBottomActionsProps): null;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
+ import { formatCurrency } from "../../../utils/number.formatter";
4
+ import { ACTION_META, MOBILE_PRIMARY_ACTIONS } from "../constants/action-defs";
5
+ export function ListingBottomActions({ listingType, anchorId, price, currency, unavailable = false, infoSuffix, }) {
6
+ const actionIds = MOBILE_PRIMARY_ACTIONS[listingType] ?? [];
7
+ const infoLabel = price !== null
8
+ ? `${formatCurrency(price, currency)}${infoSuffix ? ` ${infoSuffix}` : ""}`
9
+ : undefined;
10
+ useBottomActions(unavailable
11
+ ? {}
12
+ : {
13
+ actions: actionIds.map((id) => ({
14
+ ...ACTION_META[id],
15
+ onClick: () => {
16
+ document
17
+ .getElementById(anchorId)
18
+ ?.scrollIntoView({ behavior: "smooth" });
19
+ },
20
+ })),
21
+ infoLabel,
22
+ desktop: "after-scroll",
23
+ });
24
+ return null;
25
+ }
@@ -3,4 +3,4 @@ export interface PrizeDrawBottomActionsProps {
3
3
  currency: string;
4
4
  closed: boolean;
5
5
  }
6
- export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): null;
6
+ export declare function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }: PrizeDrawBottomActionsProps): import("react").JSX.Element;
@@ -1,24 +1,6 @@
1
1
  "use client";
2
- import { useBottomActions } from "../../layout/hooks/useBottomActions";
3
- import { formatCurrency } from "../../../utils/number.formatter";
4
- import { ACTION_ID } from "../constants/action-defs";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ListingBottomActions } from "./ListingBottomActions";
5
4
  export function PrizeDrawBottomActions({ pricePerEntry, currency, closed, }) {
6
- useBottomActions(closed
7
- ? {}
8
- : {
9
- actions: [
10
- {
11
- id: ACTION_ID.BUY_NOW,
12
- label: "Buy Now",
13
- variant: "primary",
14
- onClick: () => {
15
- document
16
- .getElementById("prize-draw-buy-bar")
17
- ?.scrollIntoView({ behavior: "smooth" });
18
- },
19
- },
20
- ],
21
- infoLabel: `${formatCurrency(pricePerEntry, currency)} per entry`,
22
- });
23
- return null;
5
+ return (_jsx(ListingBottomActions, { listingType: "prize-draw", anchorId: "prize-draw-buy-bar", price: pricePerEntry, currency: currency, unavailable: closed, infoSuffix: "per entry" }));
24
6
  }
@@ -1,33 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
- import { productRepository } from "../../../repositories";
4
2
  import { Container, Main, Heading, Section, Text } from "../../../ui";
5
3
  import { AdSlot } from "../../homepage/components/AdSlot";
6
- import { parseListingSearchParams } from "../../../utils/listing-params";
4
+ import { PRODUCT_FIELDS } from "../../../constants/field-names";
5
+ import { sortBy } from "../../../constants/sort";
6
+ import { listPublicProducts, parsePublicProductParams, defaultTogglesForListingTypes, } from "../../../_internal/server/features/products/list-public";
7
7
  import { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
8
- const DEFAULT_PAGE = 1;
8
+ const LISTING_TYPES = ["prize-draw"];
9
9
  const DEFAULT_PAGE_SIZE = 24;
10
- const DEFAULT_SORT = "-createdAt";
11
- function sp(params, key) {
12
- const v = params[key];
13
- return Array.isArray(v) ? v[0] ?? "" : v ?? "";
14
- }
15
- function buildPrizeDrawFilters(params) {
16
- const parts = ["status==published", "listingType==prize-draw"];
17
- const minPrice = sp(params, "minPrice");
18
- const maxPrice = sp(params, "maxPrice");
19
- if (minPrice)
20
- parts.push(sieveFilter("pricePerEntry", SIEVE_OP.GTE, minPrice));
21
- if (maxPrice)
22
- parts.push(sieveFilter("pricePerEntry", SIEVE_OP.LTE, maxPrice));
23
- const store = sp(params, "storeId");
24
- if (store)
25
- parts.push(sieveFilter("storeId", SIEVE_OP.EQ, store));
26
- const status = sp(params, "prizeRevealStatus");
27
- if (status)
28
- parts.push(sieveFilter("prizeRevealStatus", SIEVE_OP.EQ, status));
29
- return parts.join(",");
30
- }
10
+ const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
31
11
  /**
32
12
  * Public listing page (SB4-F). Server-fetches published prize-draw products,
33
13
  * hydrates the client `PrizeDrawsIndexListing` which renders the filter
@@ -38,13 +18,18 @@ function buildPrizeDrawFilters(params) {
38
18
  * already gone (matches `PrizeDrawCollage`'s `hideWonState` prop).
39
19
  */
40
20
  export async function PrizeDrawsListingView({ searchParams = {}, }) {
41
- const std = parseListingSearchParams(searchParams);
42
- const sort = std.sorts ?? DEFAULT_SORT;
43
- const page = std.page ?? DEFAULT_PAGE;
44
- const pageSize = std.pageSize ?? DEFAULT_PAGE_SIZE;
45
- const filters = buildPrizeDrawFilters(searchParams);
46
- const result = await productRepository
47
- .list({ filters, sorts: sort, page, pageSize })
48
- .catch(() => null);
21
+ // Routed through the shared query (2026-08-21). The hand-rolled builder this
22
+ // replaces had its own Root Cause #30 divergence: it applied the price range
23
+ // to `pricePerEntry`, while the client's refetch sends minPrice/maxPrice,
24
+ // which the shared query applies to `price`. Both fields carry the same
25
+ // value today, so the split was invisible — until one of them drifted.
26
+ // `price` wins because it is also what PRIZE_DRAW_SORT_OPTIONS sorts by for
27
+ // "Entry: Low to High".
28
+ const result = await listPublicProducts(parsePublicProductParams(searchParams, {
29
+ listingTypes: LISTING_TYPES,
30
+ pageSize: DEFAULT_PAGE_SIZE,
31
+ sorts: DEFAULT_SORT,
32
+ ...defaultTogglesForListingTypes(LISTING_TYPES),
33
+ }));
49
34
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-2xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2", color: "primary", size: "3xl", weight: "semibold", children: "Prize Draws" }), _jsx(Text, { className: "mb-6 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Fair-RNG draws for sealed Pok\u00E9mon, Hot Wheels Super Treasure Hunts, Gundam kits and more. Every winner picked by crypto.randomInt \u2014 proof on GitHub." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(PrizeDrawsIndexListing, { initialData: result ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
50
35
  }
@@ -122,6 +122,10 @@ export function ProductDetailActions({ productId, productTitle, productImage, pr
122
122
  infoLabel: price != null && currency
123
123
  ? formatCurrency(price, currency)
124
124
  : undefined,
125
+ // The bar itself owns responsive visibility now, so this one registrar serves
126
+ // both breakpoints. The desktop branch below must NOT also register —
127
+ // useBottomActions is last-mounted-wins and the two would clobber each other.
128
+ desktop: "after-scroll",
125
129
  }
126
130
  : {});
127
131
  const authModal = (_jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { productRepository, reviewRepository } from "../../../repositories";
3
+ import { productRepository } from "../../../repositories";
4
4
  const __P = {
5
5
  p3: "p-[var(--appkit-space-3)]",
6
6
  p5: "p-[var(--appkit-space-5)]",
@@ -25,7 +25,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
25
25
  import { Button, Container, Div, Heading, Main, Nav, PriceDisplay, RichText, Row, Section, Span, Stack, Text, Ul, Ol, Li, Dl, Dt, Dd, } from "../../../ui";
26
26
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
27
27
  import { safeDisplayName } from "../../../security";
28
- import { ReviewsList } from "../../reviews/components/ReviewsList";
28
+ import { ReviewsListingPanel } from "../../reviews/components/ReviewsListingPanel";
29
29
  import { ProductDetailView } from "./ProductDetailView";
30
30
  import { ProductGalleryClient } from "./ProductGalleryClient";
31
31
  import { ProductTabsShell } from "./ProductTabsShell";
@@ -37,7 +37,7 @@ import { SublistingCarouselSection } from "./SublistingCarouselSection";
37
37
  import { ShowGroupSection } from "./ShowGroupSection";
38
38
  import { HistoryTracker } from "../../history/components/HistoryTracker";
39
39
  import { RelatedItemsSection } from "./RelatedItemsSection";
40
- import { computeRelatedItems, toReview } from "../../../_internal/server/features/products/data";
40
+ import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal/server/features/products/data";
41
41
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
42
42
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
43
43
  // ---------------------------------------------------------------------------
@@ -165,12 +165,12 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
165
165
  const groupTitle = typeof p.groupTitle === "string" ? p.groupTitle : undefined;
166
166
  const storeId = typeof p.storeId === "string" ? p.storeId : null;
167
167
  // -- Fetch reviews + the shared 4-signal related-items computation + grouped listings in parallel --
168
- const [reviewDocs, related, groups] = await Promise.all([
169
- reviewRepository.findApprovedByProduct(product.id).catch(() => []),
168
+ // Only page 1 of reviews is fetched here; <ReviewsListingPanel> pages the rest client-side.
169
+ const [initialReviews, related, groups] = await Promise.all([
170
+ getReviewPageForProduct(product.id),
170
171
  computeRelatedItems(product),
171
172
  getGroupsWithItemsForProduct(product.id),
172
173
  ]);
173
- const reviews = reviewDocs.map(toReview);
174
174
  const { relatedItems, relatedByBrand, relatedByTags, relatedByStore } = related;
175
175
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: product.id, productType: "product", snapshot: {
176
176
  title: product.title,
@@ -231,7 +231,7 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
231
231
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
232
232
  : undefined, renderGroupSection: groupId
233
233
  ? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
234
- : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: allSpecs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsList, { reviews: reviews, context: "listing", emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
234
+ : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: allSpecs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: allSpecs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, ingredientsContent: ingredients.length > 0 ? (_jsx(Ul, { spacing: "comfortable", children: ingredients.map((item, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-1 flex-shrink-0 h-1.5 w-1.5 bg-primary-400", rounded: "full" }), item] }, i))) })) : undefined, howToUseContent: howToUse.length > 0 ? (_jsx(Ol, { spacing: "loose", children: howToUse.map((step, i) => (_jsxs(Li, { layout: "flex-start", gap: "3", textSize: "sm", color: "muted", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "flex-shrink-0 h-6 w-6 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300", rounded: "full", children: i + 1 }), step] }, i))) })) : undefined, reviewsContent: _jsx(ReviewsListingPanel, { source: { kind: "product", productId: product.id }, stateMode: "local", context: "listing", initialData: initialReviews, emptyLabel: "No reviews yet \u2014 be the first to review this product." }), customTabs: customSections.map((s) => ({
235
235
  id: s.id,
236
236
  label: s.title,
237
237
  content: _jsx(CustomSectionTabContent, { section: s }),
@@ -12,7 +12,7 @@ import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../constants/action-d
12
12
  import { CompareOverlay } from "./CompareOverlay";
13
13
  import { ROUTES } from "../../../next";
14
14
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
15
- import { ProductGrid, ProductFilters, PRODUCT_PUBLIC_SORT_OPTIONS } from ".";
15
+ import { ProductGrid, ProductFilters } from ".";
16
16
  import { useGuestCart } from "../../cart/hooks/useGuestCart";
17
17
  import { getGuestCartItems } from "../../cart/utils/guest-cart";
18
18
  import { useGuestWishlist } from "../../wishlist/hooks/useGuestWishlist";
@@ -28,6 +28,9 @@ import { sortBy } from "../../../constants/sort";
28
28
  import { PRODUCT_FIELDS } from "../../../constants/field-names";
29
29
  import { useBottomActions } from "../../layout";
30
30
  import { GENERIC_PRODUCT_LISTING_TYPES, PRODUCT_TYPE_FILTER_TABS } from "../constants/listing-tabs";
31
+ import { parseSelectedListingTypes } from "../utils/listing-type";
32
+ import { commonSortOptionsFor, hideDefaultsFor } from "../../../_internal/shared/listing-types/_registry";
33
+ import { TextLink } from "../../../ui";
31
34
  const __P = {
32
35
  p3: "p-[var(--appkit-space-3)]",
33
36
  };
@@ -46,7 +49,40 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
46
49
  const [compareIds, setCompareIds] = useState([]);
47
50
  const showSold = table.get(TABLE_KEYS.SHOW_SOLD) === "true";
48
51
  const [view, setView] = useState(table.get(TABLE_KEYS.VIEW) || VIEW_MODE.GRID);
49
- const productType = table.get(TABLE_KEYS.LISTING_TYPE) || "All";
52
+ // ── Type filter (multi-select) ───────────────────────────────────────────
53
+ // The URL value is a pipe-joined set; empty means "every type this page
54
+ // spans". Unknown tokens are dropped rather than passed through to a
55
+ // Firestore `==` that could never match (Root Cause #33).
56
+ const typeParam = table.get(TABLE_KEYS.LISTING_TYPE);
57
+ const selectedTypes = parseSelectedListingTypes(typeParam).filter((t) => listingTypes.includes(t));
58
+ const effectiveTypes = (selectedTypes.length > 0 ? selectedTypes : listingTypes);
59
+ // Sorts valid across every selected type. Pick Auctions alone and you get
60
+ // "Ending Soon"; pick Auctions + Products and you get only what both
61
+ // support, because a sort on `auctionEndDate` would order the products
62
+ // arbitrarily (and needs an index nobody declares for that shape).
63
+ const sortOptions = commonSortOptionsFor(effectiveTypes, "public");
64
+ const sortValue = table.get(TABLE_KEYS.SORT) || DEFAULT_SORT;
65
+ // Guard against a sort carried over from a previous selection that the new
66
+ // one can't satisfy — that combination throws FAILED_PRECONDITION in
67
+ // Firestore and surfaces as a bare empty page (Root Cause #2).
68
+ const effectiveSort = sortOptions.some((o) => o.value === sortValue)
69
+ ? sortValue
70
+ : DEFAULT_SORT;
71
+ // Which "Show X" toggles are meaningful for the current selection. A mixed
72
+ // set can need both (products sell out, auctions end).
73
+ const hideDefaults = hideDefaultsFor(effectiveTypes);
74
+ const showsSoldToggle = hideDefaults.includes("sold");
75
+ const showsEndedToggle = hideDefaults.includes("ended") || hideDefaults.includes("closed");
76
+ const showEnded = table.get(TABLE_KEYS.SHOW_ENDED) === "true";
77
+ // "Full <type> filters →" — a single selected type with a dedicated browse
78
+ // page gets a link to it, since that page carries facets this generic one
79
+ // can't (bid range, delivery-date window, reveal status).
80
+ const dedicatedPage = selectedTypes.length === 1 && pluginFor(selectedTypes[0]).browseRoute
81
+ ? {
82
+ href: pluginFor(selectedTypes[0]).browseRoute,
83
+ label: `Full ${pluginFor(selectedTypes[0]).pluralLabel} filters →`,
84
+ }
85
+ : null;
50
86
  const { pendingTable, filterActiveCount, onFilterApply, onFilterClear, onResetAll, onFilterReset } = usePendingTable(table, FILTER_KEYS);
51
87
  const openFilters = useCallback(() => {
52
88
  onFilterReset();
@@ -57,11 +93,19 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
57
93
  setFilterOpen(false);
58
94
  }, [onFilterApply]);
59
95
  const resetAll = useCallback(() => {
60
- onResetAll({ [TABLE_KEYS.QUERY]: "", [TABLE_KEYS.SORT]: "", [TABLE_KEYS.SHOW_SOLD]: "" });
96
+ onResetAll({
97
+ [TABLE_KEYS.QUERY]: "",
98
+ [TABLE_KEYS.SORT]: "",
99
+ [TABLE_KEYS.SHOW_SOLD]: "",
100
+ [TABLE_KEYS.SHOW_ENDED]: "",
101
+ [TABLE_KEYS.LISTING_TYPE]: "",
102
+ });
61
103
  setSearchInput("");
62
104
  }, [onResetAll]);
63
105
  const hasActiveState = !!table.get(TABLE_KEYS.QUERY) ||
64
106
  table.get(TABLE_KEYS.SHOW_SOLD) === "true" ||
107
+ table.get(TABLE_KEYS.SHOW_ENDED) === "true" ||
108
+ !!typeParam ||
65
109
  table.get(TABLE_KEYS.SORT) !== DEFAULT_SORT ||
66
110
  filterActiveCount > 0;
67
111
  const localCart = useGuestCart();
@@ -81,13 +125,27 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
81
125
  brand: table.get(TABLE_KEYS.BRAND) || undefined,
82
126
  storeId: table.get(TABLE_KEYS.STORE_ID) || undefined,
83
127
  freeShipping: table.get(TABLE_KEYS.FREE_SHIPPING) === "true" ? true : undefined,
84
- sort: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT,
128
+ // These three are in FILTER_KEYS (so they count toward the filter badge)
129
+ // but were never put on the wire — the drawer sections were inert.
130
+ tags: table.get(TABLE_KEYS.TAGS) || undefined,
131
+ sublistingCategory: table.get(TABLE_KEYS.SUBLISTING_CATEGORY) || undefined,
132
+ features: table.get(TABLE_KEYS.FEATURES) || undefined,
133
+ sort: effectiveSort,
85
134
  page: table.getNumber(TABLE_KEYS.PAGE, 1),
86
135
  perPage: table.getNumber(TABLE_KEYS.PAGE_SIZE, 24),
87
- listingType: productType === "All" ? listingTypes.join("|") : productType,
88
- // Hide sold-out items by default. Uses stockQuantity>0 (always-present field)
89
- // instead of status=="published" because sellers don't actively transition status.
90
- inStock: showSold ? undefined : true,
136
+ // Pipe-joined OR-group sievejs parses it as a same-field OR and the
137
+ // Firebase adapter upgrades it to a `.where(…, "in", …)` query.
138
+ listingType: effectiveTypes.join("|"),
139
+ // Hide sold-out items by default, but only when a sold-out-able type is in
140
+ // play — an auction has no meaningful stock, so applying it to an
141
+ // auctions-only view would hide every live auction.
142
+ inStock: showsSoldToggle && !showSold ? true : undefined,
143
+ // Same shape for time-boxed types. `dateFrom` only takes effect when
144
+ // exactly one type is selected (there is no single end-date field shared
145
+ // across types), which `dateFieldFor` already enforces server-side.
146
+ dateFrom: showsEndedToggle && !showEnded && selectedTypes.length === 1
147
+ ? new Date().toISOString()
148
+ : undefined,
91
149
  };
92
150
  const { products, totalPages, page, isLoading } = useProducts(params, { initialData });
93
151
  const selection = useBulkSelection({ items: products, keyExtractor: (p) => p.id });
@@ -200,14 +258,32 @@ export function ProductsIndexListing({ initialData, listingTypes = GENERIC_PRODU
200
258
  },
201
259
  },
202
260
  ] } } : {});
203
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
204
- { label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") },
261
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: searchPlaceholder, onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: effectiveSort, sortOptions: sortOptions, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
262
+ // Each toggle appears only when it means something for the selected
263
+ // types — "Show ended" against a Products-only view would be noise,
264
+ // and "Show sold" against an Auctions-only view is meaningless.
265
+ ...(showsSoldToggle
266
+ ? [{ label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") }]
267
+ : []),
268
+ ...(showsEndedToggle
269
+ ? [{ label: "Show ended", active: showEnded, onChange: (next) => table.set(TABLE_KEYS.SHOW_ENDED, next ? "true" : "") }]
270
+ : []),
205
271
  // Inline quick-filter: the highest-frequency drawer-only facet promoted
206
272
  // to the sticky toolbar so it doesn't require open-drawer → check → Apply
207
273
  // → close for every toggle. The full facet set (category/price/brand/…)
208
274
  // remains in <FilterDrawer> below.
209
275
  { label: "Free shipping", active: table.get(TABLE_KEYS.FREE_SHIPPING) === "true", onChange: (next) => table.set(TABLE_KEYS.FREE_SHIPPING, next ? "true" : "") },
210
- ] }), _jsx(Div, { padding: "y-sm", children: _jsx(FilterChipGroup, { label: "Type", tabs: typeTabs, value: productType, onChange: (v) => table.set(TABLE_KEYS.LISTING_TYPE, v === "All" ? "" : v) }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
276
+ ] }), _jsx(Div, { padding: "y-sm", children: _jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsx(FilterChipGroup, { multiple: true, label: "Type", tabs: typeTabs, value: selectedTypes.join("|"), onChange: (next) => {
277
+ // Clear `sort` alongside the type change: a sort valid for the
278
+ // old selection (say -auctionEndDate) can be invalid for the new
279
+ // one, and useUrlTable's single router.replace means we must set
280
+ // both in ONE call — a follow-up setPage/set would read stale
281
+ // searchParams and overwrite this update (Root Cause #13).
282
+ table.setMany({
283
+ [TABLE_KEYS.LISTING_TYPE]: next,
284
+ [TABLE_KEYS.SORT]: "",
285
+ });
286
+ } }), dedicatedPage && (_jsx(TextLink, { href: dedicatedPage.href, size: "sm", weight: "medium", children: dedicatedPage.label }))] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
211
287
  {
212
288
  id: ACTION_ID.ADD_TO_CART,
213
289
  label: ACTION_META[ACTION_ID.ADD_TO_CART].label,