@mohasinac/appkit 4.27.0 → 4.28.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 (271) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
  3. package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
  4. package/dist/_internal/server/features/checkout/actions.js +6 -4
  5. package/dist/_internal/server/features/classified/metadata.js +3 -1
  6. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  7. package/dist/_internal/server/features/live/metadata.js +3 -1
  8. package/dist/_internal/server/features/refunds/actions.js +3 -2
  9. package/dist/_internal/server/features/reviews/actions.js +3 -2
  10. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  11. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  12. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  13. package/dist/_internal/shared/actions/action-registry.js +14 -0
  14. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  15. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  16. package/dist/_internal/shared/features/checkout/config.js +6 -3
  17. package/dist/client.d.ts +32 -2
  18. package/dist/client.js +37 -2
  19. package/dist/constants/api-endpoints.d.ts +21 -3
  20. package/dist/constants/api-endpoints.js +14 -1
  21. package/dist/constants/field-names.d.ts +2 -2
  22. package/dist/constants/field-names.js +2 -2
  23. package/dist/core/newsletter.repository.js +14 -1
  24. package/dist/features/about/components/FAQPageView.js +1 -1
  25. package/dist/features/about/components/FeesView.js +1 -1
  26. package/dist/features/account/components/AddressForm.d.ts +2 -0
  27. package/dist/features/account/components/AddressForm.js +5 -1
  28. package/dist/features/account/components/UserBidsView.js +1 -1
  29. package/dist/features/account/components/UserSidebar.js +1 -1
  30. package/dist/features/account/components/UserSupportView.js +24 -2
  31. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  32. package/dist/features/account/hooks/useAddresses.js +4 -1
  33. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  34. package/dist/features/account/hooks/useProfile.js +1 -1
  35. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  36. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  37. package/dist/features/admin/actions/chat-actions.js +5 -1
  38. package/dist/features/admin/components/AdminAdsView.js +1 -1
  39. package/dist/features/admin/components/AdminBidsView.js +6 -1
  40. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  41. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  42. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  43. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  44. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  45. package/dist/features/admin/components/AdminSidebar.js +1 -1
  46. package/dist/features/admin/components/AdminViewCards.js +2 -2
  47. package/dist/features/admin/components/DataTable.js +2 -2
  48. package/dist/features/admin/schemas/firestore.js +9 -3
  49. package/dist/features/auctions/actions/bid-actions.js +45 -4
  50. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  51. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  52. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  53. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  54. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  55. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  56. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  57. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  58. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  59. package/dist/features/auth/hooks/useAuth.js +27 -1
  60. package/dist/features/auth/permissions/constants.d.ts +1 -1
  61. package/dist/features/auth/permissions/constants.js +3 -0
  62. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  63. package/dist/features/auth/repository/session.repository.js +23 -0
  64. package/dist/features/auth/repository/user.repository.js +13 -2
  65. package/dist/features/blog/components/BlogListView.js +2 -2
  66. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  67. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  68. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  69. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  70. package/dist/features/cart/components/index.d.ts +1 -1
  71. package/dist/features/cart/components/index.js +1 -1
  72. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  73. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  74. package/dist/features/cart/utils/line-total.d.ts +25 -0
  75. package/dist/features/cart/utils/line-total.js +26 -0
  76. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  77. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  78. package/dist/features/categories/components/CategoryTree.js +2 -2
  79. package/dist/features/contact/components/ContactForm.js +1 -1
  80. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  81. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  82. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  83. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  84. package/dist/features/events/components/EventRafflesSection.js +14 -2
  85. package/dist/features/events/components/EventStatusBadge.js +1 -1
  86. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  87. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  88. package/dist/features/faq/components/ContactCTA.js +1 -1
  89. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  90. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  91. package/dist/features/faq/repository/faqs.repository.js +64 -54
  92. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  93. package/dist/features/faq/schemas/firestore.js +2 -2
  94. package/dist/features/faq/schemas/index.d.ts +3 -3
  95. package/dist/features/faq/schemas/index.js +1 -1
  96. package/dist/features/filters/FilterFacetSection.js +2 -2
  97. package/dist/features/forms/Toggle.js +1 -1
  98. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  99. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  100. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  101. package/dist/features/homepage/components/BrandsSection.js +1 -1
  102. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  103. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  104. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  105. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  106. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  107. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  108. package/dist/features/homepage/lib/section-renderer.js +2 -1
  109. package/dist/features/layout/AppLayoutShell.js +5 -5
  110. package/dist/features/layout/BottomActions.js +5 -5
  111. package/dist/features/layout/BottomSheet.js +1 -1
  112. package/dist/features/layout/MainNavbar.js +1 -1
  113. package/dist/features/layout/NavbarLayout.js +1 -1
  114. package/dist/features/layout/TitleBarLayout.js +3 -3
  115. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  116. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  117. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  118. package/dist/features/lottery/schemas/config-write.js +139 -0
  119. package/dist/features/media/MediaPickerModal.js +3 -1
  120. package/dist/features/media/MediaVideo.js +4 -2
  121. package/dist/features/media/components/MediaSlider.js +1 -1
  122. package/dist/features/orders/components/OrdersList.js +2 -2
  123. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  124. package/dist/features/payments/repository/payout.repository.js +14 -6
  125. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  126. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  127. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  128. package/dist/features/products/columns/productTableColumns.js +3 -3
  129. package/dist/features/products/components/CompareOverlay.js +1 -1
  130. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  131. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  132. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  133. package/dist/features/products/components/ProductDetailActions.js +11 -7
  134. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  135. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  136. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  137. package/dist/features/products/components/ProductFilters.js +15 -8
  138. package/dist/features/products/components/ProductGrid.js +8 -17
  139. package/dist/features/products/components/ProductTabs.js +7 -4
  140. package/dist/features/products/components/ProductTabsShell.js +2 -7
  141. package/dist/features/products/constants/action-defs.d.ts +21 -4
  142. package/dist/features/products/constants/action-defs.js +7 -3
  143. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  144. package/dist/features/products/repository/products.repository.d.ts +1 -1
  145. package/dist/features/products/repository/products.repository.js +27 -13
  146. package/dist/features/products/schemas/firestore.d.ts +3 -2
  147. package/dist/features/products/schemas/firestore.js +1 -1
  148. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  149. package/dist/features/reviews/actions/review-actions.js +14 -3
  150. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  151. package/dist/features/reviews/components/ReviewModal.js +1 -1
  152. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  153. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  154. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  155. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  156. package/dist/features/search/components/Search.js +3 -3
  157. package/dist/features/search/components/SearchResultsSection.js +1 -1
  158. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  159. package/dist/features/seller/components/SellerBidsView.js +6 -1
  160. package/dist/features/seller/components/SellerSidebar.js +1 -1
  161. package/dist/features/shell/SectionForm.d.ts +21 -1
  162. package/dist/features/shell/SectionForm.js +15 -3
  163. package/dist/features/shell/build-sections.js +10 -1
  164. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  165. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  166. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  167. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  168. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  169. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  170. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  171. package/dist/features/stores/actions/store-query-actions.js +25 -1
  172. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  173. package/dist/features/stores/components/StoreHeader.js +1 -1
  174. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  175. package/dist/features/support/repository/support.repository.d.ts +12 -0
  176. package/dist/features/support/repository/support.repository.js +15 -0
  177. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  178. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  179. package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
  180. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
  181. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  182. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  183. package/dist/features/wishlist/components/index.d.ts +2 -1
  184. package/dist/features/wishlist/components/index.js +1 -1
  185. package/dist/index.d.ts +10 -3
  186. package/dist/index.js +19 -4
  187. package/dist/monitoring/error-tracking.js +39 -5
  188. package/dist/monitoring/server-logger.js +70 -32
  189. package/dist/next/components/ErrorView.js +1 -1
  190. package/dist/next/routing/route-map.d.ts +39 -7
  191. package/dist/next/routing/route-map.js +34 -4
  192. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  193. package/dist/providers/db-firebase/base.repository.js +56 -9
  194. package/dist/react/ErrorBoundary.js +9 -1
  195. package/dist/react/contexts/SessionContext.js +6 -2
  196. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  197. package/dist/react/hooks/useAuthGate.js +86 -7
  198. package/dist/schemas/registry.d.ts +3 -3
  199. package/dist/security/index.d.ts +1 -1
  200. package/dist/security/index.js +1 -1
  201. package/dist/security/pii-encrypt.d.ts +13 -5
  202. package/dist/security/pii-encrypt.js +132 -6
  203. package/dist/security/pii-schemas.d.ts +20 -4
  204. package/dist/security/pii-schemas.js +23 -5
  205. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  206. package/dist/seed/_helpers/product-search-txt.js +33 -0
  207. package/dist/seed/faq-seed-data.d.ts +16 -0
  208. package/dist/seed/faq-seed-data.js +22 -1
  209. package/dist/seed/products-art-seed-data.js +49 -7
  210. package/dist/seed/products-auctions-seed-data.js +12 -4
  211. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  212. package/dist/seed/products-classifieds-seed-data.js +65 -9
  213. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  214. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  215. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  216. package/dist/seed/products-live-items-seed-data.js +33 -5
  217. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  218. package/dist/seed/products-preorders-seed-data.js +6 -2
  219. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  220. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  221. package/dist/seed/products-standard-seed-data.js +6 -2
  222. package/dist/seed/products-stickers-seed-data.js +49 -7
  223. package/dist/seed/site-settings-seed-data.js +5 -3
  224. package/dist/seo/json-ld.d.ts +44 -2
  225. package/dist/seo/json-ld.js +57 -11
  226. package/dist/styles.css +368 -90
  227. package/dist/tailwind-utilities.css +1 -1
  228. package/dist/tokens/index.d.ts +1 -1
  229. package/dist/tokens/index.js +1 -1
  230. package/dist/tokens/themes/default-dark.js +6 -0
  231. package/dist/tokens/themes/default-light.js +5 -0
  232. package/dist/tokens/tokens.css +84 -0
  233. package/dist/ui/DataTable.js +1 -1
  234. package/dist/ui/DataTable.style.css +3 -3
  235. package/dist/ui/components/BulkActionBar.style.css +1 -3
  236. package/dist/ui/components/Button.js +15 -6
  237. package/dist/ui/components/Dropdown.style.css +1 -4
  238. package/dist/ui/components/FilterChipGroup.js +1 -1
  239. package/dist/ui/components/FilterDrawer.style.css +1 -1
  240. package/dist/ui/components/Icon.d.ts +53 -0
  241. package/dist/ui/components/Icon.js +41 -0
  242. package/dist/ui/components/IconButton.d.ts +13 -0
  243. package/dist/ui/components/IconButton.js +13 -0
  244. package/dist/ui/components/IconButton.style.css +18 -10
  245. package/dist/ui/components/ListingLayout.style.css +2 -10
  246. package/dist/ui/components/ListingToolbar.js +6 -6
  247. package/dist/ui/components/Modal.js +1 -1
  248. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  249. package/dist/ui/components/Pagination.style.css +1 -6
  250. package/dist/ui/components/RichTextEditor.js +1 -1
  251. package/dist/ui/components/RowActionMenu.style.css +1 -1
  252. package/dist/ui/components/SideDrawer.style.css +1 -1
  253. package/dist/ui/components/Slider.js +1 -1
  254. package/dist/ui/components/TabBarShell.d.ts +78 -0
  255. package/dist/ui/components/TabBarShell.js +95 -0
  256. package/dist/ui/components/Tabs.js +12 -8
  257. package/dist/ui/components/Tabs.style.css +249 -41
  258. package/dist/ui/components/ViewToggle.style.css +1 -5
  259. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  260. package/dist/ui/components/tabs-overflow.js +142 -0
  261. package/dist/ui/icons/icon-registry.d.ts +38 -0
  262. package/dist/ui/icons/icon-registry.js +37 -0
  263. package/dist/ui/index.d.ts +6 -0
  264. package/dist/ui/index.js +3 -0
  265. package/dist/utils/index.d.ts +1 -1
  266. package/dist/utils/index.js +1 -1
  267. package/dist/utils/media-url.d.ts +27 -4
  268. package/dist/utils/media-url.js +37 -6
  269. package/dist/utils/search-txt.d.ts +71 -0
  270. package/dist/utils/search-txt.js +118 -0
  271. package/package.json +1 -1
@@ -9,6 +9,8 @@ import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/const
9
9
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
10
10
  import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
11
11
  import { normalizeError } from "../../../errors/normalize";
12
+ import { TextLink } from "../../../ui";
13
+ import { ROUTES } from "../../../constants/index";
12
14
  const __O = {
13
15
  xAuto: "overflow-x-auto",
14
16
  };
@@ -141,7 +143,7 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
141
143
  const handleDelete = (addr) => setDeleteTargetAddr(addr);
142
144
  const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
143
145
  const setField = (key) => (value) => set(key, value);
144
- return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
146
+ return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
145
147
  const isBanned = addr.banStatus === "banned";
146
148
  return (_jsxs(Tr, { className: `border-t border-[var(--appkit-color-border)] ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx(Td, { padding: "sm", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { children: addr.label }), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }) }), _jsx(Td, { padding: "sm", children: addr.fullName }), _jsxs(Td, { padding: "sm", children: [addr.city, ", ", addr.state] }), _jsx(Td, { className: "tabular-nums", padding: "sm", children: addr.phone }), _jsx(Td, { className: "text-right", padding: "sm", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(Row, { justify: "end", gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label })] })) })] }, addr.id));
147
149
  }) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs(Stack, { gap: "md", padding: "y-2xs", children: [saveError && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError })), _jsx(FieldInput, { name: "label", label: "Label *", hint: "e.g. Warehouse, Shop, Home", type: "text", value: draft.label, onChange: setField("label"), placeholder: "Warehouse", maxLength: 60 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "fullName", label: "Full Name *", type: "text", value: draft.fullName, onChange: setField("fullName"), placeholder: "Ravi Kumar", maxLength: 100 }), _jsx(FieldInput, { name: "phone", label: "Phone *", type: "tel", value: draft.phone, onChange: setField("phone"), placeholder: "+91 98765 43210", maxLength: 20 })] }), _jsx(FieldInput, { name: "addressLine1", label: "Address Line 1 *", type: "text", value: draft.addressLine1, onChange: setField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200 }), _jsx(FieldInput, { name: "addressLine2", label: "Address Line 2", type: "text", value: draft.addressLine2, onChange: setField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200 }), _jsx(FieldInput, { name: "landmark", label: "Landmark", type: "text", value: draft.landmark, onChange: setField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "city", label: "City *", type: "text", value: draft.city, onChange: setField("city"), placeholder: "Mumbai", maxLength: 100 }), _jsx(FieldInput, { name: "state", label: "State *", type: "text", value: draft.state, onChange: setField("state"), placeholder: "Maharashtra", maxLength: 100 })] }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "postalCode", label: "Postal Code *", type: "text", value: draft.postalCode, onChange: setField("postalCode"), placeholder: "400001", maxLength: 10 }), _jsx(FieldInput, { name: "country", label: "Country *", type: "text", value: draft.country, onChange: setField("country"), placeholder: "India", maxLength: 60 })] }), _jsx(FieldCheckbox, { name: "isDefault", label: "Set as default pickup address", checked: draft.isDefault, onChange: (c) => set("isDefault", c) })] }) }), deleteTargetAddr && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Address", message: `Delete address "${deleteTargetAddr.label}"? This cannot be undone.`, onConfirm: () => { deleteById(deleteTargetAddr.id); setDeleteTargetAddr(null); }, onClose: () => setDeleteTargetAddr(null), isDeleting: deletingId === deleteTargetAddr.id }))] }));
@@ -13,6 +13,8 @@ import { SELLER_BID_STATUS_TABS } from "../../admin/constants/filter-tabs";
13
13
  import { toRecordArray, toRelativeDate, toCurrency, toStringValue } from "../hooks/useSellerListingData";
14
14
  import { DataTable } from "../../admin/components/DataTable";
15
15
  import { DataListingView } from "../../admin/components/DataListingView";
16
+ import { TextLink } from "../../../ui";
17
+ import { ROUTES } from "../../../constants/index";
16
18
  const DEFAULT_SORT = "-bidDate";
17
19
  const SORT_OPTIONS = [
18
20
  { value: sortBy("bidDate", "DESC"), label: "Newest" },
@@ -141,5 +143,8 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
141
143
  },
142
144
  };
143
145
  const detailBadge = detail ? bidStatusBadge(detail.status) : null;
144
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined })] }));
146
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined, footer: detail ? (
147
+ // The full page — a modal cannot be linked, bookmarked or reloaded
148
+ // into, which is the whole reason the page exists.
149
+ _jsx(TextLink, { href: String(ROUTES.STORE.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined })] }));
145
150
  }
@@ -24,7 +24,7 @@ const CLS_NAV_ICON_ACTIVE = "text-warning dark:text-warning";
24
24
  function NavLink({ item, isActive, onClick }) {
25
25
  return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
26
26
  ? CLS_NAV_ACTIVE
27
- : "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
27
+ : "text-[var(--appkit-color-text-muted)] hover:bg-surface-hover hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
28
28
  }
29
29
  function FlatContent({ items, activeHref, storeName, storeLogoURL, onItemClick, }) {
30
30
  const activeItem = findActiveNavItem(items, activeHref);
@@ -70,6 +70,26 @@ export interface SectionFormProps<T extends object = Record<string, JsonValue>>
70
70
  */
71
71
  onCancel?: () => void;
72
72
  cancelLabel?: string;
73
+ /**
74
+ * A destructive action for this record — almost always Delete.
75
+ *
76
+ * Added because an edit page nearly always has one and `SectionForm` had
77
+ * nowhere to put it, which left two bad options: hand-roll a row and pass
78
+ * `hideActions` (which ALSO silences the pinned mobile bar, since it is
79
+ * gated `bottomBar && !hideActions` — and that bar is the main reason to
80
+ * sectionise a short form), or render it outside the form, where every page
81
+ * invents its own placement.
82
+ *
83
+ * Rendered LEFT-aligned, opposite Save, so it is not adjacent to the
84
+ * primary action — a Delete beside Save is a misclick waiting to happen.
85
+ * `confirm` is expected to come from an ActionDef's `confirmation` config
86
+ * (Rule #7); this only carries the label and handler.
87
+ */
88
+ destructiveAction?: {
89
+ label: string;
90
+ onClick: () => void;
91
+ disabled?: boolean;
92
+ };
73
93
  /**
74
94
  * Publish Save/Cancel and the error sheet into the mobile bottom-chrome
75
95
  * tier. On by default: a SectionForm is a page-level form, which is exactly
@@ -109,5 +129,5 @@ export interface UseSectionFormNavResult<T extends object> {
109
129
  export declare function useSectionFormNav<T extends object = Record<string, JsonValue>>(sections: SectionDef<T>[], values?: T): UseSectionFormNavResult<T>;
110
130
  /** DOM id for a section panel — kept in one place so nav and render agree. */
111
131
  export declare function sectionAnchorId(id: string): string;
112
- export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
132
+ export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
113
133
  export default SectionForm;
@@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "r
4
4
  import { Button } from "../../ui/components/Button";
5
5
  import { CollapsibleSection } from "../../ui/components/CollapsibleSection";
6
6
  import { Badge } from "../../ui/components/Badge";
7
- import { Row, Span, Stack } from "../../ui";
7
+ import { Div, Row, Span, Stack } from "../../ui";
8
8
  import { FormShellContext } from "../../ui/forms/FormShell";
9
9
  import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
10
10
  import { FormSchemaContext } from "./FormShell";
@@ -55,7 +55,7 @@ export function useSectionFormNav(sections, values) {
55
55
  export function sectionAnchorId(id) {
56
56
  return `section-${id}`;
57
57
  }
58
- export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
58
+ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
59
59
  const inheritedSchema = useContext(FormSchemaContext);
60
60
  /*
61
61
  * SectionForm deliberately does not OWN a FormShellContext (the owning view
@@ -124,6 +124,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
124
124
  submitLabel,
125
125
  cancelLabel,
126
126
  isLoading,
127
+ destructiveAction,
127
128
  enabled: bottomBar && !hideActions,
128
129
  });
129
130
  const toggle = useCallback((id) => {
@@ -149,6 +150,17 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
149
150
  toggle(section.id); }, keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
150
151
  ? () => (_jsxs(Badge, { variant: "danger", children: [errorCount, " ", errorCount === 1 ? "issue" : "issues"] }))
151
152
  : undefined, children: section.render({ values, onChange: handleFieldChange, errors: fieldErrors }) }, section.id));
152
- }), !hideActions && (_jsx(Row, { justify: "end", paddingY: "y-sm", children: _jsx(Button, { variant: "primary", type: "submit", onClick: handleSubmit, isLoading: isLoading, disabled: isLoading, children: submitLabel }) }))] }));
153
+ }), !hideActions && (
154
+ /*
155
+ * `justify="between"` so a destructive action sits opposite Save
156
+ * rather than beside it. With no destructive action the empty <Div>
157
+ * still holds the left edge, keeping Save right-aligned.
158
+ *
159
+ * Cancel is rendered HERE as well as in the mobile bar. It used to
160
+ * exist only in the bar, so every page converted to SectionForm
161
+ * silently lost its Cancel button on desktop — found converting
162
+ * `store/categories/new`, which had one before.
163
+ */
164
+ _jsxs(Row, { justify: "between", align: "center", paddingY: "y-sm", gap: "sm", wrap: true, children: [_jsx(Div, { children: destructiveAction && (_jsx(Button, { variant: "danger", type: "button", onClick: destructiveAction.onClick, disabled: destructiveAction.disabled || isLoading, children: destructiveAction.label })) }), _jsxs(Row, { gap: "sm", children: [onCancel && (_jsx(Button, { variant: "ghost", type: "button", onClick: onCancel, disabled: isLoading, children: cancelLabel })), _jsx(Button, { variant: "primary", type: "submit", onClick: handleSubmit, isLoading: isLoading, disabled: isLoading, children: submitLabel })] })] }))] }));
153
165
  }
154
166
  export default SectionForm;
@@ -165,7 +165,16 @@ export function buildSectionsFromSchema(schema, opts = {}) {
165
165
  // in-flight media upload. Inference sets this for every media field.
166
166
  keepMounted: sectionFields.some((f) => f.meta.sectionKeepMounted),
167
167
  fields: sectionFields.map((f) => f.name),
168
- render: ({ values, onChange, errors }) => (_jsx(_Fragment, { children: packRows(sectionFields).map((row, i) => (_jsx(FormGroup, { columns: ROW_COLUMNS[row[0].meta.row ?? "pair"], children: row.map((field) => renderField(field, { values, onChange, errors }, opts)) }, `${sectionId}-row-${i}`))) })),
168
+ render: ({ values, onChange, errors }) => {
169
+ /*
170
+ * Filtered per render, not once at build time — the predicate reads
171
+ * the CURRENT values, so a field appears the moment its condition
172
+ * becomes true. Rows are packed after filtering so a hidden field
173
+ * does not leave a gap in a two-up row.
174
+ */
175
+ const visible = sectionFields.filter((f) => !f.meta.when || f.meta.when(values));
176
+ return (_jsx(_Fragment, { children: packRows(visible).map((row, i) => (_jsx(FormGroup, { columns: ROW_COLUMNS[row[0].meta.row ?? "pair"], children: row.map((field) => renderField(field, { values, onChange, errors }, opts)) }, `${sectionId}-row-${i}`))) }));
177
+ },
169
178
  };
170
179
  });
171
180
  }
@@ -1,4 +1,5 @@
1
1
  import type { ZodTypeAny } from "zod";
2
+ import type { FormValues } from "../../schemas/types";
2
3
  /** How much of a row a field claims on desktop. Everything is 1-up on mobile. */
3
4
  export type FieldRow = "full" | "pair" | "third" | "quarter";
4
5
  /** Which input primitive renders the field, and its default row span. */
@@ -29,6 +30,26 @@ export interface FieldUiMeta {
29
30
  * which discards an in-flight media upload.
30
31
  */
31
32
  sectionKeepMounted?: boolean;
33
+ /**
34
+ * Render this field only when the predicate holds.
35
+ *
36
+ * `SectionDef` has had a section-level `when` since W0; this is the
37
+ * per-FIELD equivalent, and without it a whole class of form cannot be
38
+ * derived at all — a shipping config shows "Price per kg" for a flat rate,
39
+ * a payment method shows bank fields for a UPI account. Those pages were
40
+ * stuck hand-writing their fields.
41
+ *
42
+ * A predicate is a UI concern living on a schema that the ROUTE also parses.
43
+ * That is already true of every key here (`section`, `row`, `kind`), and the
44
+ * server ignores the registry entirely — annotations key a WeakMap, they are
45
+ * not part of the parsed shape.
46
+ *
47
+ * 🛑 Hiding a field does NOT relax its schema. A required field hidden by
48
+ * `when` still fails validation, invisibly. Pair it with `.optional()` plus
49
+ * a `superRefine` that requires it under the same condition — the way
50
+ * `supportTicketCreateSchema` handles `orderId` for `order_issue`.
51
+ */
52
+ when?: (values: FormValues) => boolean;
32
53
  /** Row span. Drives the `<FormGroup columns>` packing. */
33
54
  row?: FieldRow;
34
55
  /** Control kind. Picks the primitive and the default row span. */
@@ -4,6 +4,8 @@ import React from "react";
4
4
  import { useApiMutation } from "@mohasinac/appkit/client";
5
5
  import { Alert, Badge, Button, Div, FieldCheckbox, FieldInput, Grid, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui";
6
6
  import { apiClient } from "../../../http";
7
+ import { createShipmentItemSchema } from "../schemas/validation";
8
+ import { ValidationError } from "../../../errors/validation-error";
7
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
10
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
9
11
  import { useShipmentItems } from "../hooks/useShipments";
@@ -42,12 +44,27 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
42
44
  const [linkTarget, setLinkTarget] = React.useState(null);
43
45
  const addItemMutation = useApiMutation({
44
46
  successMessage: "Item added",
45
- mutationFn: () => apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEMS(shipmentId, lotId), {
46
- title,
47
- quantity: Number(quantity),
48
- isForSelfUse,
49
- price: isForSelfUse ? undefined : Math.round(Number(priceRupees) * 100) / 100,
50
- }),
47
+ mutationFn: () => {
48
+ /*
49
+ * `createShipmentItemSchema` already validated this route and had no
50
+ * client consumer, so its rule — a projected sale price is required
51
+ * unless the item is for self use — only ever surfaced as a 400 that
52
+ * named no field. `Number(priceRupees)` also yields NaN on a typo,
53
+ * which the schema rejects and the old payload sent.
54
+ */
55
+ const parsed = createShipmentItemSchema.safeParse({
56
+ title,
57
+ quantity: Number(quantity),
58
+ isForSelfUse,
59
+ price: isForSelfUse ? undefined : Math.round(Number(priceRupees) * 100) / 100,
60
+ });
61
+ if (!parsed.success) {
62
+ // ValidationError carries the issue list; a bare Error flattens it to
63
+ // one string and loses which field failed.
64
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Check the item details.", parsed.error.issues);
65
+ }
66
+ return apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEMS(shipmentId, lotId), parsed.data);
67
+ },
51
68
  onSuccess: () => {
52
69
  setTitle("");
53
70
  setQuantity("1");
@@ -57,7 +57,7 @@ const CATEGORY_COLORS = {
57
57
  "Messaging": "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300",
58
58
  "Seller": "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
59
59
  "Orders": "bg-lime-100 text-lime-800 dark:bg-lime-900/30 dark:text-lime-300",
60
- "Account": "bg-[var(--appkit-color-surface)] text-zinc-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
60
+ "Account": "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
61
61
  };
62
62
  const ALL_ACTION_IDS = Object.values(ACTION_ID);
63
63
  export function ActionPermissionsManager({ initialConfig, onUpdate }) {
@@ -1,15 +1,32 @@
1
1
  /**
2
2
  * S-STORE Extension Repositories
3
3
  *
4
- * 11 lightweight BaseRepository subclasses. None of these collections store
5
- * PII directly so no `createWithId` override is required (Pattern #9). Uses
6
- * the BaseRepository public API only: `findById`, `findBy`, `findAll`,
7
- * `create`, `update`, `delete`.
4
+ * 11 lightweight BaseRepository subclasses.
5
+ *
6
+ * This header used to claim "none of these collections store PII directly" —
7
+ * that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
8
+ * `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
9
+ * and BaseRepository's write hooks encrypt them on every write path, so no
10
+ * per-method override is needed (which is the point of the hooks — Pattern #9
11
+ * described overrides that never existed).
12
+ *
13
+ * The other ten hold no PII and use the BaseRepository public API only.
8
14
  */
9
15
  import { BaseRepository } from "../../../providers/db-firebase";
16
+ import type { DocumentSnapshot } from "firebase-admin/firestore";
10
17
  import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
11
18
  export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
19
+ /**
20
+ * Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
21
+ *
22
+ * This repository had no encryption at all. Nothing queries these by equality
23
+ * (`listByStore` filters on `storeId`), so no read path depended on their
24
+ * plaintext.
25
+ */
26
+ protected piiFields: readonly ["upiVpa", "accountNumber", "ifscCode"];
12
27
  constructor();
28
+ /** Decrypt on read so callers see the shape they wrote. */
29
+ protected mapDoc<D = PayoutMethodDocument>(snap: DocumentSnapshot): D;
13
30
  listByStore(storeId: string): Promise<{
14
31
  items: PayoutMethodDocument[];
15
32
  }>;
@@ -1,16 +1,36 @@
1
1
  /**
2
2
  * S-STORE Extension Repositories
3
3
  *
4
- * 11 lightweight BaseRepository subclasses. None of these collections store
5
- * PII directly so no `createWithId` override is required (Pattern #9). Uses
6
- * the BaseRepository public API only: `findById`, `findBy`, `findAll`,
7
- * `create`, `update`, `delete`.
4
+ * 11 lightweight BaseRepository subclasses.
5
+ *
6
+ * This header used to claim "none of these collections store PII directly" —
7
+ * that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
8
+ * `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
9
+ * and BaseRepository's write hooks encrypt them on every write path, so no
10
+ * per-method override is needed (which is the point of the hooks — Pattern #9
11
+ * described overrides that never existed).
12
+ *
13
+ * The other ten hold no PII and use the BaseRepository public API only.
8
14
  */
9
15
  import { BaseRepository } from "../../../providers/db-firebase";
16
+ import { decryptPiiFields } from "../../../security/pii-encrypt";
10
17
  import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore";
11
18
  export class PayoutMethodsRepository extends BaseRepository {
12
19
  constructor() {
13
20
  super(PAYOUT_METHODS_COLLECTION);
21
+ /**
22
+ * Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
23
+ *
24
+ * This repository had no encryption at all. Nothing queries these by equality
25
+ * (`listByStore` filters on `storeId`), so no read path depended on their
26
+ * plaintext.
27
+ */
28
+ this.piiFields = ["upiVpa", "accountNumber", "ifscCode"];
29
+ }
30
+ /** Decrypt on read so callers see the shape they wrote. */
31
+ mapDoc(snap) {
32
+ const raw = super.mapDoc(snap);
33
+ return decryptPiiFields(raw, [...this.piiFields]);
14
34
  }
15
35
  async listByStore(storeId) {
16
36
  const items = await this.findBy("storeId", storeId);
@@ -66,17 +66,30 @@ export const shippingConfigFormSchema = z
66
66
  method: annotate(shippingMethodSchema, {
67
67
  section: "basics", quick: true, order: 2, row: "pair",
68
68
  }),
69
+ /*
70
+ * Each rate field shows only for the method it belongs to. A flat-rate
71
+ * rule has no price-per-kg, and showing one invites a seller to fill in a
72
+ * number the checkout will never read.
73
+ *
74
+ * These pair with the `superRefine` below, which requires the field under
75
+ * the SAME condition — hiding a required field would otherwise fail
76
+ * validation invisibly.
77
+ */
69
78
  flatRate: annotate(amount("Flat rate").optional(), {
70
79
  section: "rates", sectionLabel: "Rates", order: 1, row: "pair",
80
+ when: (v) => v.method === "flat",
71
81
  }),
72
82
  pricePerKg: annotate(amount("Price per kg").optional(), {
73
83
  section: "rates", order: 2, row: "pair",
84
+ when: (v) => v.method === "weight",
74
85
  }),
75
86
  freeAbove: annotate(amount("Free above").optional(), {
76
87
  section: "rates", order: 3, row: "pair",
88
+ when: (v) => v.method === "free",
77
89
  }),
78
90
  expressSurcharge: annotate(amount("Express surcharge").optional(), {
79
91
  section: "rates", order: 4, row: "pair",
92
+ when: (v) => v.method === "express",
80
93
  }),
81
94
  estimatedDays: annotate(z.coerce.number().int("Estimated days must be a whole number.").min(0).max(365).optional(), { section: "rates", order: 5, row: "pair" }),
82
95
  isDefault: annotate(z.boolean(), {
@@ -17,7 +17,7 @@
17
17
  * parses this: `storeId` is set from the session, never from the body, so a
18
18
  * caller cannot write a category into someone else's store by adding a field.
19
19
  *
20
- * `productIds` and `isActive` are deliberately absent: they are managed after
20
+ * `productIds` is deliberately absent: it is managed after
21
21
  * creation (assigning products, toggling visibility), not typed into the
22
22
  * create form, and the repository supplies their defaults.
23
23
  *
@@ -4,6 +4,7 @@
4
4
  * Public and seller-facing read functions for store listings, products,
5
5
  * auctions and reviews. No auth — callers decide whether auth is needed.
6
6
  */
7
+ import { cache } from "react";
7
8
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
8
9
  import { NotFoundError } from "../../../errors";
9
10
  import { filterTestDataForViewer, filterSingleTestData } from "../../../_internal/server/features/tester/visibility";
@@ -26,8 +27,31 @@ export async function listStores(params = {}, viewer) {
26
27
  const result = await storeRepository.listStores(model);
27
28
  return { ...result, items: filterTestDataForViewer(result.items, viewer) };
28
29
  }
30
+ /**
31
+ * Request-scoped memo of the raw store fetch.
32
+ *
33
+ * CLAUDE.md § SSR Architecture requires `React.cache` on any data-fetch shared
34
+ * by a page and its `generateMetadata`. `stores/[storeSlug]/layout.tsx` awaits
35
+ * `getStoreBySlug` in BOTH — so every store page view was doing two identical
36
+ * Firestore reads.
37
+ *
38
+ * 🛑 The cache wraps the REPOSITORY call, not `getStoreBySlug`, and that is
39
+ * deliberate. `React.cache` memoises per argument list, and the two call sites
40
+ * pass different arguments — `getStoreBySlug(slug)` in `generateMetadata`,
41
+ * `getStoreBySlug(slug, viewer)` in the layout body. Wrapping the exported
42
+ * function would produce two cache entries and dedupe nothing.
43
+ *
44
+ * Making them match by reading the session in `generateMetadata` would be worse
45
+ * still: `getServerSessionUser()` is a dynamic API, and a dynamic API reached
46
+ * from metadata opts the route out of static rendering — the exact defect that
47
+ * made the whole site uncacheable (see `src/app/[locale]/layout.tsx`).
48
+ *
49
+ * So the read is cached on `storeSlug` alone and the viewer-specific
50
+ * `filterSingleTestData` is applied outside it, per caller.
51
+ */
52
+ const fetchStoreBySlug = cache((storeSlug) => storeRepository.findBySlug(storeSlug));
29
53
  export async function getStoreBySlug(storeSlug, viewer) {
30
- const store = await storeRepository.findBySlug(storeSlug);
54
+ const store = await fetchStoreBySlug(storeSlug);
31
55
  return filterSingleTestData(store, viewer);
32
56
  }
33
57
  export async function getStoreProducts(storeSlug, params = {}, viewer) {
@@ -25,5 +25,5 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
25
25
  ].join(" "), 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: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
26
26
  e.preventDefault();
27
27
  onSelect(store.id, !isSelected);
28
- }, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-white border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
28
+ }, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
29
29
  }
@@ -8,7 +8,7 @@ import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge";
8
8
  const __O = {
9
9
  hidden: "overflow-hidden",
10
10
  };
11
- const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-white border-[var(--appkit-color-border-subtle)] bg-warning-surface dark:bg-warning-surface flex items-center justify-center text-warning dark:text-warning font-bold text-[length:var(--appkit-text-2xl)] shadow-sm";
11
+ const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-[var(--appkit-color-border-subtle)] bg-warning-surface dark:bg-warning-surface flex items-center justify-center text-warning dark:text-warning font-bold text-[length:var(--appkit-text-2xl)] shadow-sm";
12
12
  const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-warning";
13
13
  const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
14
14
  const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
@@ -1,8 +1,7 @@
1
1
  "use client";
2
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useRouter } from "next/navigation";
4
- import { Button, Div, Select, TextLink } from "../../../ui";
5
- const CLS_ACTIVE_TAB = "border-primary text-primary";
4
+ import { TabBarButton, TabBarShell, TabsNavSelect, TextLink } from "../../../ui";
6
5
  export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
7
6
  const router = useRouter();
8
7
  const handleChange = (v) => onTabChange?.(v);
@@ -18,9 +17,22 @@ export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "B
18
17
  router.push(tab.href);
19
18
  handleChange(tab.value);
20
19
  };
21
- return (_jsxs(Div, { layout: "flex", gap: "2", align: "center", role: "tablist", border: "bottom", className: `overflow-x-auto ${className}`, children: [dropdownTabs.length > 0 && (_jsx(Select, { bare: true, "aria-label": dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, onValueChange: handleDropdownChange, className: `max-w-[12rem] flex-shrink-0 whitespace-nowrap ${activeDropdownTab ? CLS_ACTIVE_TAB : ""}` })), tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, paddingX: "md", paddingY: "xs", size: "sm", weight: "medium", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
22
- ? CLS_ACTIVE_TAB
23
- : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)) : (_jsx(Button, { variant: "ghost", type: "button", role: "tab", "aria-selected": activeValue === tab.value, onClick: () => handleChange(tab.value), paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", rounded: "none", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
24
- ? CLS_ACTIVE_TAB
25
- : "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)))] }));
20
+ /*
21
+ * The listing-type picker goes in `leading` — i.e. INSIDE the shell but
22
+ * OUTSIDE the role="tablist" element. It used to be a direct child of the
23
+ * tablist, which is invalid ARIA: a <select> is not a role="tab". Its old
24
+ * `max-w-[12rem] flex-shrink-0` sizing also went onto the inner <select>,
25
+ * the wrong element for sizing utilities (Root Cause #29); those now live
26
+ * on `.appkit-tabs-dropdown` in Tabs.style.css.
27
+ */
28
+ const leading = dropdownTabs.length > 0 ? (_jsx(TabsNavSelect, { ariaLabel: dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, selected: Boolean(activeDropdownTab), onValueChange: handleDropdownChange })) : undefined;
29
+ /*
30
+ * Triggers carry `.appkit-tabs-trigger` and NOTHING else — no padding /
31
+ * size / weight props, no conditional colour classes. `important: true` is
32
+ * set in both tailwind configs, so any surviving utility would beat the
33
+ * component CSS unconditionally. The old inactive string also contained
34
+ * contradictory duplicates (`hover:text-neutral-800` immediately overridden
35
+ * by `hover:text-…text-muted`), so hover had been a no-op.
36
+ */
37
+ return (_jsx(TabBarShell, { className: className, ariaLabel: "Store sections", leading: leading, activeKey: activeValue, children: tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, className: "appkit-tabs-trigger", children: tab.label }, tab.value)) : (_jsx(TabBarButton, { selected: activeValue === tab.value, onClick: () => handleChange(tab.value), children: tab.label }, tab.value))) }));
26
38
  }
@@ -1,5 +1,6 @@
1
1
  import { BaseRepository } from "../../../providers/db-firebase";
2
2
  import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
+ import type { DocumentSnapshot } from "firebase-admin/firestore";
3
4
  import { type SupportTicketDocument, type SupportTicketCreateInput, type SupportTicketUpdateInput, type TicketMessage, type TicketStatus } from "../schemas/firestore";
4
5
  import { type HistoryActor } from "../../../_internal/shared/history/index";
5
6
  /** Who/why for a write that lands in `statusHistory`. */
@@ -11,7 +12,18 @@ export interface TicketWriteContext {
11
12
  }
12
13
  export declare class SupportRepository extends BaseRepository<SupportTicketDocument> {
13
14
  static readonly SIEVE_FIELDS: FirebaseSieveFields;
15
+ /**
16
+ * Ticket PII, encrypted by BaseRepository's write hooks.
17
+ *
18
+ * `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
19
+ * history — the documents themselves were never encrypted. Safe to enable:
20
+ * nothing queries these by equality (the admin list searches `subject`), so
21
+ * no read path depends on their plaintext.
22
+ */
23
+ protected piiFields: readonly ["userEmail", "userDisplayName", "assignedToName"];
14
24
  constructor();
25
+ /** Decrypt on read so callers see the shape they wrote. */
26
+ protected mapDoc<D = SupportTicketDocument>(snap: DocumentSnapshot): D;
15
27
  createTicket(input: SupportTicketCreateInput): Promise<SupportTicketDocument>;
16
28
  getUserTickets(userId: string, page?: number, pageSize?: number): Promise<FirebaseSieveResult<SupportTicketDocument>>;
17
29
  getTicketById(ticketId: string): Promise<SupportTicketDocument | null>;
@@ -3,11 +3,26 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
3
3
  import { normalizeError } from "../../../errors/normalize";
4
4
  import { DatabaseError } from "../../../errors";
5
5
  import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names";
6
+ import { decryptPiiFields } from "../../../security/pii-encrypt";
6
7
  import { SUPPORT_TICKET_COLLECTION, SUPPORT_TICKET_PII_FIELDS, SUPPORT_TICKET_TRACKED_FIELDS, TicketStatusValues, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore";
7
8
  import { withHistory } from "../../../_internal/shared/history/index";
8
9
  export class SupportRepository extends BaseRepository {
9
10
  constructor() {
10
11
  super(SUPPORT_TICKET_COLLECTION);
12
+ /**
13
+ * Ticket PII, encrypted by BaseRepository's write hooks.
14
+ *
15
+ * `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
16
+ * history — the documents themselves were never encrypted. Safe to enable:
17
+ * nothing queries these by equality (the admin list searches `subject`), so
18
+ * no read path depends on their plaintext.
19
+ */
20
+ this.piiFields = SUPPORT_TICKET_PII_FIELDS;
21
+ }
22
+ /** Decrypt on read so callers see the shape they wrote. */
23
+ mapDoc(snap) {
24
+ const raw = super.mapDoc(snap);
25
+ return decryptPiiFields(raw, [...SUPPORT_TICKET_PII_FIELDS]);
11
26
  }
12
27
  async createTicket(input) {
13
28
  try {
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { type TicketCategory } from "./firestore";
3
+ /**
4
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
5
+ * by schema instance and every zod wrapper returns a new one.
6
+ */
7
+ export declare const supportTicketCreateSchema: z.ZodEffects<z.ZodObject<{
8
+ category: z.ZodEnum<[TicketCategory, ...TicketCategory[]]>;
9
+ subject: z.ZodString;
10
+ description: z.ZodString;
11
+ orderId: z.ZodOptional<z.ZodString>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ category: TicketCategory;
14
+ description: string;
15
+ subject: string;
16
+ orderId?: string | undefined;
17
+ }, {
18
+ category: TicketCategory;
19
+ description: string;
20
+ subject: string;
21
+ orderId?: string | undefined;
22
+ }>, {
23
+ category: TicketCategory;
24
+ description: string;
25
+ subject: string;
26
+ orderId?: string | undefined;
27
+ }, {
28
+ category: TicketCategory;
29
+ description: string;
30
+ subject: string;
31
+ orderId?: string | undefined;
32
+ }>;
33
+ export type SupportTicketCreateValues = z.infer<typeof supportTicketCreateSchema>;