@mohasinac/appkit 4.53.1 → 4.54.0-next.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 (220) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/client/theme/ThemeProvider.js +15 -1
  3. package/dist/_internal/server/features/auth/capabilities.js +7 -5
  4. package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
  5. package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
  6. package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
  7. package/dist/_internal/server/features/price-index/market.d.ts +57 -0
  8. package/dist/_internal/server/features/price-index/market.js +120 -0
  9. package/dist/_internal/server/features/products/list-public.d.ts +12 -0
  10. package/dist/_internal/server/features/products/list-public.js +10 -1
  11. package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
  12. package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
  13. package/dist/_internal/server/features/search/index-build/sources.js +43 -2
  14. package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
  15. package/dist/_internal/server/features/stores/tabs.js +5 -3
  16. package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
  17. package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
  18. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  19. package/dist/_internal/shared/actions/action-registry.js +128 -0
  20. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
  21. package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
  22. package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
  23. package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
  24. package/dist/_internal/shared/listing-types/tiers.js +150 -0
  25. package/dist/_internal/shared/search/match.js +28 -2
  26. package/dist/cli/index.js +0 -2
  27. package/dist/client.d.ts +21 -2
  28. package/dist/client.js +23 -4
  29. package/dist/constants/api-endpoints.d.ts +28 -0
  30. package/dist/constants/api-endpoints.js +14 -0
  31. package/dist/features/account/components/UserOrdersView.js +26 -7
  32. package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
  33. package/dist/features/addresses/utils/postal-autofill.js +54 -0
  34. package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
  35. package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
  36. package/dist/features/admin/components/OwnerQuickActions.js +142 -0
  37. package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
  38. package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
  39. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
  40. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
  41. package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
  42. package/dist/features/auth/permissions/constants.d.ts +29 -2
  43. package/dist/features/auth/permissions/constants.js +68 -1
  44. package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
  45. package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
  46. package/dist/features/cart/components/CartDrawer.js +3 -2
  47. package/dist/features/cart/components/CartGroupLineRow.js +2 -1
  48. package/dist/features/cart/hooks/useAddToCart.js +10 -5
  49. package/dist/features/cart/utils/pending-ops.d.ts +11 -1
  50. package/dist/features/cart/utils/pending-ops.js +14 -2
  51. package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
  52. package/dist/features/categories/components/BrandDetailPageView.js +15 -6
  53. package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
  54. package/dist/features/categories/components/BrandDetailTabs.js +18 -3
  55. package/dist/features/categories/components/BundleCollage.js +3 -2
  56. package/dist/features/categories/components/BundleItemsPicker.js +3 -4
  57. package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
  58. package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
  59. package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
  60. package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
  61. package/dist/features/categories/components/CategoryGrid.js +9 -2
  62. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
  63. package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
  64. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
  65. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
  66. package/dist/features/events/components/EventCard.d.ts +11 -1
  67. package/dist/features/events/components/EventCard.js +9 -2
  68. package/dist/features/layout/AppLayoutShell.js +2 -1
  69. package/dist/features/layout/BottomActions.d.ts +1 -0
  70. package/dist/features/layout/BottomActions.js +330 -0
  71. package/dist/features/layout/BottomChrome.d.ts +45 -0
  72. package/dist/features/layout/BottomChrome.js +5 -17
  73. package/dist/features/layout/SideActionDock.js +36 -13
  74. package/dist/features/layout/index.d.ts +2 -7
  75. package/dist/features/layout/index.js +7 -7
  76. package/dist/features/layout/useIsDesktop.d.ts +36 -0
  77. package/dist/features/layout/useIsDesktop.js +48 -0
  78. package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
  79. package/dist/features/media/messages/en.json +67 -67
  80. package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
  81. package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
  82. package/dist/features/orders/components/index.d.ts +2 -1
  83. package/dist/features/orders/components/index.js +1 -1
  84. package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
  85. package/dist/features/orders/utils/order-announcement-message.js +16 -0
  86. package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
  87. package/dist/features/orders/utils/payment-proof-message.js +17 -0
  88. package/dist/features/parts/schemas/firestore.d.ts +227 -0
  89. package/dist/features/parts/schemas/firestore.js +6 -6
  90. package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
  91. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
  92. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
  93. package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
  94. package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
  95. package/dist/features/price-index/lookup-target.d.ts +48 -0
  96. package/dist/features/price-index/lookup-target.js +107 -0
  97. package/dist/features/products/api/[id]/route.js +8 -1
  98. package/dist/features/products/api/route.js +16 -4
  99. package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
  100. package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
  101. package/dist/features/products/components/CompareOverlay.d.ts +2 -1
  102. package/dist/features/products/components/CompareOverlay.js +2 -1
  103. package/dist/features/products/components/GroupSettingsPanel.js +2 -1
  104. package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
  105. package/dist/features/products/components/InteractiveProductCard.js +16 -4
  106. package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
  107. package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
  108. package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
  109. package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
  110. package/dist/features/products/components/ProductDetailPageView.js +34 -4
  111. package/dist/features/products/components/ProductGrid.d.ts +14 -1
  112. package/dist/features/products/components/ProductGrid.js +22 -5
  113. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  114. package/dist/features/products/components/SublistingCarouselSection.js +2 -1
  115. package/dist/features/products/repository/products.repository.d.ts +1 -0
  116. package/dist/features/products/repository/products.repository.js +5 -1
  117. package/dist/features/products/utils/group-member-image.d.ts +9 -2
  118. package/dist/features/products/utils/group-member-image.js +2 -1
  119. package/dist/features/reviews/api/[id]/route.js +83 -23
  120. package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
  121. package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
  122. package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
  123. package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
  124. package/dist/features/reviews/components/index.d.ts +4 -0
  125. package/dist/features/reviews/components/index.js +2 -0
  126. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  127. package/dist/features/seller/actions/seller-actions.js +49 -3
  128. package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
  129. package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
  130. package/dist/features/seller/components/SellerProductsView.js +3 -2
  131. package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
  132. package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
  133. package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
  134. package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
  135. package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
  136. package/dist/features/stores/components/index.d.ts +0 -2
  137. package/dist/features/stores/components/index.js +0 -1
  138. package/dist/features/stores/schemas/index.d.ts +1 -1
  139. package/dist/features/stores/schemas/index.js +10 -1
  140. package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
  141. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -0
  142. package/dist/index.d.ts +8 -27
  143. package/dist/index.js +14 -35
  144. package/dist/next/routing/route-map.d.ts +2 -0
  145. package/dist/next/routing/route-map.js +9 -0
  146. package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
  147. package/dist/providers/shipping-shiprocket/index.js +353 -0
  148. package/dist/schemas/registry.d.ts +2 -2
  149. package/dist/security/csp.d.ts +16 -1
  150. package/dist/security/csp.js +5 -3
  151. package/dist/seed/_helpers/category-forest.js +27 -0
  152. package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
  153. package/dist/seed/_helpers/category-parts.d.ts +1 -1
  154. package/dist/seed/_helpers/category-parts.js +13412 -3020
  155. package/dist/seed/categories-seed-data.js +48 -6
  156. package/dist/seed/homepage-sections-seed-data.js +41 -28
  157. package/dist/seed/model-composition-seed-data.js +6 -8
  158. package/dist/seed/part-glossary/chunk0.d.ts +9 -0
  159. package/dist/seed/part-glossary/chunk0.js +35044 -0
  160. package/dist/seed/part-glossary/chunk1.d.ts +9 -0
  161. package/dist/seed/part-glossary/chunk1.js +39955 -0
  162. package/dist/seed/part-glossary/chunk2.d.ts +9 -0
  163. package/dist/seed/part-glossary/chunk2.js +50335 -0
  164. package/dist/seed/part-glossary/chunk3.d.ts +9 -0
  165. package/dist/seed/part-glossary/chunk3.js +52386 -0
  166. package/dist/seed/part-glossary/chunk4.d.ts +9 -0
  167. package/dist/seed/part-glossary/chunk4.js +50549 -0
  168. package/dist/seed/part-glossary-seed-data.d.ts +1 -1
  169. package/dist/seed/part-glossary-seed-data.js +23 -47483
  170. package/dist/seed/products-beyblade-x-seed-data.js +36 -1
  171. package/dist/server.d.ts +6 -8
  172. package/dist/server.js +16 -20
  173. package/dist/styles.css +516 -358
  174. package/dist/tailwind-utilities.css +1 -1
  175. package/dist/tokens/themes/index.d.ts +3 -1
  176. package/dist/tokens/themes/index.js +11 -1
  177. package/dist/tokens/themes/technical-dark.d.ts +30 -0
  178. package/dist/tokens/themes/technical-dark.js +103 -0
  179. package/dist/tokens/themes/technical-light.d.ts +22 -0
  180. package/dist/tokens/themes/technical-light.js +89 -0
  181. package/dist/tokens/tokens.css +155 -0
  182. package/dist/ui/components/GatedPrice.js +30 -3
  183. package/dist/ui/components/GatedPrice.style.css +22 -19
  184. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  185. package/dist/ui/forms/FormShell.style.css +260 -260
  186. package/package.json +1 -1
  187. package/scripts/seed-cli.mjs +133 -27
  188. package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
  189. package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
  190. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
  191. package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
  192. package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
  193. package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
  194. package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
  195. package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
  196. package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
  197. package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
  198. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
  199. package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
  200. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
  201. package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
  202. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
  203. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
  204. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
  205. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
  206. package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
  207. package/dist/features/whatsapp-bot/components/index.js +0 -2
  208. package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
  209. package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
  210. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
  211. package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
  212. package/dist/features/whatsapp-bot/index.d.ts +0 -3
  213. package/dist/features/whatsapp-bot/index.js +0 -3
  214. package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
  215. package/dist/features/whatsapp-bot/manifest.js +0 -13
  216. package/dist/features/whatsapp-bot/messages/en.json +0 -7
  217. package/dist/features/whatsapp-bot/server.d.ts +0 -7
  218. package/dist/features/whatsapp-bot/server.js +0 -7
  219. package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
  220. package/dist/features/whatsapp-bot/types/index.js +0 -1
@@ -79,6 +79,11 @@ export const ACCOUNT_ENDPOINTS = {
79
79
  HISTORY_ITEM: (productId) => `/api/user/history/${encodeURIComponent(productId)}`,
80
80
  HISTORY_MERGE: "/api/user/history/merge",
81
81
  REVIEWS: "/api/user/reviews",
82
+ /**
83
+ * "Have I bought this, and did I review it?" for one product — backs the
84
+ * purchase strip on a product page. Per-viewer, never cached.
85
+ */
86
+ PURCHASE_CONTEXT: (productId) => `/api/user/purchases/${encodeURIComponent(productId)}`,
82
87
  BIDS: "/api/user/bids",
83
88
  BID_BY_ID: (id) => `/api/user/bids/${id}`,
84
89
  EXPORT: "/api/user/export",
@@ -348,6 +353,13 @@ export const TOOL_ENDPOINTS = {
348
353
  * is the multi-get `searchPriceIndex` otherwise pays for every lookup.
349
354
  */
350
355
  PRICE_INDEX_BANDS: (ids) => `/api/tools/price-index/bands?ids=${ids.map(encodeURIComponent).join(",")}`,
356
+ /**
357
+ * The competitor-crawl market band for the same leaves — SIGNED-IN ONLY, and
358
+ * therefore a separate request from `PRICE_INDEX_BANDS` rather than more
359
+ * fields on it. That route is public and edge-cached; this one must not be,
360
+ * or one signed-in lookup populates an entry every guest reads.
361
+ */
362
+ PRICE_INDEX_MARKET: (ids) => `/api/tools/price-index/market?ids=${ids.map(encodeURIComponent).join(",")}`,
351
363
  };
352
364
  /**
353
365
  * The sticker-sheet kiss-cut tracer. Admin-only, every route behind
@@ -585,6 +597,8 @@ export const PROMOTION_ENDPOINTS = {
585
597
  export const REVIEW_ENDPOINTS = {
586
598
  LIST: "/api/reviews",
587
599
  FEATURED: "/api/reviews?featured=true",
600
+ /** GET / PATCH / DELETE one review. The PATCH is the buyer's own edit. */
601
+ BY_ID: (id) => `/api/reviews/${encodeURIComponent(id)}`,
588
602
  };
589
603
  // ---------------------------------------------------------------------------
590
604
  // Search
@@ -11,7 +11,23 @@ import { OrderCard } from "../../orders/components/OrdersList.js";
11
11
  import { useOrderScope } from "../../orders/components/OrderScopeTabs.js";
12
12
  import { useSearchParams } from "next/navigation.js";
13
13
  const CANCELLABLE_STATUSES = new Set(["pending", "confirmed", "processing"]);
14
- const TRACKABLE_STATUSES = new Set(["shipped"]);
14
+ /*
15
+ * 🛑 Every PRE-DELIVERY status, not just "shipped".
16
+ *
17
+ * It was `["shipped"]` alone, so a buyer whose order was pending, confirmed
18
+ * or processing had no "where is it" link at all — which is precisely when
19
+ * they most want one. The timeline on the track page reads correctly for all
20
+ * of these: it shows the stages reached and the stages still ahead.
21
+ *
22
+ * Delivered, cancelled, refunded and returned are excluded because tracking a
23
+ * finished order is the order detail page's job, not a courier's.
24
+ */
25
+ const TRACKABLE_STATUSES = new Set([
26
+ "pending",
27
+ "confirmed",
28
+ "processing",
29
+ "shipped",
30
+ ]);
15
31
  const SORT_OPTIONS = [
16
32
  { value: sortBy("createdAt", "DESC"), label: "Newest first" },
17
33
  { value: sortBy("createdAt", "ASC"), label: "Oldest first" },
@@ -77,12 +93,15 @@ export function UserOrdersView({ onOrderClick }) {
77
93
  portal: "user",
78
94
  title: "My Orders",
79
95
  search: {
80
- placeholder: "Search by order id…",
81
- // `/api/user/orders` filters in memory on `o.id` and nothing else — not
82
- // the product titles the rows display, which is what a buyer would try
83
- // first. Saying "order id" is the honest promise until the route can
84
- // match more.
85
- fields: ["id"],
96
+ placeholder: "Search by item or order id…",
97
+ /*
98
+ * The route now matches product titles as well as the order id, so the
99
+ * placeholder can promise it. It said "order id" because that was all
100
+ * the route could do, with a comment calling that "the honest promise
101
+ * until the route can match more" — this is the promise catching up,
102
+ * not a widened claim.
103
+ */
104
+ fields: ["id", "productTitle"],
86
105
  },
87
106
  emptyLabel: emptyMessage,
88
107
  filterKeys: ["status", "orderType"],
@@ -0,0 +1,39 @@
1
+ interface CityState {
2
+ city: string;
3
+ state: string;
4
+ }
5
+ /**
6
+ * What the last autofill wrote, so a later one can tell its own output apart
7
+ * from a human's typing. A field is absent when the helper declined to write
8
+ * it — which makes it the human's, and untouchable.
9
+ */
10
+ export type PostalAutofillMemo = Partial<CityState>;
11
+ /**
12
+ * Merge `resolved` into `prev`, returning the new values and an updated memo.
13
+ *
14
+ * ── 🛑 THE BUG THIS EXISTS TO FIX ─────────────────────────────────────────
15
+ *
16
+ * Both address forms used `prev.city.trim() ? prev.city : city` — fill only
17
+ * an EMPTY field. That intent is right and is preserved here: correcting an
18
+ * address someone deliberately typed is a bug they cannot report, because
19
+ * they watch their own input vanish with no idea what did it.
20
+ *
21
+ * But "empty" cannot tell WHO filled the field. After the first lookup the
22
+ * city is non-empty — written by the autofill itself — so every subsequent
23
+ * lookup is refused. Enter 413001, get Solapur/Maharashtra; correct the PIN
24
+ * to 500005 (Hyderabad) and the form still shows Solapur. The PIN and the
25
+ * city now disagree, the form validates, and the parcel ships to the wrong
26
+ * city — worse than never autofilling at all.
27
+ *
28
+ * ── The rule ──────────────────────────────────────────────────────────────
29
+ *
30
+ * A field is ours to overwrite when it is EMPTY, or when it still holds
31
+ * exactly what we last wrote into it. The moment a human edits it, it stops
32
+ * matching the memo and becomes untouchable again — the original guarantee,
33
+ * now actually enforceable rather than approximated by "is it blank".
34
+ */
35
+ export declare function applyPostalAutofill<T extends CityState>(prev: T, resolved: Partial<CityState>, memo: PostalAutofillMemo): {
36
+ next: T;
37
+ memo: PostalAutofillMemo;
38
+ };
39
+ export {};
@@ -0,0 +1,54 @@
1
+ /*
2
+ * WHY: A postal lookup must fill the city/state it knows WITHOUT overwriting
3
+ * what the person typed — and the obvious test for that ("is the field
4
+ * empty?") silently breaks every lookup after the first.
5
+ * WHAT: `applyPostalAutofill` — merge a resolved city/state into form values,
6
+ * overwriting only values this helper itself wrote earlier.
7
+ *
8
+ * EXPORTS: applyPostalAutofill, type PostalAutofillMemo
9
+ *
10
+ * @tag domain:addresses
11
+ * @tag layer:utils
12
+ * @tag pattern:pure
13
+ * @tag sideEffects:none
14
+ */
15
+ /**
16
+ * Merge `resolved` into `prev`, returning the new values and an updated memo.
17
+ *
18
+ * ── 🛑 THE BUG THIS EXISTS TO FIX ─────────────────────────────────────────
19
+ *
20
+ * Both address forms used `prev.city.trim() ? prev.city : city` — fill only
21
+ * an EMPTY field. That intent is right and is preserved here: correcting an
22
+ * address someone deliberately typed is a bug they cannot report, because
23
+ * they watch their own input vanish with no idea what did it.
24
+ *
25
+ * But "empty" cannot tell WHO filled the field. After the first lookup the
26
+ * city is non-empty — written by the autofill itself — so every subsequent
27
+ * lookup is refused. Enter 413001, get Solapur/Maharashtra; correct the PIN
28
+ * to 500005 (Hyderabad) and the form still shows Solapur. The PIN and the
29
+ * city now disagree, the form validates, and the parcel ships to the wrong
30
+ * city — worse than never autofilling at all.
31
+ *
32
+ * ── The rule ──────────────────────────────────────────────────────────────
33
+ *
34
+ * A field is ours to overwrite when it is EMPTY, or when it still holds
35
+ * exactly what we last wrote into it. The moment a human edits it, it stops
36
+ * matching the memo and becomes untouchable again — the original guarantee,
37
+ * now actually enforceable rather than approximated by "is it blank".
38
+ */
39
+ export function applyPostalAutofill(prev, resolved, memo) {
40
+ const nextMemo = {};
41
+ const merge = (field) => {
42
+ const current = prev[field];
43
+ const incoming = resolved[field];
44
+ const ours = current.trim() === "" || current === memo[field];
45
+ if (!ours || !incoming)
46
+ return current; // human's value, or nothing to say
47
+ nextMemo[field] = incoming;
48
+ return incoming;
49
+ };
50
+ return {
51
+ next: { ...prev, city: merge("city"), state: merge("state") },
52
+ memo: nextMemo,
53
+ };
54
+ }
@@ -6,11 +6,20 @@ import { useQueryClient } from "@tanstack/react-query";
6
6
  import { Checkbox, Details, Div, SideDrawer, Span, Stack, Summary, useToast } from "../../../ui/index.js";
7
7
  import { apiClient } from "../../../http/index.js";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
9
+ import { ALL_STORE_CAPABILITIES, DEFAULT_STORE_CAPABILITIES, } from "../../auth/permissions/constants.js";
9
10
  import { adminStoreUpdateSchema } from "../schemas/admin-editor-forms.js";
10
11
  import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
11
12
  import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell.js";
12
13
  import { SectionForm, useSectionFormNav, buildSectionsFromSchema } from "../../shell/index.js";
13
- const DEFAULT_CAPABILITIES = ["suggest_brands", "create_coupons"];
14
+ /*
15
+ * 🛑 Imported, not restated. This value is the `??` fallback on the load path
16
+ * below, so whatever it holds is what gets SAVED BACK for a store whose
17
+ * `capabilities` field is absent. A local copy that drifted from
18
+ * `DEFAULT_STORE_CAPABILITIES` would silently rewrite entitlements on the next
19
+ * unrelated edit — Root Cause #38's re-save shape, with two owners of one
20
+ * default as the cause.
21
+ */
22
+ const DEFAULT_CAPABILITIES = [...DEFAULT_STORE_CAPABILITIES];
14
23
  /** Add or remove one capability, returning the new list. */
15
24
  function toggleCapability(current, key) {
16
25
  const next = new Set(current);
@@ -20,41 +29,71 @@ function toggleCapability(current, key) {
20
29
  next.add(key);
21
30
  return Array.from(next);
22
31
  }
23
- const CAPABILITY_GROUPS = [
24
- {
25
- label: "Listings",
26
- caps: [
27
- { key: "host_auctions", label: "Host Auctions" },
28
- { key: "host_preorders", label: "Host Pre-orders" },
29
- { key: "create_categories", label: "Request New Categories" },
30
- { key: "suggest_brands", label: "Suggest Brands" },
31
- { key: "create_coupons", label: "Create Coupons" },
32
- { key: "bulk_listing_import", label: "Bulk Listing Import" },
33
- { key: "extended_return_window", label: "Extended Return Window" },
34
- ],
35
- },
36
- {
37
- label: "Trust & Visibility",
38
- caps: [
39
- { key: "verified_seller", label: "Verified Seller Badge" },
40
- { key: "featured_placement", label: "Featured Placement" },
41
- { key: "promotional_banner", label: "Promotional Banner" },
42
- { key: "priority_support", label: "Priority Support" },
43
- ],
44
- },
45
- {
46
- label: "Platform",
47
- caps: [
48
- { key: "multiple_stores", label: "Multiple Stores" },
49
- { key: "custom_store_slug", label: "Custom Store Slug" },
50
- { key: "api_access", label: "API Access" },
51
- { key: "lower_commission_rate", label: "Lower Commission Rate" },
52
- { key: "early_access_features", label: "Early Access Features" },
53
- { key: "advanced_analytics", label: "Advanced Analytics" },
54
- { key: "whatsapp_catalog_sync", label: "WhatsApp Catalog Sync" },
55
- ],
32
+ /**
33
+ * The three capability groups, named once.
34
+ *
35
+ * A literal repeated per capability is how a group silently splits in two the
36
+ * day someone types "Trust and Visibility" in one of nineteen entries — the
37
+ * `CAPABILITY_GROUPS` derivation below filters on equality, so the typo'd entry
38
+ * would land in no rendered group at all and that capability would vanish from
39
+ * the admin UI without any error.
40
+ */
41
+ const CAP_GROUP = {
42
+ LISTINGS: "Listings",
43
+ TRUST: "Trust & Visibility",
44
+ PLATFORM: "Platform",
45
+ };
46
+ /**
47
+ * Every capability's group and label.
48
+ *
49
+ * 🛑 `Record<StoreCapability, …>`, so a capability added to the union MUST be
50
+ * given a home here or this file does not compile. The groups used to be three
51
+ * hand-written arrays, which meant a new capability could be granted in code,
52
+ * checked by a server gate, and remain **unreachable from the admin UI** — the
53
+ * only place it can be turned on. That is the whole drift class in one
54
+ * sentence, and it had already happened: `advanced_listing_types` is the
55
+ * nineteenth member and the arrays listed eighteen.
56
+ */
57
+ const CAPABILITY_META = {
58
+ advanced_listing_types: {
59
+ group: CAP_GROUP.LISTINGS,
60
+ label: "Advanced Listing Types (pre-orders, prize draws, classifieds, digital codes, live, art, stickers)",
56
61
  },
62
+ // Legacy: no longer checked anywhere — `auction` is a basic type now.
63
+ host_auctions: { group: CAP_GROUP.LISTINGS, label: "Host Auctions (legacy — no longer required)" },
64
+ // Legacy per-type grant, still honoured so existing stores keep access.
65
+ host_preorders: { group: CAP_GROUP.LISTINGS, label: "Host Pre-orders (legacy grant)" },
66
+ create_categories: { group: CAP_GROUP.LISTINGS, label: "Request New Categories" },
67
+ suggest_brands: { group: CAP_GROUP.LISTINGS, label: "Suggest Brands" },
68
+ create_coupons: { group: CAP_GROUP.LISTINGS, label: "Create Coupons" },
69
+ bulk_listing_import: { group: CAP_GROUP.LISTINGS, label: "Bulk Listing Import" },
70
+ extended_return_window: { group: CAP_GROUP.LISTINGS, label: "Extended Return Window" },
71
+ verified_seller: { group: CAP_GROUP.TRUST, label: "Verified Seller Badge" },
72
+ featured_placement: { group: CAP_GROUP.TRUST, label: "Featured Placement" },
73
+ promotional_banner: { group: CAP_GROUP.TRUST, label: "Promotional Banner" },
74
+ priority_support: { group: CAP_GROUP.TRUST, label: "Priority Support" },
75
+ multiple_stores: { group: CAP_GROUP.PLATFORM, label: "Multiple Stores" },
76
+ custom_store_slug: { group: CAP_GROUP.PLATFORM, label: "Custom Store Slug" },
77
+ api_access: { group: CAP_GROUP.PLATFORM, label: "API Access" },
78
+ lower_commission_rate: { group: CAP_GROUP.PLATFORM, label: "Lower Commission Rate" },
79
+ early_access_features: { group: CAP_GROUP.PLATFORM, label: "Early Access Features" },
80
+ advanced_analytics: { group: CAP_GROUP.PLATFORM, label: "Advanced Analytics" },
81
+ whatsapp_catalog_sync: { group: CAP_GROUP.PLATFORM, label: "WhatsApp Catalog Sync" },
82
+ };
83
+ const CAPABILITY_GROUP_ORDER = [
84
+ CAP_GROUP.LISTINGS,
85
+ CAP_GROUP.TRUST,
86
+ CAP_GROUP.PLATFORM,
57
87
  ];
88
+ /**
89
+ * Derived, so every capability appears in exactly one group by construction.
90
+ * Keeps the `{ label, caps: [{ key, label }] }` shape the renderer below
91
+ * already expects.
92
+ */
93
+ const CAPABILITY_GROUPS = CAPABILITY_GROUP_ORDER.map((group) => ({
94
+ label: group,
95
+ caps: ALL_STORE_CAPABILITIES.filter((key) => CAPABILITY_META[key].group === group).map((key) => ({ key, label: CAPABILITY_META[key].label })),
96
+ }));
58
97
  const STATUS_OPTIONS = [
59
98
  { label: "Active", value: "active" },
60
99
  { label: "Pending", value: "pending" },
@@ -0,0 +1,29 @@
1
+ /** Which registry bucket supplies the labels and the confirm copy. */
2
+ export type OwnerQuickActionsResource = "PRODUCT" | "CATEGORY" | "BRAND" | "BUNDLE" | "GROUP" | "SUBLISTING" | "EVENT" | "BLOG" | "REVIEW";
3
+ export interface OwnerQuickActionsProps {
4
+ resource: OwnerQuickActionsResource;
5
+ /** Where the pencil goes — that record's dashboard editor. */
6
+ editHref: string;
7
+ /** Called after the confirm is accepted. Must do the DELETE itself. */
8
+ onDelete?: () => void | Promise<void>;
9
+ /**
10
+ * Admin-only clone. Omit entirely for resources with no duplicate endpoint —
11
+ * categories, bundles, carousel slides and homepage sections all have
12
+ * derived or positional state that a copy would carry wrongly, so the
13
+ * absence is deliberate rather than unfinished.
14
+ */
15
+ onClone?: () => void | Promise<void>;
16
+ /** Auth UID of the owner, when the record has one. Admins pass regardless. */
17
+ ownerId?: string;
18
+ /** Store SLUG for store-owned records — `SessionUser.storeId` is the slug. */
19
+ storeId?: string;
20
+ /** Names the thing in the confirm dialog: "Delete Dranzer S?" */
21
+ recordLabel?: string;
22
+ /**
23
+ * `card` sits in a hero corner and uses a scrim so it reads on photography;
24
+ * `detail` sits in a header row beside the title.
25
+ */
26
+ placement?: "card" | "detail";
27
+ className?: string;
28
+ }
29
+ export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
@@ -0,0 +1,142 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Pencil / clone / bin, on a PUBLIC surface, for whoever owns the record.
5
+ *
6
+ * ── What this is for ──────────────────────────────────────────────────────
7
+ *
8
+ * An admin or seller spots a problem on the page a visitor sees — a wrong
9
+ * price, a typo, a bad photo — and today has to navigate to the dashboard,
10
+ * find the list, search for the row and open it. This is the two-click path to
11
+ * the editor from where the problem was noticed.
12
+ *
13
+ * ── Why it is a new component and not a slot on each card ─────────────────
14
+ *
15
+ * 🛑 Measured before building: ZERO public detail views render any owner-only
16
+ * control, and the only precedent anywhere is `CouponCard`, which has had
17
+ * `onEdit` / `onToggleActive` / `onDelete` props the whole time and whose two
18
+ * public consumers pass NONE of them. So there was no pattern to follow and no
19
+ * second implementation to reconcile — one component, used by every surface,
20
+ * is what keeps the visibility rule and the confirm copy in one place.
21
+ *
22
+ * ── The visibility rule, and why it has three states ──────────────────────
23
+ *
24
+ * 🛑 Public pages are static or ISR (`products/[slug]` is `revalidate = 3600`
25
+ * and reads no session), so `SessionProvider` mounts `initialUser={null}` and
26
+ * `loading` starts TRUE on every hard load — for signed-in users too. A
27
+ * two-state `!user → hide` gate therefore renders nothing, then pops the
28
+ * toolbar in a few hundred milliseconds later, on every single page load.
29
+ *
30
+ * So: render NOTHING while `isAuthResolving`. Appearing late is invisible;
31
+ * appearing with a flash is a bug people report. Same reasoning as
32
+ * `useCanSeePrices`, which exists for the mirror-image problem.
33
+ *
34
+ * ── Who sees what ─────────────────────────────────────────────────────────
35
+ *
36
+ * pencil + bin the record's owner, or any admin
37
+ * clone admins ONLY
38
+ *
39
+ * 🛑 Clone is gated on `useIsAdmin()`, NOT on the same check as the other two.
40
+ * `useIsOwner` returns `true` for admins AND for the owner, so reusing it
41
+ * would hand every seller a button that is meant to be a staff tool.
42
+ *
43
+ * @tag domain:admin
44
+ * @tag layer:components
45
+ * @tag access:client
46
+ */
47
+ import { useCallback, useState } from "react";
48
+ import { useIsAdmin, useIsOwner } from "../../auth/hooks/useRBAC.js";
49
+ import { useOptionalSession } from "../../../react/contexts/SessionContext.js";
50
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
51
+ import { ConfirmDeleteModal } from "../../../ui/components/ConfirmDeleteModal.js";
52
+ import { Icon } from "../../../ui/components/Icon.js";
53
+ import { IconButton } from "../../../ui/components/IconButton.js";
54
+ import { Row, useToast } from "../../../ui/index.js";
55
+ import { normalizeError } from "../../../errors/normalize.js";
56
+ import { toUserMessage } from "../../../errors/error-display-map.js";
57
+ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", className = "", }) {
58
+ const session = useOptionalSession();
59
+ const isOwnerOrAdmin = useIsOwner(ownerId);
60
+ const isAdmin = useIsAdmin();
61
+ const { showToast } = useToast();
62
+ const [confirmOpen, setConfirmOpen] = useState(false);
63
+ const [deleting, setDeleting] = useState(false);
64
+ const bucket = ACTIONS[resource];
65
+ const handleConfirmedDelete = useCallback(async () => {
66
+ if (!onDelete)
67
+ return;
68
+ setDeleting(true);
69
+ try {
70
+ await onDelete();
71
+ setConfirmOpen(false);
72
+ }
73
+ catch (err) {
74
+ const normalized = normalizeError(err);
75
+ /*
76
+ * 🛑 A failed delete MUST say so. Without this the dialog simply closed
77
+ * its spinner and the record was still there — and because the caller
78
+ * navigates away on success, "nothing happened" is indistinguishable
79
+ * from "it worked but the page did not move". This is a destructive
80
+ * action on a public page; silence is the worst available answer.
81
+ */
82
+ showToast(toUserMessage(normalized.code ?? "UNKNOWN"), "error");
83
+ }
84
+ finally {
85
+ // Always clears, so a failed delete leaves a usable dialog rather than a
86
+ // permanently-spinning button the user cannot dismiss or retry.
87
+ setDeleting(false);
88
+ }
89
+ }, [onDelete, showToast]);
90
+ /*
91
+ * 🛑 Nothing at all until auth resolves — see the header. `loading` starts
92
+ * true on every hard load of a static page, so this is the common path on
93
+ * first paint, not an edge case.
94
+ */
95
+ if (!session || session.loading)
96
+ return null;
97
+ /*
98
+ * A store-owned record is also "mine" when I own that store.
99
+ * `SessionUser.storeId` holds the store SLUG and `StoreDocument.storeId` is
100
+ * the slug too, while `ownerId` is the Auth UID — two different id spaces,
101
+ * so both have to be checked rather than one standing in for the other.
102
+ */
103
+ const ownsStore = Boolean(storeId && session.user?.storeId === storeId);
104
+ /*
105
+ * 🛑 `isAdmin` is its OWN term, not folded into `useIsOwner`.
106
+ *
107
+ * `useIsOwner` early-returns `false` when `resourceOwnerId` is falsy — BEFORE
108
+ * it checks admin — so for a record with no owner at all it denies admins
109
+ * too. Categories, brands, franchises, carousel slides and homepage sections
110
+ * have no `ownerId`, and those are precisely the collections an admin needs
111
+ * this toolbar on. Relying on `useIsOwner` alone would have hidden it from
112
+ * the only person who can edit them.
113
+ */
114
+ if (!isAdmin && !isOwnerOrAdmin && !ownsStore)
115
+ return null;
116
+ const editDef = bucket.edit;
117
+ const deleteDef = bucket.delete;
118
+ const confirmation = deleteDef?.confirmation;
119
+ const showClone = Boolean(onClone) && isAdmin;
120
+ return (_jsxs(_Fragment, { children: [_jsxs(Row, { gap: "xs", align: "center", className: className,
121
+ /*
122
+ * Stops the click reaching an ancestor `<Link>` or the card's own
123
+ * `onClick`. Without it, pressing Edit on a card both navigates to the
124
+ * editor AND opens the public detail page behind it.
125
+ */
126
+ onClick: (e) => {
127
+ e.stopPropagation();
128
+ }, children: [_jsx(IconButton, { "aria-label": editDef?.ariaLabel ?? editDef?.label ?? "Edit", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "edit" }), onClick: () => {
129
+ /*
130
+ * `window.location`, not `next/link`. This component is rendered
131
+ * inside cards whose whole body is already an anchor, and nesting
132
+ * an `<a>` in an `<a>` is invalid HTML that browsers resolve by
133
+ * closing the outer one early — which breaks the card's own
134
+ * navigation, not just this button's.
135
+ */
136
+ if (typeof window !== "undefined")
137
+ window.location.assign(editHref);
138
+ } }), showClone && (_jsx(IconButton, { "aria-label": (bucket.duplicate?.ariaLabel ?? bucket.duplicate?.label) ||
139
+ "Duplicate", size: "touch", variant: placement === "card" ? "scrim" : "ghost", icon: _jsx(Icon, { name: "duplicate" }), onClick: () => void onClone?.() })), onDelete && (_jsx(IconButton, { "aria-label": deleteDef?.ariaLabel ?? deleteDef?.label ?? "Delete", size: "touch", variant: placement === "card" ? "scrim" : "danger", icon: _jsx(Icon, { name: "delete" }), onClick: () => setConfirmOpen(true) }))] }), onDelete && (_jsx(ConfirmDeleteModal, { isOpen: confirmOpen, onClose: () => setConfirmOpen(false), onConfirm: handleConfirmedDelete, isDeleting: deleting, title: confirmation?.title ?? "Delete this record?", message: recordLabel && confirmation
140
+ ? `${confirmation.body}\n\nDeleting: ${recordLabel}`
141
+ : confirmation?.body, confirmText: confirmation?.confirmLabel ?? deleteDef?.label ?? "Delete", variant: "danger" }))] }));
142
+ }
@@ -12,7 +12,7 @@ export declare const adminStoreUpdateSchema: z.ZodEffects<z.ZodObject<{
12
12
  isVerified: z.ZodOptional<z.ZodBoolean>;
13
13
  isFeatured: z.ZodOptional<z.ZodBoolean>;
14
14
  suspensionReason: z.ZodOptional<z.ZodString>;
15
- capabilities: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
15
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<[string, ...string[]]>, "many">>;
16
16
  adminNotes: z.ZodOptional<z.ZodString>;
17
17
  }, "strip", z.ZodTypeAny, {
18
18
  storeStatus: StoreStatus;
@@ -29,6 +29,7 @@ import { z } from "zod";
29
29
  import { annotate } from "../../shell/field-ui-meta.js";
30
30
  import { mediaUrlSchema } from "../../../validation/schemas.js";
31
31
  import { StoreStatusValues } from "../../stores/schemas/firestore.js";
32
+ import { ALL_STORE_CAPABILITIES } from "../../auth/permissions/constants.js";
32
33
  const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
33
34
  /*
34
35
  * `adminAddressFormSchema` is DELETED (W5 / D19).
@@ -84,7 +85,9 @@ export const adminStoreUpdateSchema = z
84
85
  section: "standing", order: 4, row: "full", kind: "textarea",
85
86
  when: (v) => v.storeStatus === "suspended",
86
87
  }),
87
- capabilities: annotate(z.array(z.string().min(1)).max(50).optional(), {
88
+ // 🛑 z.enum, not z.string().min(1) — see storeFirestoreSchema. A typo'd
89
+ // capability used to save cleanly and grant nothing.
90
+ capabilities: annotate(z.array(z.enum(ALL_STORE_CAPABILITIES)).max(50).optional(), {
88
91
  section: "capabilities", sectionLabel: "Capabilities", order: 1, row: "full", kind: "list",
89
92
  }),
90
93
  adminNotes: annotate(z.string().trim().max(2000).optional(), {
@@ -27,6 +27,7 @@ import { RelatedProducts } from "../../products/components/RelatedProducts.js";
27
27
  import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
28
28
  import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges.js";
29
29
  import { FeatureBadgeList } from "../../products/components/FeatureBadge.js";
30
+ import { productImageUrls } from "../../products/schemas/firestore.js";
30
31
  import { HistoryTracker } from "../../history/components/HistoryTracker.js";
31
32
  import { ShareButton } from "../../products/components/ShareButton.js";
32
33
  import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid.js";
@@ -149,8 +150,12 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
149
150
  ? p.price
150
151
  : 0;
151
152
  const minBidIncrementOverride = typeof p.minBidIncrement === "number" ? p.minBidIncrement : undefined;
152
- const images = Array.isArray(p.images)
153
- ? p.images
153
+ // `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
154
+ // normaliser (it also accepts the legacy `string[]`). Casting it to
155
+ // `string[]` handed the gallery objects and rendered a placeholder.
156
+ const galleryUrls = productImageUrls(p.images);
157
+ const images = galleryUrls.length > 0
158
+ ? galleryUrls
154
159
  : typeof p.mainImage === "string"
155
160
  ? [p.mainImage]
156
161
  : [];
@@ -206,11 +211,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
206
211
  });
207
212
  return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "auction", snapshot: {
208
213
  title: typeof p.title === "string" ? p.title : undefined,
209
- thumb: typeof p.mainImage === "string"
210
- ? p.mainImage
211
- : Array.isArray(p.images) && typeof p.images[0] === "string"
212
- ? p.images[0]
213
- : undefined,
214
+ thumb: typeof p.mainImage === "string" ? p.mainImage : images[0],
214
215
  price: typeof p.price === "number" ? p.price : undefined,
215
216
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
216
217
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
@@ -74,5 +74,15 @@ export interface MarketplaceAuctionCardProps {
74
74
  onNavigate?: (href: string) => void;
75
75
  wishlistActions?: WishlistToggleActions;
76
76
  labels?: MarketplaceAuctionCardLabels;
77
+ /**
78
+ * Owner-only pencil / clone / bin. Pass `<OwnerQuickActions>`; it renders
79
+ * nothing for a visitor who does not own the record.
80
+ *
81
+ * 🛑 Rendered OUTSIDE this card's own anchor, as a sibling. A `<button>`
82
+ * inside an `<a>` is invalid HTML that browsers resolve by closing the
83
+ * anchor early, which breaks the CARD's navigation rather than just the
84
+ * button's — `stopPropagation` stops a click but cannot fix a parse.
85
+ */
86
+ renderOwnerActions?: () => React.ReactNode;
77
87
  }
78
- export declare function MarketplaceAuctionCard({ product, buyoutPrice, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, wishlistActions, labels, }: MarketplaceAuctionCardProps): import("react").JSX.Element;
88
+ export declare function MarketplaceAuctionCard({ product, buyoutPrice, className, variant, selectable, isSelected, onSelect, inWishlist: initialInWishlist, href, hrefBuilder, onNavigate, wishlistActions, labels, renderOwnerActions, }: MarketplaceAuctionCardProps): import("react").JSX.Element;
@@ -94,7 +94,7 @@ function formatCountdownLabel(remaining, endedLabel) {
94
94
  return `${hours}h ${minutes}m ${seconds}s`;
95
95
  return `${minutes}m ${seconds}s`;
96
96
  }
97
- export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, wishlistActions, labels, }) {
97
+ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", variant = "grid", selectable = false, isSelected = false, onSelect, inWishlist: initialInWishlist = false, href, hrefBuilder, onNavigate, wishlistActions, labels, renderOwnerActions, }) {
98
98
  const router = useRouter();
99
99
  const mergedLabels = { ...DEFAULT_LABELS, ...labels };
100
100
  const baseVariant = variant === "list" ? "list" : "grid";
@@ -180,5 +180,12 @@ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", v
180
180
  remaining, resolvedBuyoutPrice, inWishlist, wishlistLoading, wishlistActions,
181
181
  countdownClass, mergedLabels, handleWishlist, handleNavigate,
182
182
  };
183
- return (_jsxs(BaseListingCard, { isSelected: isSelected, isDisabled: isEnded, variant: baseVariant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [renderAuctionCardHero({ product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect }), _jsx(BaseListingCard.Info, { variant: baseVariant, children: baseVariant === "list" ? renderAuctionCardInfoList(cardInfoProps) : renderAuctionCardInfoGrid(cardInfoProps) })] }));
183
+ const card = (_jsxs(BaseListingCard, { isSelected: isSelected, isDisabled: isEnded, variant: baseVariant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [renderAuctionCardHero({ product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect }), _jsx(BaseListingCard.Info, { variant: baseVariant, children: baseVariant === "list" ? renderAuctionCardInfoList(cardInfoProps) : renderAuctionCardInfoGrid(cardInfoProps) })] }));
184
+ if (!renderOwnerActions)
185
+ return card;
186
+ /*
187
+ * Sibling of the card, never a child — see the prop's docstring. `z-20`
188
+ * clears this card's own overlays, which all sit at `z-10`.
189
+ */
190
+ return (_jsxs(Div, { className: "relative h-full", children: [card, _jsx(Div, { className: "absolute right-2 bottom-2 z-20", children: renderOwnerActions() })] }));
184
191
  }
@@ -67,8 +67,18 @@ export declare const ROUTE_PERMISSION_MAP: Record<string, Permission>;
67
67
  *
68
68
  * Defaults on store creation: ["suggest_brands", "create_coupons"]
69
69
  */
70
- export type StoreCapability = "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics" | "whatsapp_catalog_sync";
71
- /** Default capabilities granted to every new store on creation. */
70
+ export type StoreCapability = "advanced_listing_types" | "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics" | "whatsapp_catalog_sync";
71
+ export declare const ALL_STORE_CAPABILITIES: StoreCapability[];
72
+ /** Narrow an untrusted string to a real capability. */
73
+ export declare function isStoreCapability(value: unknown): value is StoreCapability;
74
+ /**
75
+ * Default capabilities granted to every new store on creation.
76
+ *
77
+ * 🛑 THE one definition. `AdminStoreEditorView` hardcoded its own copy, which
78
+ * is also the `??` fallback on its load path — so the two could disagree about
79
+ * what a store with no `capabilities` field is entitled to, and the admin
80
+ * editor's copy is the one that gets SAVED BACK. Import this; never restate it.
81
+ */
72
82
  export declare const DEFAULT_STORE_CAPABILITIES: StoreCapability[];
73
83
  /**
74
84
  * Check whether a permission array includes a required permission.
@@ -87,6 +97,23 @@ export declare function hasAllPermissions(permissions: Permission[], required: P
87
97
  * Check whether a store's capabilities include a specific capability.
88
98
  */
89
99
  export declare function hasCapability(capabilities: StoreCapability[], required: StoreCapability): boolean;
100
+ /**
101
+ * Resolve a store's stored `capabilities` array to its effective set.
102
+ *
103
+ * 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
104
+ * already holds the store document does not pay a second Firestore read to ask
105
+ * what it is entitled to (Rule #6). Three WhatsApp routes were reading
106
+ * `store.capabilities ?? []` inline for exactly that reason, which made them a
107
+ * second, divergent reader of the field — they skipped the default merge, so
108
+ * they and `getStoreCapabilities` could disagree about the same store.
109
+ *
110
+ * Note the empty-array case falls back to the defaults, matching
111
+ * `getStoreCapabilities`' long-standing `raw.length > 0` test. That is
112
+ * arguably the wrong call — an admin who deliberately unticks everything gets
113
+ * the defaults handed back — but it is the established behaviour and changing
114
+ * it would silently re-grant capabilities on stores that had them removed.
115
+ */
116
+ export declare function resolveStoreCapabilities(raw: readonly string[] | null | undefined): StoreCapability[];
90
117
  /**
91
118
  * Groups permission prefixes into human-labeled domains for display —
92
119
  * used by the employee-permission editor and the read-only permissions