@mohasinac/appkit 4.26.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 (277) 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/bundles/data.d.ts +19 -0
  5. package/dist/_internal/server/features/bundles/data.js +32 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  7. package/dist/_internal/server/features/bundles/index.js +1 -1
  8. package/dist/_internal/server/features/checkout/actions.js +6 -4
  9. package/dist/_internal/server/features/classified/metadata.js +3 -1
  10. package/dist/_internal/server/features/digital-code/metadata.js +3 -1
  11. package/dist/_internal/server/features/live/metadata.js +3 -1
  12. package/dist/_internal/server/features/refunds/actions.js +3 -2
  13. package/dist/_internal/server/features/reviews/actions.js +3 -2
  14. package/dist/_internal/server/features/seo/sitemap.js +189 -40
  15. package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
  17. package/dist/_internal/shared/actions/action-registry.js +14 -0
  18. package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
  19. package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
  20. package/dist/_internal/shared/features/checkout/config.js +6 -3
  21. package/dist/client.d.ts +32 -2
  22. package/dist/client.js +37 -2
  23. package/dist/constants/api-endpoints.d.ts +21 -3
  24. package/dist/constants/api-endpoints.js +14 -1
  25. package/dist/constants/field-names.d.ts +2 -2
  26. package/dist/constants/field-names.js +2 -2
  27. package/dist/core/newsletter.repository.js +14 -1
  28. package/dist/features/about/components/FAQPageView.js +1 -1
  29. package/dist/features/about/components/FeesView.js +1 -1
  30. package/dist/features/account/components/AddressForm.d.ts +2 -0
  31. package/dist/features/account/components/AddressForm.js +5 -1
  32. package/dist/features/account/components/UserBidsView.js +1 -1
  33. package/dist/features/account/components/UserSidebar.js +1 -1
  34. package/dist/features/account/components/UserSupportView.js +24 -2
  35. package/dist/features/account/hooks/useAddresses.d.ts +29 -1
  36. package/dist/features/account/hooks/useAddresses.js +4 -1
  37. package/dist/features/account/hooks/useProfile.d.ts +1 -1
  38. package/dist/features/account/hooks/useProfile.js +1 -1
  39. package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
  40. package/dist/features/addresses/repository/addresses.repository.js +19 -0
  41. package/dist/features/admin/actions/chat-actions.js +5 -1
  42. package/dist/features/admin/components/AdminAdsView.js +1 -1
  43. package/dist/features/admin/components/AdminBidsView.js +6 -1
  44. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
  45. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
  46. package/dist/features/admin/components/AdminCarouselView.js +1 -1
  47. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
  48. package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
  49. package/dist/features/admin/components/AdminSidebar.js +1 -1
  50. package/dist/features/admin/components/AdminViewCards.js +2 -2
  51. package/dist/features/admin/components/DataTable.js +2 -2
  52. package/dist/features/admin/schemas/firestore.js +9 -3
  53. package/dist/features/auctions/actions/bid-actions.js +45 -4
  54. package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
  55. package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
  56. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
  57. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  58. package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
  59. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
  60. package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
  61. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  62. package/dist/features/auth/hooks/useAuth.d.ts +23 -1
  63. package/dist/features/auth/hooks/useAuth.js +27 -1
  64. package/dist/features/auth/permissions/constants.d.ts +1 -1
  65. package/dist/features/auth/permissions/constants.js +3 -0
  66. package/dist/features/auth/repository/session.repository.d.ts +16 -1
  67. package/dist/features/auth/repository/session.repository.js +23 -0
  68. package/dist/features/auth/repository/user.repository.js +13 -2
  69. package/dist/features/blog/components/BlogListView.js +2 -2
  70. package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
  71. package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
  72. package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
  73. package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
  74. package/dist/features/cart/components/index.d.ts +1 -1
  75. package/dist/features/cart/components/index.js +1 -1
  76. package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
  77. package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
  78. package/dist/features/cart/utils/line-total.d.ts +25 -0
  79. package/dist/features/cart/utils/line-total.js +26 -0
  80. package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
  81. package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
  82. package/dist/features/categories/components/CategoryTree.js +2 -2
  83. package/dist/features/contact/components/ContactForm.js +1 -1
  84. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
  85. package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
  86. package/dist/features/events/components/AdminEventEditorView.js +16 -9
  87. package/dist/features/events/components/AdminEventEntriesView.js +7 -13
  88. package/dist/features/events/components/EventRafflesSection.js +14 -2
  89. package/dist/features/events/components/EventStatusBadge.js +1 -1
  90. package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
  91. package/dist/features/events/components/event-entry-detail-fields.js +13 -0
  92. package/dist/features/faq/components/ContactCTA.js +1 -1
  93. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  94. package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
  95. package/dist/features/faq/repository/faqs.repository.js +64 -54
  96. package/dist/features/faq/schemas/firestore.d.ts +3 -2
  97. package/dist/features/faq/schemas/firestore.js +2 -2
  98. package/dist/features/faq/schemas/index.d.ts +3 -3
  99. package/dist/features/faq/schemas/index.js +1 -1
  100. package/dist/features/filters/FilterFacetSection.js +2 -2
  101. package/dist/features/forms/Toggle.js +1 -1
  102. package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
  103. package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
  104. package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
  105. package/dist/features/homepage/components/BrandsSection.js +1 -1
  106. package/dist/features/homepage/components/CTABannerSection.js +1 -1
  107. package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
  108. package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
  109. package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
  110. package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
  111. package/dist/features/homepage/components/WelcomeSection.js +1 -1
  112. package/dist/features/homepage/lib/section-renderer.js +2 -1
  113. package/dist/features/layout/AppLayoutShell.js +5 -5
  114. package/dist/features/layout/BottomActions.js +5 -5
  115. package/dist/features/layout/BottomSheet.js +1 -1
  116. package/dist/features/layout/MainNavbar.js +1 -1
  117. package/dist/features/layout/NavbarLayout.js +1 -1
  118. package/dist/features/layout/TitleBarLayout.js +3 -3
  119. package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
  120. package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
  121. package/dist/features/lottery/schemas/config-write.d.ts +141 -0
  122. package/dist/features/lottery/schemas/config-write.js +139 -0
  123. package/dist/features/media/MediaPickerModal.js +3 -1
  124. package/dist/features/media/MediaVideo.js +4 -2
  125. package/dist/features/media/components/MediaSlider.js +1 -1
  126. package/dist/features/orders/components/OrdersList.js +2 -2
  127. package/dist/features/payments/repository/payout.repository.d.ts +11 -1
  128. package/dist/features/payments/repository/payout.repository.js +14 -6
  129. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
  130. package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
  131. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
  132. package/dist/features/products/columns/productTableColumns.js +3 -3
  133. package/dist/features/products/components/CompareOverlay.js +1 -1
  134. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  135. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
  136. package/dist/features/products/components/PrizeDrawsSection.js +5 -1
  137. package/dist/features/products/components/ProductDetailActions.js +11 -7
  138. package/dist/features/products/components/ProductDetailPageView.js +3 -3
  139. package/dist/features/products/components/ProductFeatureBadges.js +2 -2
  140. package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
  141. package/dist/features/products/components/ProductFilters.js +15 -8
  142. package/dist/features/products/components/ProductGrid.js +8 -17
  143. package/dist/features/products/components/ProductTabs.js +7 -4
  144. package/dist/features/products/components/ProductTabsShell.js +2 -7
  145. package/dist/features/products/constants/action-defs.d.ts +21 -4
  146. package/dist/features/products/constants/action-defs.js +7 -3
  147. package/dist/features/products/hooks/useRelatedProducts.js +2 -1
  148. package/dist/features/products/repository/products.repository.d.ts +1 -1
  149. package/dist/features/products/repository/products.repository.js +27 -13
  150. package/dist/features/products/schemas/firestore.d.ts +3 -2
  151. package/dist/features/products/schemas/firestore.js +1 -1
  152. package/dist/features/reviews/actions/review-actions.d.ts +8 -1
  153. package/dist/features/reviews/actions/review-actions.js +14 -3
  154. package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
  155. package/dist/features/reviews/components/ReviewModal.js +1 -1
  156. package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
  157. package/dist/features/reviews/repository/reviews.repository.js +53 -13
  158. package/dist/features/reviews/schemas/firestore.d.ts +6 -0
  159. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  160. package/dist/features/search/components/Search.js +3 -3
  161. package/dist/features/search/components/SearchResultsSection.js +1 -1
  162. package/dist/features/seller/components/SellerAddressesView.js +3 -1
  163. package/dist/features/seller/components/SellerBidsView.js +6 -1
  164. package/dist/features/seller/components/SellerSidebar.js +1 -1
  165. package/dist/features/shell/SectionForm.d.ts +21 -1
  166. package/dist/features/shell/SectionForm.js +15 -3
  167. package/dist/features/shell/build-sections.js +10 -1
  168. package/dist/features/shell/field-ui-meta.d.ts +21 -0
  169. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
  170. package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
  171. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
  172. package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
  173. package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
  174. package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
  175. package/dist/features/stores/actions/store-query-actions.js +25 -1
  176. package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
  177. package/dist/features/stores/components/StoreHeader.js +1 -1
  178. package/dist/features/stores/components/StoreNavTabs.js +20 -8
  179. package/dist/features/support/repository/support.repository.d.ts +12 -0
  180. package/dist/features/support/repository/support.repository.js +15 -0
  181. package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
  182. package/dist/features/support/schemas/ticket-create-form.js +82 -0
  183. package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
  184. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
  185. package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
  186. package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
  187. package/dist/features/wishlist/components/index.d.ts +2 -1
  188. package/dist/features/wishlist/components/index.js +1 -1
  189. package/dist/index.d.ts +10 -3
  190. package/dist/index.js +19 -4
  191. package/dist/monitoring/error-tracking.js +39 -5
  192. package/dist/monitoring/server-logger.js +70 -32
  193. package/dist/next/components/ErrorView.js +1 -1
  194. package/dist/next/routing/route-map.d.ts +39 -7
  195. package/dist/next/routing/route-map.js +34 -4
  196. package/dist/providers/db-firebase/base.repository.d.ts +30 -0
  197. package/dist/providers/db-firebase/base.repository.js +56 -9
  198. package/dist/react/ErrorBoundary.js +9 -1
  199. package/dist/react/contexts/SessionContext.js +6 -2
  200. package/dist/react/hooks/useAuthGate.d.ts +22 -0
  201. package/dist/react/hooks/useAuthGate.js +86 -7
  202. package/dist/schemas/registry.d.ts +3 -3
  203. package/dist/security/index.d.ts +1 -1
  204. package/dist/security/index.js +1 -1
  205. package/dist/security/pii-encrypt.d.ts +13 -5
  206. package/dist/security/pii-encrypt.js +132 -6
  207. package/dist/security/pii-schemas.d.ts +20 -4
  208. package/dist/security/pii-schemas.js +23 -5
  209. package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
  210. package/dist/seed/_helpers/product-search-txt.js +33 -0
  211. package/dist/seed/faq-seed-data.d.ts +16 -0
  212. package/dist/seed/faq-seed-data.js +22 -1
  213. package/dist/seed/products-art-seed-data.js +49 -7
  214. package/dist/seed/products-auctions-seed-data.js +12 -4
  215. package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
  216. package/dist/seed/products-classifieds-seed-data.js +65 -9
  217. package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
  218. package/dist/seed/products-digital-codes-seed-data.js +65 -9
  219. package/dist/seed/products-live-items-seed-data.d.ts +5 -0
  220. package/dist/seed/products-live-items-seed-data.js +33 -5
  221. package/dist/seed/products-preorders-seed-data.d.ts +1 -1
  222. package/dist/seed/products-preorders-seed-data.js +6 -2
  223. package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
  224. package/dist/seed/products-prize-draws-seed-data.js +57 -8
  225. package/dist/seed/products-standard-seed-data.js +6 -2
  226. package/dist/seed/products-stickers-seed-data.js +49 -7
  227. package/dist/seed/site-settings-seed-data.js +5 -3
  228. package/dist/seo/json-ld.d.ts +44 -2
  229. package/dist/seo/json-ld.js +57 -11
  230. package/dist/server.d.ts +1 -1
  231. package/dist/server.js +1 -1
  232. package/dist/styles.css +368 -90
  233. package/dist/tailwind-utilities.css +1 -1
  234. package/dist/tokens/index.d.ts +1 -1
  235. package/dist/tokens/index.js +1 -1
  236. package/dist/tokens/themes/default-dark.js +6 -0
  237. package/dist/tokens/themes/default-light.js +5 -0
  238. package/dist/tokens/tokens.css +84 -0
  239. package/dist/ui/DataTable.js +1 -1
  240. package/dist/ui/DataTable.style.css +3 -3
  241. package/dist/ui/components/BulkActionBar.style.css +1 -3
  242. package/dist/ui/components/Button.js +15 -6
  243. package/dist/ui/components/Dropdown.style.css +1 -4
  244. package/dist/ui/components/FilterChipGroup.js +1 -1
  245. package/dist/ui/components/FilterDrawer.style.css +1 -1
  246. package/dist/ui/components/Icon.d.ts +53 -0
  247. package/dist/ui/components/Icon.js +41 -0
  248. package/dist/ui/components/IconButton.d.ts +13 -0
  249. package/dist/ui/components/IconButton.js +13 -0
  250. package/dist/ui/components/IconButton.style.css +18 -10
  251. package/dist/ui/components/ListingLayout.style.css +2 -10
  252. package/dist/ui/components/ListingToolbar.js +6 -6
  253. package/dist/ui/components/Modal.js +1 -1
  254. package/dist/ui/components/PaginatedSelect.style.css +3 -3
  255. package/dist/ui/components/Pagination.style.css +1 -6
  256. package/dist/ui/components/RichTextEditor.js +1 -1
  257. package/dist/ui/components/RowActionMenu.style.css +1 -1
  258. package/dist/ui/components/SideDrawer.style.css +1 -1
  259. package/dist/ui/components/Slider.js +1 -1
  260. package/dist/ui/components/TabBarShell.d.ts +78 -0
  261. package/dist/ui/components/TabBarShell.js +95 -0
  262. package/dist/ui/components/Tabs.js +12 -8
  263. package/dist/ui/components/Tabs.style.css +249 -41
  264. package/dist/ui/components/ViewToggle.style.css +1 -5
  265. package/dist/ui/components/tabs-overflow.d.ts +17 -0
  266. package/dist/ui/components/tabs-overflow.js +142 -0
  267. package/dist/ui/icons/icon-registry.d.ts +38 -0
  268. package/dist/ui/icons/icon-registry.js +37 -0
  269. package/dist/ui/index.d.ts +6 -0
  270. package/dist/ui/index.js +3 -0
  271. package/dist/utils/index.d.ts +1 -1
  272. package/dist/utils/index.js +1 -1
  273. package/dist/utils/media-url.d.ts +27 -4
  274. package/dist/utils/media-url.js +37 -6
  275. package/dist/utils/search-txt.d.ts +71 -0
  276. package/dist/utils/search-txt.js +118 -0
  277. package/package.json +1 -1
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { normalizeError } from "../../../errors/normalize";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import { useRouter } from "next/navigation";
6
- import { Clock, Gavel, Heart, Play, ShoppingBag, Star } from "lucide-react";
6
+ import { Clock, Gavel, Play, ShoppingBag, Star } from "lucide-react";
7
+ import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
7
8
  import { useLongPress } from "../../../react/hooks/useLongPress";
8
9
  import { MediaImage } from "../../media";
9
10
  import { useWishlistToggle } from "../../wishlist";
@@ -17,19 +18,17 @@ const CLS_STAR_ICON = "h-5 w-5 fill-warning text-warning";
17
18
  const CLS_LIVE_BADGE = "rounded-full bg-error-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-error-on-solid";
18
19
  const CLS_ENDING_BADGE = "rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
19
20
  const CLS_RESERVE_BADGE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
20
- const CLS_HEART_ON = "fill-error text-error";
21
- const CLS_HEART_OFF = "text-zinc-400";
22
21
  function renderAuctionCardHero(props) {
23
22
  const { product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect } = props;
24
23
  return (_jsxs(BaseListingCard.Hero, { aspect: "square", variant: "grid", onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "absolute inset-0 block", children: _jsx(MediaImage, { src: currentSrc, alt: product.title, size: "card", fallback: "Auction image", className: `transition-transform duration-500 ${hovered && images.length > 1 ? "scale-105" : "scale-100"}` }) }), hasVideo && imageIndex === 0 ? (_jsx(Div, { className: "absolute right-2 top-2", children: _jsx(Span, { surface: "overlay-sm", layout: "flex-center", color: "inverse", className: "h-8 w-8", rounded: "full", children: _jsx(Play, { className: "h-4 w-4", "aria-label": mergedLabels.videoLabel }) }) })) : null, images.length > 1 ? (_jsx(Row, { gap: "xs", className: "pointer-events-none absolute bottom-2 left-1/2 -translate-x-1/2", children: images.map((_, index) => (_jsx(Span, { className: `transition-all duration-200 ${index === imageIndex ? "h-1.5 w-3 bg-[white]" : "h-1.5 w-1.5 bg-[white]/60"}`, rounded: "full" }, `${product.id}-image-${index}`))) })) : null, product.featured ? (_jsx(Div, { className: "pointer-events-none absolute left-2 top-2 z-10", children: _jsx(Star, { className: CLS_STAR_ICON }) })) : null, onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: handleSelect, label: isSelected ? mergedLabels.deselectItem : mergedLabels.selectItem, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Stack, { className: `absolute left-2 z-10 ${product.featured ? "top-8" : "top-2"}`, gap: "xs", children: [!isEnded ? _jsx(Span, { size: "xs", weight: "bold", className: CLS_LIVE_BADGE, children: mergedLabels.liveBadge }) : null, isEndingSoon ? _jsx(Span, { size: "xs", weight: "bold", className: CLS_ENDING_BADGE, children: mergedLabels.endingSoon }) : null, isEnded ? _jsx(Span, { color: "inverse", size: "xs", weight: "bold", surface: "overlay-lg", rounded: "full", padding: "pill-xs", children: mergedLabels.ended }) : null, product.status === "sold" ? _jsx(Span, { color: "inverse", size: "xs", weight: "bold", surface: "overlay-xl", rounded: "full", padding: "pill-xs", children: mergedLabels.sold }) : null] }), _jsx(Div, { className: "pointer-events-none absolute bottom-2 right-2 z-10", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_RESERVE_BADGE, children: [_jsx(Gavel, { className: "h-3 w-3" }), mergedLabels.typeBadge] }) })] }));
25
24
  }
26
25
  function renderAuctionCardInfoList(props) {
27
26
  const { product, auctionHref, displayBid, bidCount, isEnded, remaining, inWishlist, wishlistLoading, wishlistActions, countdownClass, mergedLabels, handleWishlist, handleNavigate } = props;
28
- return (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: `flex-1 leading-snug transition-colors hover:text-primary`, color: "primary", truncate: 2, size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(Button, { rounded: "full", type: "button", variant: "ghost", onClick: handleWishlist, disabled: wishlistLoading, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, className: "shrink-0 p-[0.25rem]", children: _jsx(Heart, { className: `h-3.5 w-3.5 ${inWishlist ? CLS_HEART_ON : CLS_HEART_OFF}` }) })) : null] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), _jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", className: `items-[center] py-[0.125rem] text-[11px] ${countdownClass}`, padding: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-2.5 w-2.5" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), bidCount > 0 && _jsx(Caption, { className: "text-[11px]", children: mergedLabels.totalBids(bidCount) })] }), !isEnded && (_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", className: "self-start px-[0.625rem] mt-0.5", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }))] }));
27
+ return (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: `flex-1 leading-snug transition-colors hover:text-primary`, color: "primary", truncate: 2, size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, isLoading: wishlistLoading, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), _jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", className: `items-[center] py-[0.125rem] text-[11px] ${countdownClass}`, padding: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-2.5 w-2.5" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), bidCount > 0 && _jsx(Caption, { className: "text-[11px]", children: mergedLabels.totalBids(bidCount) })] }), !isEnded && (_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", className: "self-start px-[0.625rem] mt-0.5", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }))] }));
29
28
  }
30
29
  function renderAuctionCardInfoGrid(props) {
31
30
  const { product, auctionHref, displayBid, bidCount, hasCurrentBid, isEnded, remaining, resolvedBuyoutPrice, inWishlist, wishlistLoading, wishlistActions, countdownClass, mergedLabels, handleWishlist, handleNavigate } = props;
32
- return (_jsxs(_Fragment, { children: [_jsxs(Row, { align: "start", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "min-w-0 flex-1 leading-snug transition-colors hover:text-primary", color: "primary", size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(Button, { rounded: "full", type: "button", variant: "ghost", onClick: handleWishlist, disabled: wishlistLoading, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, className: "-mt-0.5 p-[0.25rem]", children: _jsx(Heart, { className: `h-4 w-4 ${inWishlist ? CLS_HEART_ON : CLS_HEART_OFF}` }) })) : null] }), _jsxs(Div, { children: [_jsx(Caption, { children: isEnded && hasCurrentBid ? mergedLabels.winningBid : hasCurrentBid ? mergedLabels.currentBid : mergedLabels.startingBid }), _jsx(Text, { className: "leading-none text-primary", size: "base", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), isEnded && product.winnerDisplayName && (_jsxs(Caption, { className: "mt-0.5", color: "muted", children: [mergedLabels.wonBy, ": ", maskDisplayName(product.winnerDisplayName)] }))] }), _jsxs(Row, { wrap: true, justify: "between", gap: "sm", className: "gap-x-2 gap-y-1", children: [_jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", textSize: "xs", className: `items-[center] ${countdownClass}`, paddingY: "y-2xs", paddingX: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-3 w-3" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), _jsx(Caption, { children: mergedLabels.totalBids(bidCount) })] }), _jsx(Row, { wrap: true, gap: "xs", className: "mt-auto", children: isEnded ? (_jsx(Button, { gap: "xs", type: "button", variant: "ghost", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1 cursor-not-allowed opacity-60", disabled: true, children: _jsx(Span, { children: mergedLabels.ended }) })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }), resolvedBuyoutPrice ? (
31
+ return (_jsxs(_Fragment, { children: [_jsxs(Row, { align: "start", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "min-w-0 flex-1 leading-snug transition-colors hover:text-primary", color: "primary", size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, isLoading: wishlistLoading, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] }), _jsxs(Div, { children: [_jsx(Caption, { children: isEnded && hasCurrentBid ? mergedLabels.winningBid : hasCurrentBid ? mergedLabels.currentBid : mergedLabels.startingBid }), _jsx(Text, { className: "leading-none text-primary", size: "base", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), isEnded && product.winnerDisplayName && (_jsxs(Caption, { className: "mt-0.5", color: "muted", children: [mergedLabels.wonBy, ": ", maskDisplayName(product.winnerDisplayName)] }))] }), _jsxs(Row, { wrap: true, justify: "between", gap: "sm", className: "gap-x-2 gap-y-1", children: [_jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", textSize: "xs", className: `items-[center] ${countdownClass}`, paddingY: "y-2xs", paddingX: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-3 w-3" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), _jsx(Caption, { children: mergedLabels.totalBids(bidCount) })] }), _jsx(Row, { wrap: true, gap: "xs", className: "mt-auto", children: isEnded ? (_jsx(Button, { gap: "xs", type: "button", variant: "ghost", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1 cursor-not-allowed opacity-60", disabled: true, children: _jsx(Span, { children: mergedLabels.ended }) })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }), resolvedBuyoutPrice ? (
33
32
  /* `secondary`, not `danger` — this button only navigates to the
34
33
  PDP, and `danger` is reserved for destructive actions (Rule #7
35
34
  kinds). ACTIONS.AUCTION["buy-it-now"] is itself `secondary`. */
@@ -5,6 +5,31 @@ export interface PlaceBidInput {
5
5
  bidAmount: number;
6
6
  autoMaxBid?: number;
7
7
  }
8
+ /**
9
+ * The `ActionResult` envelope every server action in this app returns.
10
+ *
11
+ * 🛑 Exactly ONE level. `onPlaceBid`/`onBuyNow` used to be typed
12
+ * `Promise<unknown>`, and the consumer's actions returned a *doubly*-wrapped
13
+ * `{ ok: true, data: { ok: false, error } }`. Reading the outer `ok` therefore
14
+ * saw `true` for every possible outcome — including every failure — so Buy Now
15
+ * placed a real bid and a real locked cart line and then silently
16
+ * `router.refresh()`ed a page that renders nothing from the cart. Typing these
17
+ * props is what makes a re-introduced second envelope a compile error rather
18
+ * than a silent no-op button.
19
+ */
20
+ export type BidActionEnvelope<T> = {
21
+ ok: true;
22
+ data: T;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ code?: string;
27
+ issues?: unknown[];
28
+ };
29
+ /** The subset of `BuyNowAuctionResult` this component actually consumes. */
30
+ export interface BuyNowSuccess {
31
+ checkoutUrl?: string;
32
+ }
8
33
  export interface PlaceBidFormClientProps {
9
34
  productId: string;
10
35
  currentBid: number;
@@ -17,8 +42,8 @@ export interface PlaceBidFormClientProps {
17
42
  buyNowPrice: number | null;
18
43
  bidCount: number;
19
44
  tags?: string[];
20
- onPlaceBid: (input: PlaceBidInput) => Promise<unknown>;
21
- onBuyNow?: () => Promise<unknown>;
45
+ onPlaceBid: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
46
+ onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
22
47
  }
23
48
  export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
24
49
  /**
@@ -48,6 +48,12 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
48
48
  const [isBuyNowPending, startBuyNowTransition] = useTransition();
49
49
  const [success, setSuccess] = useState(false);
50
50
  const [showLoginModal, setShowLoginModal] = useState(false);
51
+ /**
52
+ * Buy Now has no field to hang an error off — it is a standalone button, not
53
+ * a form input — so it needs its own slot. Without one, every domain failure
54
+ * (`AUCTION_ENDED`, `BUY_NOW_UNAVAILABLE`, rate-limit) was silent.
55
+ */
56
+ const [buyNowError, setBuyNowError] = useState(null);
51
57
  /** Last amount the form itself wrote — see `seedAmount` below. */
52
58
  const lastSeeded = useRef(minBid);
53
59
  /**
@@ -140,43 +146,59 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
140
146
  }
141
147
  }
142
148
  function bidErrorMessage(result) {
143
- if (!result || typeof result !== "object" || !("ok" in result))
144
- return null;
145
- const r = result;
146
- if (r.ok !== false)
149
+ if (result.ok)
147
150
  return null;
148
- return (r.code && BID_ERROR_DISPLAY[r.code]) ?? r.error ?? "Failed to place bid. Please try again.";
151
+ return ((result.code && BID_ERROR_DISPLAY[result.code]) ??
152
+ result.error ??
153
+ "Failed to place bid. Please try again.");
154
+ }
155
+ /**
156
+ * Show a Buy Now failure.
157
+ *
158
+ * Every failure used to be indistinguishable from success and was discarded.
159
+ * An auth failure never THROWS across a server-action boundary — it arrives
160
+ * as a code — so `isAuthError` in the catch below could never fire for it.
161
+ *
162
+ * ONLY `UNAUTHENTICATED` (401) means "sign in". `FORBIDDEN` (403) is "signed
163
+ * in, but not allowed" — bidding on your own auction, or an active
164
+ * `place_bids` soft ban. A login prompt for those tells the buyer to fix
165
+ * something that is not the problem.
166
+ */
167
+ function reportBuyNowFailure(code, message) {
168
+ if (code === "UNAUTHENTICATED") {
169
+ setShowLoginModal(true);
170
+ return;
171
+ }
172
+ setBuyNowError((code && BID_ERROR_DISPLAY[code]) ??
173
+ message ??
174
+ "Buy Now failed. Please try again.");
149
175
  }
150
176
  function handleBuyNow() {
151
177
  if (!onBuyNow)
152
178
  return;
153
179
  setSuccess(false);
180
+ setBuyNowError(null);
154
181
  startBuyNowTransition(async () => {
155
182
  try {
156
183
  const result = await onBuyNow();
157
- if (result &&
158
- typeof result === "object" &&
159
- "ok" in result &&
160
- result.ok === false) {
161
- setSuccess(false);
184
+ if (!result.ok) {
185
+ reportBuyNowFailure(result.code, result.error);
162
186
  return;
163
187
  }
164
188
  setSuccess(true);
165
189
  // Buy-Now writes a locked cart line, so send the buyer straight to the
166
- // auction checkout lane to pay for it. This used to just call
167
- // `router.refresh()` and discard the action's result entirely, leaving
168
- // the buyer on the product page with no indication that anything was
169
- // owed — the action returned an orderId nobody ever used.
170
- const checkoutUrl = result?.checkoutUrl;
171
- if (checkoutUrl)
172
- router.push(checkoutUrl);
190
+ // auction checkout lane to pay for it. `buyNowAuction` deliberately
191
+ // never touches the product document, so falling through to
192
+ // `router.refresh()` re-renders a byte-identical page — which is
193
+ // exactly what "the button does nothing" looked like.
194
+ if (result.data.checkoutUrl)
195
+ router.push(result.data.checkoutUrl);
173
196
  else
174
197
  router.refresh();
175
198
  }
176
199
  catch (err) {
177
200
  void normalizeError(err);
178
- if (isAuthError(err))
179
- setShowLoginModal(true);
201
+ reportBuyNowFailure(isAuthError(err) ? "UNAUTHENTICATED" : undefined, err instanceof Error ? err.message : undefined);
180
202
  }
181
203
  });
182
204
  }
@@ -212,7 +234,7 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
212
234
  return applyZodIssues(parsed.error.issues, setFieldError);
213
235
  setSuccess(false);
214
236
  startTransition(() => submitBid(parsed.data.bidAmount, setFieldError));
215
- }, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", disabled: isBuyNowPending || isPending, isLoading: isBuyNowPending, onClick: handleBuyNow, children: `Buy Now — ${formatCurrency(buyNowPrice, currency)}` }), _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Skips the bidding. The auction stays live until you pay \u2014 complete checkout within ", AUCTION_BUYOUT_WINDOW_MINUTES, " minutes."] })] }))] })) }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", size: "xs", children: tag }, tag))) }) }))] }));
237
+ }, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", disabled: isBuyNowPending || isPending, isLoading: isBuyNowPending, onClick: handleBuyNow, children: `Buy Now — ${formatCurrency(buyNowPrice, currency)}` }), buyNowError && (_jsx(Text, { align: "center", size: "xs", variant: "error", role: "alert", children: buyNowError })), _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Skips the bidding. The auction stays live until you pay \u2014 complete checkout within ", AUCTION_BUYOUT_WINDOW_MINUTES, " minutes."] })] }))] })) }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", size: "xs", children: tag }, tag))) }) }))] }));
216
238
  }
217
239
  /**
218
240
  * `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Button, Div, Row, Span } from "../../../ui";
3
3
  export function SocialAuthButtons({ onGoogle, disabled, dividerLabel, googleLabel, }) {
4
- return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-neutral-50 hover:bg-[var(--appkit-color-surface-elevated)] disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
4
+ return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
5
5
  }
@@ -74,10 +74,32 @@ export declare function useVerifyEmail(options?: {
74
74
  onSuccess?: (data: JsonValue) => void;
75
75
  onError?: (error: Error) => void;
76
76
  }): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, VerifyEmailData, unknown>;
77
+ /**
78
+ * Re-send the verification email — Firebase-native, like the send that
79
+ * happens at registration.
80
+ *
81
+ * ## What this used to do, and why it could not work
82
+ *
83
+ * It POSTed `/api/auth/resend-verification`, a route **deleted** in the
84
+ * Firebase-native auth migration. Root Cause #54 is the reason it can never
85
+ * come back: the Admin SDK cannot send email at all — it only *generates*
86
+ * action links — so the only way to deliver a verification mail without
87
+ * routing it through Resend is the CLIENT SDK's own
88
+ * `sendEmailVerification()`, which uses Firebase's hosted template.
89
+ *
90
+ * So every call 404'd, and `useRegister` above says in as many words that
91
+ * "the user can re-request verification later" — a documented recovery path
92
+ * that did not exist. Found by `audit-client-verb-match`'s NO_ROUTE rule.
93
+ *
94
+ * `ResendVerificationData.email` is accepted and unused: the client SDK
95
+ * re-sends for the CURRENTLY signed-in user and takes no address, so honouring
96
+ * an arbitrary email here would be a lie about what happens. Kept in the
97
+ * signature so the two existing call shapes stay source-compatible.
98
+ */
77
99
  export declare function useResendVerification(options?: {
78
100
  onSuccess?: (data: JsonValue) => void;
79
101
  onError?: (error: Error) => void;
80
- }): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, ResendVerificationData, unknown>;
102
+ }): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, void | ResendVerificationData, unknown>;
81
103
  export declare function useForgotPassword(options?: {
82
104
  onSuccess?: (data: JsonValue) => void;
83
105
  onError?: (error: Error) => void;
@@ -337,9 +337,35 @@ export function useVerifyEmail(options) {
337
337
  onError: options?.onError,
338
338
  });
339
339
  }
340
+ /**
341
+ * Re-send the verification email — Firebase-native, like the send that
342
+ * happens at registration.
343
+ *
344
+ * ## What this used to do, and why it could not work
345
+ *
346
+ * It POSTed `/api/auth/resend-verification`, a route **deleted** in the
347
+ * Firebase-native auth migration. Root Cause #54 is the reason it can never
348
+ * come back: the Admin SDK cannot send email at all — it only *generates*
349
+ * action links — so the only way to deliver a verification mail without
350
+ * routing it through Resend is the CLIENT SDK's own
351
+ * `sendEmailVerification()`, which uses Firebase's hosted template.
352
+ *
353
+ * So every call 404'd, and `useRegister` above says in as many words that
354
+ * "the user can re-request verification later" — a documented recovery path
355
+ * that did not exist. Found by `audit-client-verb-match`'s NO_ROUTE rule.
356
+ *
357
+ * `ResendVerificationData.email` is accepted and unused: the client SDK
358
+ * re-sends for the CURRENTLY signed-in user and takes no address, so honouring
359
+ * an arbitrary email here would be a lie about what happens. Kept in the
360
+ * signature so the two existing call shapes stay source-compatible.
361
+ */
340
362
  export function useResendVerification(options) {
341
363
  return useMutation({
342
- mutationFn: (data) => apiClient.post(AUTH_ENDPOINTS.RESEND_VERIFICATION, data),
364
+ mutationFn: async () => {
365
+ const authProvider = getClientAuthProvider();
366
+ await authProvider.sendEmailVerification();
367
+ return { success: true };
368
+ },
343
369
  onSuccess: options?.onSuccess,
344
370
  onError: options?.onError,
345
371
  });
@@ -11,7 +11,7 @@
11
11
  * Permission naming: admin:resource:action
12
12
  * Ban action naming: plain verb phrase — write_reviews, place_bids, etc.
13
13
  */
14
- export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
14
+ export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:grouped-listings:read" | "admin:grouped-listings:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
15
15
  /**
16
16
  * Granular user actions that can be soft-banned individually.
17
17
  * A user may have multiple simultaneous soft bans of different types.
@@ -126,6 +126,8 @@ export const PERMISSION_GROUPS = {
126
126
  ],
127
127
  catalog_manager: [
128
128
  "admin:dashboard:view",
129
+ "admin:grouped-listings:read",
130
+ "admin:grouped-listings:write",
129
131
  "admin:categories:read",
130
132
  "admin:categories:write",
131
133
  "admin:categories:delete",
@@ -277,6 +279,7 @@ export const ROUTE_PERMISSION_MAP = {
277
279
  "/admin/audit-log": "admin:audit-log:read",
278
280
  "/admin/shipments": "admin:shipments:read",
279
281
  "/admin/catalogue-approvals": "admin:catalogue:read",
282
+ "/admin/grouped-listings": "admin:grouped-listings:read",
280
283
  "/admin/categories": "admin:categories:read",
281
284
  "/admin/brands": "admin:brands:read",
282
285
  "/admin/coupons": "admin:coupons:read",
@@ -1,9 +1,24 @@
1
- import type { DocumentReference } from "firebase-admin/firestore";
1
+ import type { DocumentReference, DocumentSnapshot } from "firebase-admin/firestore";
2
2
  import { BaseRepository, type FirebaseSieveFields, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
3
3
  import { type SessionCreateInput, type SessionDocument, type SessionUpdateInput } from "../schemas";
4
4
  export declare class SessionRepository extends BaseRepository<SessionDocument> {
5
5
  static readonly SIEVE_FIELDS: FirebaseSieveFields;
6
+ /**
7
+ * Session PII, encrypted by BaseRepository's write hooks.
8
+ *
9
+ * This repository had no encryption at all — `createSession` calls
10
+ * `createWithId`, and before the hooks existed that method applied nothing —
11
+ * so device IPs, user agents and city were stored in cleartext.
12
+ *
13
+ * `deviceInfo.browser` / `deviceInfo.os` are deliberately NOT here: they are
14
+ * low-entropy strings the admin sessions view filters on (see SIEVE_FIELDS
15
+ * above), and encrypting them would break those queries while revealing
16
+ * nothing an IP does not.
17
+ */
18
+ protected piiFields: readonly ["deviceInfo.ip", "deviceInfo.userAgent", "location.city"];
6
19
  constructor();
20
+ /** Decrypt on read so callers see the same shape they wrote. */
21
+ protected mapDoc<D = SessionDocument>(snap: DocumentSnapshot): D;
7
22
  list(model: SieveModel): Promise<FirebaseSieveResult<SessionDocument>>;
8
23
  listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<SessionDocument>>;
9
24
  createSession(userId: string, data: Omit<SessionCreateInput, "userId" | "expiresAt" | "isActive">): Promise<SessionDocument>;
@@ -3,10 +3,33 @@ import { DatabaseError } from "../../../errors";
3
3
  import { serverLogger } from "../../../monitoring";
4
4
  import { BaseRepository, parseSieveDateValue, } from "../../../providers/db-firebase";
5
5
  import { generateSessionId } from "../token-helpers";
6
+ import { decryptPiiFields } from "../../../security/pii-encrypt";
6
7
  import { SESSION_COLLECTION, SESSION_EXPIRATION_MS, SESSION_FIELDS, } from "../schemas";
7
8
  export class SessionRepository extends BaseRepository {
8
9
  constructor() {
9
10
  super(SESSION_COLLECTION);
11
+ /**
12
+ * Session PII, encrypted by BaseRepository's write hooks.
13
+ *
14
+ * This repository had no encryption at all — `createSession` calls
15
+ * `createWithId`, and before the hooks existed that method applied nothing —
16
+ * so device IPs, user agents and city were stored in cleartext.
17
+ *
18
+ * `deviceInfo.browser` / `deviceInfo.os` are deliberately NOT here: they are
19
+ * low-entropy strings the admin sessions view filters on (see SIEVE_FIELDS
20
+ * above), and encrypting them would break those queries while revealing
21
+ * nothing an IP does not.
22
+ */
23
+ this.piiFields = [
24
+ "deviceInfo.ip",
25
+ "deviceInfo.userAgent",
26
+ "location.city",
27
+ ];
28
+ }
29
+ /** Decrypt on read so callers see the same shape they wrote. */
30
+ mapDoc(snap) {
31
+ const raw = super.mapDoc(snap);
32
+ return decryptPiiFields(raw, [...this.piiFields]);
10
33
  }
11
34
  async list(model) {
12
35
  return this.sieveQuery(model, SessionRepository.SIEVE_FIELDS);
@@ -1,7 +1,7 @@
1
1
  import { normalizeError } from "../../../errors/normalize";
2
2
  import { DatabaseError } from "../../../errors";
3
3
  import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
4
- import { USER_PII_FIELDS, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
4
+ import { USER_PII_FIELDS, USER_PII_INDEX_MAP, piiIndicesFor, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
5
5
  import { USER_COLLECTION, USER_FIELDS, createUserId, } from "../schemas";
6
6
  export class UserRepository extends BaseRepository {
7
7
  constructor() {
@@ -23,7 +23,18 @@ export class UserRepository extends BaseRepository {
23
23
  // addPiiIndices is intentionally NOT called here — it spreads the original
24
24
  // plaintext `data` back into the result, which would overwrite the encrypted
25
25
  // ciphertext with the original plaintext values, defeating the encryption.
26
- const encrypted = encryptPiiFields(data, [...USER_PII_FIELDS]);
26
+ //
27
+ // The mapped indices below are not optional. `encryptPiiFields` derives its
28
+ // sibling name mechanically as `${field}Index`, so `phoneNumber` produces
29
+ // `phoneNumberIndex` — but `findByPhone` queries `USER_FIELDS.PHONE_INDEX`,
30
+ // which is `phoneIndex`. Only the seed paths ever wrote that name, so
31
+ // **phone lookup could never match a user created by the app.**
32
+ // `piiIndicesFor` applies USER_PII_INDEX_MAP's real names and, unlike
33
+ // `addPiiIndices`, carries no plaintext back over the ciphertext.
34
+ const encrypted = {
35
+ ...encryptPiiFields(data, [...USER_PII_FIELDS]),
36
+ ...piiIndicesFor(data, USER_PII_INDEX_MAP),
37
+ };
27
38
  const access = encrypted;
28
39
  if (access.payoutDetails) {
29
40
  access.payoutDetails = encryptPayoutDetails(access.payoutDetails);
@@ -25,7 +25,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
25
25
  : "";
26
26
  const isInteractive = !!(href || onClick);
27
27
  const card = (_jsxs(Article, { role: onClick && !href ? "button" : undefined, tabIndex: onClick && !href ? 0 : undefined, onKeyDown: onClick && !href
28
- ? (e) => (e.key === "Enter" || e.key === " ") && onClick(post)
28
+ ? (e) => (e.key === "Enter" || e.key === "") && onClick(post)
29
29
  : undefined, onClick: onClick && !href ? () => onClick(post) : undefined, className: `group flex flex-col h-full overflow-hidden rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md ${isInteractive ? "cursor-pointer" : ""} ${className}`, children: [_jsx(Div, { className: `relative aspect-video w-full ${__O.hidden} bg-[var(--appkit-color-surface)] dark:bg-[var(--appkit-color-surface-elevated)] flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Row, { surface: "muted", className: "h-full w-full", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-30", size: "4xl", "aria-hidden": "true", children: "\u270D\uFE0F" }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p5}`, children: [_jsxs(Row, { className: "mb-2", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: "bg-primary/10 text-primary", rounded: "full", padding: "pill-xs", transform: "capitalize", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: "Featured" })), post.readTimeMinutes && (_jsxs(Span, { size: "xs", color: "muted", children: [post.readTimeMinutes, " min read"] }))] }), _jsx(Heading, { level: 3, className: `group-hover:text-primary`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: post.title }), post.excerpt && (_jsx(Text, { className: `mt-2 flex-1`, color: "muted", truncate: 3, size: "sm", children: post.excerpt })), _jsxs(Row, { className: "mt-auto", padding: "t-sm", gap: "3", children: [post.authorAvatar ? (_jsx(Div, { className: "relative h-7 w-7 flex-shrink-0", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: post.authorAvatar, alt: post.authorName ?? "author", size: "avatar" }) })) : post.authorName ? (_jsx(Row, { textWeight: "bold", textSize: "xs", className: "h-7 w-7 flex-shrink-0 bg-primary/10 text-primary", align: "center", justify: "center", rounded: "full", children: post.authorName.charAt(0).toUpperCase() })) : null, _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] min-w-0", size: "xs", children: [post.authorName && (_jsx(Span, { weight: "medium", className: "text-neutral-700", children: safeDisplayName(post.authorName, "Author") })), date && _jsxs(Span, { className: "ml-1 text-neutral-400", children: ["\u00B7 ", date] })] })] })] })] }));
30
30
  if (href) {
31
31
  return (_jsx(Link, { href: href, className: "block h-full", children: card }));
@@ -33,7 +33,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
33
33
  return card;
34
34
  }
35
35
  export function BlogCategoryTabs({ categories, active, onSelect, labels = {}, }) {
36
- return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), variant: active === cat ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-neutral-200 dark:hover:bg-slate-700"}`, children: labels[cat] ?? cat }, cat)))] }));
36
+ return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), variant: active === cat ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels[cat] ?? cat }, cat)))] }));
37
37
  }
38
38
  export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onPostClick, emptyLabel = "No posts found", slots, }) {
39
39
  if (isLoading) {
@@ -55,7 +55,17 @@ export interface CartPriceBreakdownProps {
55
55
  unavailableNote?: React.ReactNode;
56
56
  /** Subtotal to show when `preview` is null (guest carts). */
57
57
  fallbackSubtotal?: number;
58
+ /**
59
+ * Shown when the preview request FAILED, as opposed to never having run.
60
+ *
61
+ * Separate from `unavailableNote` because the two say different things and
62
+ * the failure case can coexist with a rendered breakdown: on a refetch error
63
+ * the last good `preview` is deliberately kept, so the buyer sees real
64
+ * figures plus a note that they may be out of date — strictly better than
65
+ * blanking to ₹0, which is what a swallowed error used to produce.
66
+ */
67
+ errorNote?: React.ReactNode;
58
68
  isLoading?: boolean;
59
69
  className?: string;
60
70
  }
61
- export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
71
+ export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
@@ -9,10 +9,10 @@ function Line({ label, amount, tone = "muted", strong = false, }) {
9
9
  function withStoreCount(label, count) {
10
10
  return count > 1 ? `${label} (${count} stores)` : label;
11
11
  }
12
- export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, isLoading = false, className = "", }) {
12
+ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", }) {
13
13
  const money = (value) => formatCurrency(value, currency);
14
14
  if (!preview) {
15
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), unavailableNote && (_jsx(Text, { size: "xs", color: "muted", children: unavailableNote }))] }));
15
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), (errorNote ?? unavailableNote) && (_jsx(Text, { size: "xs", color: errorNote ? "error" : "muted", children: errorNote ?? unavailableNote }))] }));
16
16
  }
17
17
  const storeCount = preview.stores.length;
18
18
  // Count how many stores are actually charged each per-store add-on, so the
@@ -21,5 +21,5 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
21
21
  const subtotalLabel = itemCount !== undefined
22
22
  ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
23
23
  : "Subtotal";
24
- return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" }))] }));
24
+ return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), errorNote ? (_jsx(Text, { size: "xs", color: "error", children: errorNote })) : (isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" })))] }));
25
25
  }
@@ -24,6 +24,15 @@ export interface StoreAddonsRates {
24
24
  shipmentProtectionFeePercent?: number;
25
25
  shipmentProtectionFeeMin?: number;
26
26
  }
27
+ /**
28
+ * True when the site offers at least one paid add-on.
29
+ *
30
+ * Exported because the picker renders NOTHING when the answer is false, and a
31
+ * parent that doesn't know that ends up drawing a bare "Add-ons" heading over
32
+ * empty space. Callers use this to decide between mounting the picker and
33
+ * showing an explanatory line instead.
34
+ */
35
+ export declare function hasAnyStoreAddon(rates: StoreAddonsRates | null | undefined): boolean;
27
36
  export interface StoreAddonsPickerProps {
28
37
  storeId: string;
29
38
  /** This store's subtotal — shipment protection is a percentage of it. */
@@ -6,12 +6,22 @@ import { IconBox } from "../../../ui/components/IconBox";
6
6
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
7
7
  import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
8
8
  import { formatCurrency } from "../../../utils/number.formatter";
9
- export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
10
- const anyEnabled = !!rates &&
11
- (rates.whatsappNotifyFeeEnabled ||
9
+ /**
10
+ * True when the site offers at least one paid add-on.
11
+ *
12
+ * Exported because the picker renders NOTHING when the answer is false, and a
13
+ * parent that doesn't know that ends up drawing a bare "Add-ons" heading over
14
+ * empty space. Callers use this to decide between mounting the picker and
15
+ * showing an explanatory line instead.
16
+ */
17
+ export function hasAnyStoreAddon(rates) {
18
+ return (!!rates &&
19
+ !!(rates.whatsappNotifyFeeEnabled ||
12
20
  rates.giftWrapFeeEnabled ||
13
- rates.shipmentProtectionFeeEnabled);
14
- if (!anyEnabled)
21
+ rates.shipmentProtectionFeeEnabled));
22
+ }
23
+ export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
24
+ if (!hasAnyStoreAddon(rates) || !rates)
15
25
  return null;
16
26
  const money = (value) => formatCurrency(value, currency);
17
27
  const set = (patch) => onChange(storeId, { ...value, ...patch });
@@ -5,7 +5,7 @@ export { CartView } from "./CartView";
5
5
  export type { CartViewProps, CartOrderGroup } from "./CartView";
6
6
  export { CartSummary } from "./CartSummary";
7
7
  export type { CartSummaryProps } from "./CartSummary";
8
- export { StoreAddonsPicker } from "./StoreAddonsPicker";
8
+ export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
9
9
  export type { StoreAddonsPickerProps, StoreAddonsValue, StoreAddonsRates, } from "./StoreAddonsPicker";
10
10
  export { CartPriceBreakdown } from "./CartPriceBreakdown";
11
11
  export type { CartPriceBreakdownProps, CartPriceBreakdownData, CartPriceBreakdownStore, } from "./CartPriceBreakdown";
@@ -2,7 +2,7 @@ export { CartItemRow, CartDrawer } from "./CartDrawer";
2
2
  export { CartGroupLineRow } from "./CartGroupLineRow";
3
3
  export { CartView } from "./CartView";
4
4
  export { CartSummary } from "./CartSummary";
5
- export { StoreAddonsPicker } from "./StoreAddonsPicker";
5
+ export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
6
6
  export { CartPriceBreakdown } from "./CartPriceBreakdown";
7
7
  export { CheckoutView } from "./CheckoutView";
8
8
  export { CheckoutAddressStep } from "./CheckoutAddressStep";
@@ -1,11 +1,18 @@
1
1
  import { type GuestCartItem } from "../utils/guest-cart";
2
+ import { type NormalizedError } from "../../../errors/normalize";
2
3
  export interface UseGuestCartMergeOptions {
3
4
  userId: string | null | undefined;
4
5
  mergeEndpoint?: string;
5
6
  onNavigate?: (url: string) => void;
6
7
  mergeFn?: (items: GuestCartItem[]) => Promise<void>;
7
8
  onMerged?: (count: number) => void;
8
- onMergeFailed?: (error: unknown) => void;
9
+ /**
10
+ * Receives a typed NormalizedError, not the raw `unknown` — the hook is the
11
+ * boundary that knows how to classify the failure, so every consumer would
12
+ * otherwise repeat the same narrowing. Zero external consumers at the time of
13
+ * narrowing, so this broke nothing.
14
+ */
15
+ onMergeFailed?: (error: NormalizedError) => void;
9
16
  }
10
17
  /**
11
18
  * useGuestCartMerge
@@ -3,6 +3,7 @@ import { useEffect, useRef } from "react";
3
3
  import { apiClient } from "../../../http";
4
4
  import { clearGuestCart, clearGuestReturnTo, getGuestCartItems, getGuestReturnTo, } from "../utils/guest-cart";
5
5
  import { CART_ENDPOINTS } from "../../../constants/api-endpoints";
6
+ import { normalizeError } from "../../../errors/normalize";
6
7
  /**
7
8
  * useGuestCartMerge
8
9
  *
@@ -42,7 +43,7 @@ export function useGuestCartMerge(options) {
42
43
  }
43
44
  })
44
45
  .catch((err) => {
45
- options.onMergeFailed?.(err);
46
+ options.onMergeFailed?.(normalizeError(err));
46
47
  if (returnTo)
47
48
  clearGuestReturnTo();
48
49
  });