@mohasinac/appkit 4.30.15 → 4.30.17

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 (120) hide show
  1. package/dist/_internal/server/functions/https.js +17 -2
  2. package/dist/_internal/server/jobs/core/listingProcessor.js +5 -1
  3. package/dist/_internal/shared/types/action-error-reporter.d.ts +40 -0
  4. package/dist/_internal/shared/types/action-error-reporter.js +27 -0
  5. package/dist/_internal/shared/types/action-result.d.ts +10 -0
  6. package/dist/_internal/shared/types/action-result.js +26 -0
  7. package/dist/client/api/surface-error.d.ts +6 -4
  8. package/dist/client/api/surface-error.js +19 -8
  9. package/dist/contracts/realtime-session.d.ts +27 -0
  10. package/dist/contracts/realtime-session.js +118 -0
  11. package/dist/core/unit-of-work.js +21 -13
  12. package/dist/errors/base-error.js +14 -1
  13. package/dist/errors/database-error.js +11 -1
  14. package/dist/errors/describe-cause.d.ts +5 -0
  15. package/dist/errors/describe-cause.js +76 -0
  16. package/dist/errors/error-display-map.d.ts +32 -1
  17. package/dist/errors/error-display-map.js +46 -0
  18. package/dist/errors/error-mapping.d.ts +47 -7
  19. package/dist/errors/error-mapping.js +46 -5
  20. package/dist/errors/index.d.ts +1 -0
  21. package/dist/errors/index.js +4 -0
  22. package/dist/features/about/components/PublicProfileView.js +4 -4
  23. package/dist/features/account/components/AddressBook.js +2 -2
  24. package/dist/features/account/components/AddressesIndexListing.js +2 -2
  25. package/dist/features/addresses/repository/addresses.repository.d.ts +12 -1
  26. package/dist/features/addresses/repository/addresses.repository.js +16 -7
  27. package/dist/features/admin/components/AdminAnalyticsView.js +2 -2
  28. package/dist/features/admin/components/AdminCouponsView.js +2 -2
  29. package/dist/features/admin/components/AdminGuideHubView.js +2 -2
  30. package/dist/features/admin/components/AdminMediaView.js +2 -2
  31. package/dist/features/admin/components/AdminViewCards.js +2 -2
  32. package/dist/features/auctions/components/AuctionDetailPageView.js +1 -1
  33. package/dist/features/auctions/components/MarketplaceAuctionGrid.d.ts +1 -2
  34. package/dist/features/auctions/components/MarketplaceAuctionGrid.js +8 -7
  35. package/dist/features/auctions/components/PlaceBidFormClient.js +32 -15
  36. package/dist/features/auth/repository/user.repository.d.ts +22 -1
  37. package/dist/features/auth/repository/user.repository.js +31 -28
  38. package/dist/features/before-after/components/BeforeAfterGallery.js +2 -2
  39. package/dist/features/blog/components/BlogIndexListing.js +3 -3
  40. package/dist/features/blog/components/BlogListView.js +3 -3
  41. package/dist/features/blog/components/BlogPostView.js +2 -2
  42. package/dist/features/cart/repository/cart.repository.d.ts +15 -2
  43. package/dist/features/cart/repository/cart.repository.js +15 -13
  44. package/dist/features/categories/components/BundleCollage.js +2 -2
  45. package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
  46. package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
  47. package/dist/features/categories/components/CategoryGrid.js +2 -2
  48. package/dist/features/categories/components/CategoryProductsListing.js +2 -2
  49. package/dist/features/categories/components/CategoryStoresListing.js +2 -2
  50. package/dist/features/collections/components/CollectionCard.js +2 -2
  51. package/dist/features/events/components/EventRafflesSection.js +2 -2
  52. package/dist/features/events/components/EventsIndexListing.js +2 -2
  53. package/dist/features/events/components/EventsListView.js +3 -3
  54. package/dist/features/homepage/components/FeaturedBundlesSection.js +2 -2
  55. package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
  56. package/dist/features/homepage/components/HomepageSkeleton.js +1 -1
  57. package/dist/features/homepage/components/SecurityHighlightsSection.js +1 -1
  58. package/dist/features/homepage/components/SocialFeedSection.js +2 -2
  59. package/dist/features/homepage/components/StatsCounterSection.js +1 -1
  60. package/dist/features/homepage/components/TrustBadges.js +2 -2
  61. package/dist/features/homepage/components/TrustFeaturesSection.js +1 -1
  62. package/dist/features/messages/hooks/useConversation.js +39 -15
  63. package/dist/features/messages/hooks/useConversations.js +36 -14
  64. package/dist/features/messages/repository/conversations.repository.d.ts +23 -0
  65. package/dist/features/messages/repository/conversations.repository.js +53 -0
  66. package/dist/features/orders/repository/orders.repository.d.ts +12 -1
  67. package/dist/features/orders/repository/orders.repository.js +14 -5
  68. package/dist/features/payments/repository/payout.repository.d.ts +19 -9
  69. package/dist/features/payments/repository/payout.repository.js +26 -20
  70. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -3
  71. package/dist/features/products/components/AuctionsIndexListing.js +2 -3
  72. package/dist/features/products/components/PrizeDrawCollage.js +2 -2
  73. package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -3
  74. package/dist/features/products/components/PrizeDrawsSection.js +2 -2
  75. package/dist/features/products/components/ProductFilters.d.ts +19 -1
  76. package/dist/features/products/components/ProductFilters.js +11 -3
  77. package/dist/features/products/components/ProductGrid.js +2 -7
  78. package/dist/features/products/components/ProductsIndexListing.d.ts +4 -2
  79. package/dist/features/products/components/ProductsIndexListing.js +41 -28
  80. package/dist/features/products/components/RelatedProducts.js +2 -2
  81. package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
  82. package/dist/features/reviews/components/ReviewDetailPageView.js +2 -2
  83. package/dist/features/reviews/components/ReviewsListingPanel.js +2 -2
  84. package/dist/features/reviews/repository/reviews.repository.d.ts +10 -8
  85. package/dist/features/reviews/repository/reviews.repository.js +16 -19
  86. package/dist/features/seller/components/SellerCouponsView.js +2 -2
  87. package/dist/features/seller/components/SellerDashboardView.js +3 -3
  88. package/dist/features/server-errors/schemas/firestore.d.ts +9 -0
  89. package/dist/features/stores/components/StoreAuctionsListing.js +2 -3
  90. package/dist/features/stores/components/StoreClassifiedsListing.js +2 -3
  91. package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -3
  92. package/dist/features/stores/components/StoreGuideHubView.js +2 -2
  93. package/dist/features/stores/components/StoreLiveItemsListing.js +2 -3
  94. package/dist/features/stores/components/StorePreOrdersListing.js +2 -3
  95. package/dist/features/stores/components/StoreProductsListing.js +2 -2
  96. package/dist/features/stores/components/StoresIndexListing.js +3 -3
  97. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +215 -18
  98. package/dist/next/api/routeHandler.js +27 -7
  99. package/dist/providers/db-firebase/base.repository.d.ts +12 -0
  100. package/dist/providers/db-firebase/base.repository.js +48 -20
  101. package/dist/react/hooks/usePendingFilters.d.ts +12 -2
  102. package/dist/react/hooks/usePendingFilters.js +2 -2
  103. package/dist/react/hooks/usePendingTable.d.ts +9 -2
  104. package/dist/react/hooks/useRealtimeEvent.js +22 -4
  105. package/dist/security/pii-schemas.d.ts +19 -1
  106. package/dist/security/pii-schemas.js +19 -1
  107. package/dist/server.d.ts +2 -0
  108. package/dist/server.js +5 -0
  109. package/dist/styles.css +84 -20
  110. package/dist/tailwind-utilities.css +1 -1
  111. package/dist/tokens/tokens.css +7 -2
  112. package/dist/ui/components/Layout.d.ts +31 -18
  113. package/dist/ui/components/Layout.js +30 -19
  114. package/dist/ui/components/Layout.style.css +76 -17
  115. package/dist/ui/components/ListingLayout.d.ts +2 -0
  116. package/dist/ui/components/ListingLayout.js +16 -2
  117. package/dist/ui/components/StickyToolbar.js +3 -48
  118. package/dist/ui/components/toolbar-collapse.d.ts +44 -0
  119. package/dist/ui/components/toolbar-collapse.js +93 -0
  120. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Div, Pagination, Row, Stack, Text } from "../../../ui";
3
+ import { Div, Grid, Pagination, Row, Stack, Text } from "../../../ui";
4
4
  import { EventCard } from "./EventCard";
5
5
  const __P = {
6
6
  p4: "p-[var(--appkit-space-4)]",
7
7
  };
8
8
  export function EventsListView({ events, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onParticipate, emptyLabel = "No events found", slots, }) {
9
9
  if (isLoading) {
10
- return (_jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden", surface: "subtle", rounded: "xl", border: "default", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-5 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" })] })] }, i))) }));
10
+ return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden", surface: "subtle", rounded: "xl", border: "default", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-5 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" })] })] }, i))) }));
11
11
  }
12
12
  if (events.length === 0) {
13
13
  if (slots?.renderEmptyState) {
@@ -17,7 +17,7 @@ export function EventsListView({ events, isLoading, totalPages = 1, currentPage
17
17
  }
18
18
  return (_jsxs(Stack, { gap: "xl", children: [slots?.renderHeader
19
19
  ? slots.renderHeader({ total })
20
- : null, _jsx(Div, { layout: "grid", gap: "6", className: "sm:grid-cols-2 lg:grid-cols-3", children: events.map((event, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(event, i) }, event.id)) : (_jsx(EventCard, { event: event, onParticipate: onParticipate ? (e) => onParticipate(e) : undefined }, event.id))) }), slots?.renderFooter ? slots.renderFooter({
20
+ : null, _jsx(Grid, { cols: "cards", gap: "lg", children: events.map((event, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(event, i) }, event.id)) : (_jsx(EventCard, { event: event, onParticipate: onParticipate ? (e) => onParticipate(e) : undefined }, event.id))) }), slots?.renderFooter ? slots.renderFooter({
21
21
  page: currentPage,
22
22
  totalPages,
23
23
  }) : totalPages > 1 && onPageChange ? (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) })) : null] }));
@@ -9,7 +9,7 @@
9
9
  "use client";
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import Link from "next/link";
12
- import { Badge, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
12
+ import { Badge, Div, Grid, Heading, Row, Section, Stack, Text } from "../../../ui";
13
13
  import { MediaImage } from "../../media/MediaImage";
14
14
  import { ROUTES } from "../../../next/routing/route-map";
15
15
  import { formatCurrency } from "../../../utils/number.formatter";
@@ -29,7 +29,7 @@ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitl
29
29
  const items = initialItems.filter((c) => c.categoryType === "bundle");
30
30
  if (items.length === 0)
31
31
  return null;
32
- return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
32
+ return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
33
33
  }
34
34
  function FeaturedBundleCard({ bundle, onBuyNow }) {
35
35
  const stock = bundle.bundleStockStatus ?? "in_stock";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { normalizeError } from "../../../errors/normalize";
3
- import { Anchor, Div, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
3
+ import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
4
4
  import { MediaImage } from "../../media/MediaImage";
5
5
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
6
6
  import { fetchGoogleReviews } from "../lib/google-reviews-fetcher";
@@ -65,7 +65,7 @@ export async function GoogleReviewsSection(config) {
65
65
  }
66
66
  const gridClass = layout === "carousel"
67
67
  ? "flex gap-[var(--appkit-space-4)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
68
- : "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-[var(--appkit-space-4)]";
68
+ : `${GRID_MAP.cards} gap-[var(--appkit-space-4)]`;
69
69
  const cardClass = layout === "carousel" ? "flex-shrink-0 w-[85%] sm:w-72 snap-start" : "";
70
70
  const mapsHref = googleMapsUrl || (placeId ? `https://search.google.com/local/reviews?placeid=${placeId}` : "");
71
71
  return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", padding: "x-md", children: [_jsxs(HandModeRow, { className: "mb-8", align: "end", justify: "between", gap: "md", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, children: "What Our Customers Say" }), aggregateRating > 0 && (_jsxs(Row, { className: "mt-1", align: "center", gap: "sm", children: [_jsx(StarRating, { rating: Math.round(aggregateRating) }), _jsxs(Text, { size: "sm", variant: "muted", children: [aggregateRating.toFixed(1), " \u00B7 ", totalRatings.toLocaleString(), " reviews"] })] }))] }), linkToGoogleMaps && mapsHref && (_jsx(Anchor, { href: mapsHref, tone: "none", underline: "hover", className: CLS_GMAPS_LINK, children: "View on Google \u2192" }))] }), reviews.length === 0 ? (_jsx(EmptyState, {})) : (_jsx(Div, { className: gridClass, children: reviews.map((review, i) => (_jsx(Div, { className: cardClass, children: _jsx(ReviewCard, { review: review, showRating: showRating, showDate: showDate }) }, `${review.authorName}-${i}`))) }))] }) }));
@@ -17,5 +17,5 @@ export function HomepageSkeleton() {
17
17
  const trustCardH = "h-[clamp(120px,18vh,170px)]";
18
18
  const categoryTileH = "h-[clamp(112px,16vh,160px)]";
19
19
  const newsletterH = "h-[clamp(220px,28vh,320px)]";
20
- return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, { className: "grid-cols-2 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4", children: Array.from({ length: 4 }).map((_, i) => (_jsx(Stack, { className: `${skeleton.card} ${trustCardH}`, align: "center", gap: "3", padding: "lg" }, i))) }) }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsx(Div, { className: `${skeleton.heading} w-48 mx-auto mb-6` }), _jsx(Grid, { className: "grid-cols-2 sm:grid-cols-4", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: `${skeleton.card} ${categoryTileH}` }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "muted", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-52` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { className: "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-56` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { className: "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-5", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Div, { className: `${skeleton.card} max-w-2xl mx-auto ${newsletterH}`, rounded: "2xl" }) })] }));
20
+ return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 4 }).map((_, i) => (_jsx(Stack, { className: `${skeleton.card} ${trustCardH}`, align: "center", gap: "3", padding: "lg" }, i))) }) }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsx(Div, { className: `${skeleton.heading} w-48 mx-auto mb-6` }), _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: `${skeleton.card} ${categoryTileH}` }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "muted", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-52` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-56` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Div, { className: `${skeleton.card} max-w-2xl mx-auto ${newsletterH}`, rounded: "2xl" }) })] }));
21
21
  }
@@ -35,5 +35,5 @@ export function SecurityHighlightsSection({ title, subtitle, pillLabel, items, l
35
35
  observer.observe(el);
36
36
  return () => observer.disconnect();
37
37
  }, []);
38
- return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, { gap: "xl", className: "grid-cols-1 sm:grid-cols-2 xl:grid-cols-4", children: items.map((item, index) => (_jsx(SecurityCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.key))) }), learnMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel }) }))] }) }));
38
+ return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, { cols: "cards", gap: "xl", children: items.map((item, index) => (_jsx(SecurityCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.key))) }), learnMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel }) }))] }) }));
39
39
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { normalizeError } from "../../../errors/normalize";
3
- import { Anchor, Div, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
3
+ import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
4
4
  import { SocialPostCard } from "./SocialPostCard";
5
5
  import { fetchInstagramPosts, fetchFacebookPosts, fetchTikTokPosts, fetchDeviantArtPosts, } from "../lib/social-feed-fetcher";
6
6
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
@@ -93,7 +93,7 @@ export async function SocialFeedSection(config) {
93
93
  ? "flex gap-[var(--appkit-space-3)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
94
94
  : layout === "masonry"
95
95
  ? "columns-2 sm:columns-3 lg:columns-4 gap-[var(--appkit-space-3)] space-y-3"
96
- : "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-3)]";
96
+ : `${GRID_MAP.cards} gap-[var(--appkit-space-3)]`;
97
97
  const cardClass = layout === "carousel"
98
98
  ? "flex-shrink-0 w-[85%] sm:w-56 snap-start"
99
99
  : layout === "masonry"
@@ -31,5 +31,5 @@ export function StatsCounterSection({ stats, className = "", }) {
31
31
  observer.observe(el);
32
32
  return () => observer.disconnect();
33
33
  }, []);
34
- return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, { gap: "none", className: "grid-cols-2 md:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) }) }) }));
34
+ return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) }) }) }));
35
35
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Div, Row, Section, Stack, Text } from "../../../ui";
2
+ import { Div, Grid, Row, Section, Stack, Text } from "../../../ui";
3
3
  const BADGE_ICONS = {
4
4
  shipping: (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: [_jsx("path", { d: "M5 12H19" }), _jsx("path", { d: "M15 7l5 5-5 5" }), _jsx("path", { d: "M1 17V7a4 4 0 0 1 4-4h10" }), _jsx("path", { d: "M22 17a4 4 0 0 1-4 4H5" })] })),
5
5
  support: (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) })),
@@ -42,5 +42,5 @@ const FALLBACK_BADGES = [
42
42
  ];
43
43
  export function TrustBadges({ badges }) {
44
44
  const items = badges && badges.length > 0 ? badges : FALLBACK_BADGES;
45
- return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(Div, { layout: "grid", gap: "6", className: "grid-cols-2 sm:grid-cols-4", children: items.map((badge) => (_jsxs(Stack, { className: "text-left [flex-direction:row] items-[center] gap-[1rem]", align: "center", gap: "sm", children: [_jsx(Row, { className: "h-12 w-12 shrink-0 bg-[rgba(240,196,23,0.12)] text-[var(--color-red)] [border:2px_solid_var(--border-ink)]", align: "center", justify: "center", rounded: "full", children: BADGE_ICONS[badge.iconKey] }), _jsxs(Div, { children: [_jsx(Text, { className: "font-black uppercase tracking-wide text-[var(--section-title-color)]", size: "sm", children: badge.title }), _jsx(Text, { className: "text-[var(--color-muted)]", size: "xs", weight: "medium", children: badge.sub })] })] }, badge.id))) }) }) }));
45
+ return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((badge) => (_jsxs(Stack, { className: "text-left [flex-direction:row] items-[center] gap-[1rem]", align: "center", gap: "sm", children: [_jsx(Row, { className: "h-12 w-12 shrink-0 bg-[rgba(240,196,23,0.12)] text-[var(--color-red)] [border:2px_solid_var(--border-ink)]", align: "center", justify: "center", rounded: "full", children: BADGE_ICONS[badge.iconKey] }), _jsxs(Div, { children: [_jsx(Text, { className: "font-black uppercase tracking-wide text-[var(--section-title-color)]", size: "sm", children: badge.title }), _jsx(Text, { className: "text-[var(--color-muted)]", size: "xs", weight: "medium", children: badge.sub })] })] }, badge.id))) }) }) }));
46
46
  }
@@ -38,5 +38,5 @@ export function TrustFeaturesSection({ items, title, variant = "cards", classNam
38
38
  const doubled = [...items, ...items];
39
39
  return (_jsx(Section, { border: "subtle", ref: sectionRef, className: `overflow-hidden border-y ${className}`, padding: "y-md", children: _jsx(Div, { layout: "flex", className: "animate-marquee", children: doubled.map((item, i) => (_jsx(TrustStripItem, { item: item }, `${item.iconName}-${i}`))) }) }));
40
40
  }
41
- return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, { gap: "lg", className: "grid-cols-2 sm:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4", children: items.map((item, index) => (_jsx(TrustFeatureCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.iconName))) })] }) }));
41
+ return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((item, index) => (_jsx(TrustFeatureCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.iconName))) })] }) }));
42
42
  }
@@ -11,10 +11,11 @@
11
11
  */
12
12
  import { useCallback, useEffect, useState } from "react";
13
13
  import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
14
+ import { acquireRealtimeSession } from "../../../contracts/realtime-session";
14
15
  import { conversationPingPath, userConversationsPingPath, } from "../realtime";
15
16
  import { normalizeError } from "../../../errors/normalize";
16
17
  import { apiClient, ApiClientError } from "../../../http/ApiClient";
17
- import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
18
+ import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS, } from "../../../constants/api-endpoints";
18
19
  /**
19
20
  * @deprecated Re-exported for backwards compat — import `conversationPingPath`
20
21
  * from `@mohasinac/appkit` instead.
@@ -69,22 +70,45 @@ export function useConversation(conversationId) {
69
70
  setIsConnected(false);
70
71
  return;
71
72
  }
72
- try {
73
- const unsubscribe = getClientRealtimeProvider().subscribe(conversationPingPath(conversationId), () => {
74
- setIsConnected(true);
75
- void refetch();
76
- }, () => setIsConnected(false));
77
- return () => {
73
+ let unsubscribe;
74
+ let release;
75
+ let cancelled = false;
76
+ // 🛑 Sign in BEFORE subscribing. Without a session `auth == null`, and the
77
+ // `chats/$conversationId` rule requires
78
+ // `auth.token.conversationIds[$conversationId]` — a claim that, until this
79
+ // change, NOTHING issued (the token route only ever sent `chatIds`, from
80
+ // the unrelated `chatRooms` collection). Two stacked faults on one channel:
81
+ // no sign-in, and a claim with no issuer. The recipient of a message never
82
+ // saw it until they manually refreshed; the sender only saw their own
83
+ // because `send()` calls `refetch()` directly.
84
+ void (async () => {
85
+ try {
86
+ release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
87
+ if (cancelled) {
88
+ release?.();
89
+ return;
90
+ }
91
+ unsubscribe = getClientRealtimeProvider().subscribe(conversationPingPath(conversationId), () => {
92
+ setIsConnected(true);
93
+ void refetch();
94
+ }, (err) => {
95
+ void normalizeError(err);
96
+ setIsConnected(false);
97
+ });
98
+ }
99
+ catch (_err) {
100
+ void normalizeError(_err);
101
+ // Provider unregistered or token fetch failed — falls back to the
102
+ // one-shot fetch above.
78
103
  setIsConnected(false);
79
- unsubscribe();
80
- };
81
- }
82
- catch (_err) {
83
- void normalizeError(_err);
84
- // Realtime provider not registered — falls back to one-shot fetch above.
104
+ }
105
+ })();
106
+ return () => {
107
+ cancelled = true;
85
108
  setIsConnected(false);
86
- return undefined;
87
- }
109
+ unsubscribe?.();
110
+ release?.();
111
+ };
88
112
  }, [conversationId, refetch]);
89
113
  const sendMessage = useCallback(async (body) => {
90
114
  if (!conversationId)
@@ -6,10 +6,11 @@
6
6
  */
7
7
  import { useCallback, useEffect, useState } from "react";
8
8
  import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
9
+ import { acquireRealtimeSession } from "../../../contracts/realtime-session";
9
10
  import { userConversationsPingPath } from "../realtime";
10
11
  import { normalizeError } from "../../../errors/normalize";
11
12
  import { apiClient } from "../../../http/ApiClient";
12
- import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
13
+ import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
13
14
  const LIST_ENDPOINT = CONVERSATION_ENDPOINTS.LIST;
14
15
  async function fetchList(endpoint) {
15
16
  const data = await apiClient.get(endpoint);
@@ -46,19 +47,40 @@ export function useConversations(userId, opts = {}) {
46
47
  useEffect(() => {
47
48
  if (!userId)
48
49
  return;
49
- try {
50
- const unsubscribe = getClientRealtimeProvider().subscribe(userConversationsPingPath(userId), () => {
51
- void refetch();
52
- }, () => {
53
- // ignore — list will refresh on next focus / explicit refetch
54
- });
55
- return unsubscribe;
56
- }
57
- catch (_err) {
58
- void normalizeError(_err);
59
- // Realtime provider not registered — non-fatal; list still works.
60
- return undefined;
61
- }
50
+ let unsubscribe;
51
+ let release;
52
+ let cancelled = false;
53
+ // 🛑 Sign in BEFORE subscribing. This hook used to call subscribe() with no
54
+ // authenticated session at all, so `auth == null` and the
55
+ // `chats/user/$userId` rule (`auth.uid == $userId`) could never pass. Every
56
+ // update was permission-denied and the error handler below was a literal
57
+ // `// ignore`, so the whole live-list channel was inert while looking wired.
58
+ void (async () => {
59
+ try {
60
+ release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
61
+ if (cancelled) {
62
+ release?.();
63
+ return;
64
+ }
65
+ unsubscribe = getClientRealtimeProvider().subscribe(userConversationsPingPath(userId), () => {
66
+ void refetch();
67
+ }, (err) => {
68
+ // Surfaced, not ignored. A denied subscription is a real defect and
69
+ // silence here is what hid it for months.
70
+ void normalizeError(err);
71
+ });
72
+ }
73
+ catch (_err) {
74
+ void normalizeError(_err);
75
+ // Provider unregistered or token fetch failed — non-fatal; the list
76
+ // still works via the one-shot fetch above, just without live updates.
77
+ }
78
+ })();
79
+ return () => {
80
+ cancelled = true;
81
+ unsubscribe?.();
82
+ release?.();
83
+ };
62
84
  }, [userId, refetch]);
63
85
  const totalUnread = conversations.reduce((sum, c) => sum + (c[unreadField] ?? 0), 0);
64
86
  return { conversations, isLoading, error, totalUnread, refetch };
@@ -30,6 +30,29 @@ export declare class ConversationsRepository {
30
30
  findById(id: string): Promise<ConversationDocument | null>;
31
31
  listByBuyer(buyerId: string): Promise<ConversationDocument[]>;
32
32
  listByStore(storeId: string): Promise<ConversationDocument[]>;
33
+ /**
34
+ * Conversation ids this user may subscribe to, for the RTDB `conversationIds`
35
+ * custom-token claim.
36
+ *
37
+ * 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
38
+ * rule requires `auth.token.conversationIds[$conversationId] == true`, but
39
+ * `/api/realtime/token` only ever issued `chatIds`, built by
40
+ * `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
41
+ * a different collection with different document ids. So the buyer↔seller
42
+ * live-message channel was denied for every user, and both subscribing hooks
43
+ * swallowed the permission error (one into `setIsConnected(false)`, one into
44
+ * a literal `// ignore`). Messages only ever appeared for the sender, via an
45
+ * unrelated `refetch()` after send.
46
+ *
47
+ * `.select()` with no fields fetches ids only — the document bodies are not
48
+ * needed and this runs on every token issue.
49
+ *
50
+ * @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
51
+ * total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
52
+ * user in hundreds of conversations would make `createCustomToken` throw
53
+ * and take the whole route down (see the route's own comment).
54
+ */
55
+ getConversationIdsForUser(userId: string, storeIds?: readonly string[], limit?: number): Promise<string[]>;
33
56
  /**
34
57
  * Append a message to a conversation in a transaction. Bumps the counterparty
35
58
  * unread counter; updates lastMessage / lastMessageAt / updatedAt.
@@ -133,6 +133,59 @@ export class ConversationsRepository {
133
133
  .get();
134
134
  return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
135
135
  }
136
+ /**
137
+ * Conversation ids this user may subscribe to, for the RTDB `conversationIds`
138
+ * custom-token claim.
139
+ *
140
+ * 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
141
+ * rule requires `auth.token.conversationIds[$conversationId] == true`, but
142
+ * `/api/realtime/token` only ever issued `chatIds`, built by
143
+ * `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
144
+ * a different collection with different document ids. So the buyer↔seller
145
+ * live-message channel was denied for every user, and both subscribing hooks
146
+ * swallowed the permission error (one into `setIsConnected(false)`, one into
147
+ * a literal `// ignore`). Messages only ever appeared for the sender, via an
148
+ * unrelated `refetch()` after send.
149
+ *
150
+ * `.select()` with no fields fetches ids only — the document bodies are not
151
+ * needed and this runs on every token issue.
152
+ *
153
+ * @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
154
+ * total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
155
+ * user in hundreds of conversations would make `createCustomToken` throw
156
+ * and take the whole route down (see the route's own comment).
157
+ */
158
+ async getConversationIdsForUser(userId, storeIds = [], limit = 25) {
159
+ const queries = [
160
+ this.collection()
161
+ .where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
162
+ .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
163
+ .limit(limit)
164
+ .select()
165
+ .get(),
166
+ // Firestore `in` caps at 30 values; a seller with more stores than that
167
+ // is not a shape this app produces.
168
+ ...(storeIds.length > 0
169
+ ? [
170
+ this.collection()
171
+ .where(CONVERSATION_FIELDS.STORE_ID, "in", storeIds.slice(0, 30))
172
+ .orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
173
+ .limit(limit)
174
+ .select()
175
+ .get(),
176
+ ]
177
+ : []),
178
+ ];
179
+ const snaps = await Promise.all(queries);
180
+ const ids = new Set();
181
+ for (const snap of snaps) {
182
+ for (const doc of snap.docs)
183
+ ids.add(doc.id);
184
+ }
185
+ // Most-recent-first is already the sort order; truncating here keeps the
186
+ // claim inside the byte budget when a user is both buyer and seller.
187
+ return [...ids].slice(0, limit);
188
+ }
136
189
  /**
137
190
  * Append a message to a conversation in a transaction. Bumps the counterparty
138
191
  * unread counter; updates lastMessage / lastMessageAt / updatedAt.
@@ -27,7 +27,18 @@ export interface OrderWriteContext {
27
27
  declare class OrderRepository extends BaseRepository<OrderDocument> {
28
28
  constructor();
29
29
  private decryptOrder;
30
- private encryptOrderData;
30
+ /**
31
+ * Declared, not hand-rolled.
32
+ *
33
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
34
+ * `encryptOrderData` did — encrypt the listed fields, then add the mapped blind
35
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
36
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
37
+ * `*InBatch` family that inherited unhooked base implementations while the
38
+ * encryption lived in a private method only three call sites remembered to
39
+ * call.
40
+ */
41
+ protected piiFields: readonly ["userName", "userEmail", "sellerEmail"];
31
42
  protected mapDoc<D = OrderDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
32
43
  create(input: OrderCreateInput): Promise<OrderDocument>;
33
44
  findByUser(userId: string): Promise<OrderDocument[]>;
@@ -3,7 +3,7 @@ import { planSearchTxt, refineSearchTxt, emptySearchResult, } from "../../../uti
3
3
  import { normalizeError } from "../../../errors/normalize";
4
4
  import { DatabaseError, NotFoundError } from "../../../errors";
5
5
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
6
- import { decryptPiiFields, encryptPiiFields, ORDER_PII_FIELDS, } from "../../../security";
6
+ import { decryptPiiFields, ORDER_PII_FIELDS, } from "../../../security";
7
7
  import { serverTimestamp, arrayUnion } from "../../../contracts/field-ops";
8
8
  import { createOrderId, ORDER_COLLECTION, ORDER_TRACKED_FIELDS, OrderStatusValues, } from "../schemas";
9
9
  import { withHistory } from "../../../_internal/shared/history/index";
@@ -24,15 +24,24 @@ const ACTIVE_ALLOWANCE_STATUSES = [
24
24
  class OrderRepository extends BaseRepository {
25
25
  constructor() {
26
26
  super(ORDER_COLLECTION);
27
+ /**
28
+ * Declared, not hand-rolled.
29
+ *
30
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
31
+ * `encryptOrderData` did — encrypt the listed fields, then add the mapped blind
32
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
33
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
34
+ * `*InBatch` family that inherited unhooked base implementations while the
35
+ * encryption lived in a private method only three call sites remembered to
36
+ * call.
37
+ */
38
+ this.piiFields = ORDER_PII_FIELDS;
27
39
  }
28
40
  decryptOrder(doc) {
29
41
  return decryptPiiFields(doc, [
30
42
  ...ORDER_PII_FIELDS,
31
43
  ]);
32
44
  }
33
- encryptOrderData(data) {
34
- return encryptPiiFields(data, [...ORDER_PII_FIELDS]);
35
- }
36
45
  mapDoc(snap) {
37
46
  const raw = super.mapDoc(snap);
38
47
  return this.decryptOrder(raw);
@@ -46,7 +55,7 @@ class OrderRepository extends BaseRepository {
46
55
  createdAt: new Date(),
47
56
  updatedAt: new Date(),
48
57
  };
49
- const encrypted = this.encryptOrderData(orderData);
58
+ const encrypted = this.applyWriteHooks(orderData);
50
59
  await this.db
51
60
  .collection(this.collection)
52
61
  .doc(id)
@@ -20,17 +20,27 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
20
20
  /** Decrypt PII fields on a payout document after Firestore read */
21
21
  private decryptPayout;
22
22
  /**
23
- * Encrypt PII, then attach blind indices derived from the plaintext.
23
+ * Declared, not hand-rolled.
24
24
  *
25
- * Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
26
- * re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
27
- * then spread that over the ciphertext, so plaintext won and `sellerEmail`
28
- * and `upiId` were written to Firestore in the clear beside a valid index.
29
- * `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
30
- * overwritten. The identical bug was fixed in UserRepository and never
31
- * propagated here.
25
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
26
+ * `encryptPayoutData` did — encrypt the listed fields, then add the mapped blind
27
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
28
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
29
+ * `*InBatch` family that inherited unhooked base implementations while the
30
+ * encryption lived in a private method only three call sites remembered to
31
+ * call.
32
32
  */
33
- private encryptPayoutData;
33
+ protected piiFields: readonly ["sellerEmail", "upiId"];
34
+ protected piiIndexMap: Record<string, string>;
35
+ /**
36
+ * The base hook, plus the nested objects it cannot see.
37
+ *
38
+ * `piiFields` is a flat list of top-level names, so a nested blob has to be
39
+ * encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
40
+ * encryption path with one extra step on top, rather than two parallel paths
41
+ * that have to agree — which is what the hand-rolled override was.
42
+ */
43
+ protected applyWriteHooks<D extends object>(data: D): D;
34
44
  /** Override mapDoc to auto-decrypt PII on every Firestore read */
35
45
  protected mapDoc<D = PayoutDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
36
46
  /**
@@ -7,10 +7,23 @@
7
7
  import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
8
8
  import { PAYOUT_COLLECTION, PAYOUT_FIELDS, PAYOUT_TRACKED_FIELDS, createPayoutId } from "../schemas";
9
9
  import { withHistory } from "../../../_internal/shared/history/index";
10
- import { encryptPiiFields, decryptPiiFields, piiIndicesFor, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
10
+ import { decryptPiiFields, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
11
11
  export class PayoutRepository extends BaseRepository {
12
12
  constructor() {
13
13
  super(PAYOUT_COLLECTION);
14
+ /**
15
+ * Declared, not hand-rolled.
16
+ *
17
+ * `BaseRepository.applyWriteHooks` already does exactly what the private
18
+ * `encryptPayoutData` did — encrypt the listed fields, then add the mapped blind
19
+ * indices derived from the PLAINTEXT source. Declaring the fields means all
20
+ * nine mutating methods get it, including the `createWithId` / `*InTx` /
21
+ * `*InBatch` family that inherited unhooked base implementations while the
22
+ * encryption lived in a private method only three call sites remembered to
23
+ * call.
24
+ */
25
+ this.piiFields = PAYOUT_PII_FIELDS;
26
+ this.piiIndexMap = PAYOUT_PII_INDEX_MAP;
14
27
  }
15
28
  /** Decrypt PII fields on a payout document after Firestore read */
16
29
  decryptPayout(doc) {
@@ -21,26 +34,19 @@ export class PayoutRepository extends BaseRepository {
21
34
  return decrypted;
22
35
  }
23
36
  /**
24
- * Encrypt PII, then attach blind indices derived from the plaintext.
37
+ * The base hook, plus the nested objects it cannot see.
25
38
  *
26
- * Previously this reassigned `encrypted = addPiiIndices(data, …)` — which
27
- * re-reads the ORIGINAL plaintext and returns `{...data, ...indices}` — and
28
- * then spread that over the ciphertext, so plaintext won and `sellerEmail`
29
- * and `upiId` were written to Firestore in the clear beside a valid index.
30
- * `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
31
- * overwritten. The identical bug was fixed in UserRepository and never
32
- * propagated here.
39
+ * `piiFields` is a flat list of top-level names, so a nested blob has to be
40
+ * encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
41
+ * encryption path with one extra step on top, rather than two parallel paths
42
+ * that have to agree — which is what the hand-rolled override was.
33
43
  */
34
- encryptPayoutData(data) {
35
- const encrypted = {
36
- ...encryptPiiFields(data, [...PAYOUT_PII_FIELDS]),
37
- ...piiIndicesFor(data, PAYOUT_PII_INDEX_MAP),
38
- };
39
- const access = encrypted;
40
- if (access.bankAccount) {
44
+ applyWriteHooks(data) {
45
+ const out = super.applyWriteHooks(data);
46
+ const access = out;
47
+ if (access.bankAccount)
41
48
  access.bankAccount = encryptPayoutBankAccount(access.bankAccount);
42
- }
43
- return encrypted;
49
+ return out;
44
50
  }
45
51
  /** Override mapDoc to auto-decrypt PII on every Firestore read */
46
52
  mapDoc(snap) {
@@ -61,7 +67,7 @@ export class PayoutRepository extends BaseRepository {
61
67
  updatedAt: now,
62
68
  };
63
69
  // Encrypt PII before persisting
64
- const encrypted = this.encryptPayoutData(data);
70
+ const encrypted = this.applyWriteHooks(data);
65
71
  await this.db
66
72
  .collection(this.collection)
67
73
  .doc(id)
@@ -70,7 +76,7 @@ export class PayoutRepository extends BaseRepository {
70
76
  }
71
77
  /** Override base update to preserve encryption/indexing for changed PII fields. */
72
78
  async update(payoutId, data) {
73
- const encrypted = this.encryptPayoutData(data);
79
+ const encrypted = this.applyWriteHooks(data);
74
80
  return super.update(payoutId, encrypted);
75
81
  }
76
82
  /**
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
4
4
  import { Columns, Heart, ShoppingCart } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useProducts } from "../../products/hooks/useProducts";
7
- import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
7
+ import { BulkActionBar, Div, FilterDrawer, Grid, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
8
8
  import { usePendingTable } from "../../../react/hooks/usePendingTable";
9
9
  import { useAuthGate } from "../../../react/hooks/useAuthGate";
10
10
  import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
@@ -133,7 +133,6 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
133
133
  showToast("Added to cart", "success");
134
134
  }, [localCart, showToast]);
135
135
  const selection = useBulkSelection({ items: preOrders, keyExtractor: (p) => p.id });
136
- const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-4)]";
137
136
  const handleBulkAddToCart = useCallback(() => {
138
137
  const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
139
138
  selected.forEach((p) => {
@@ -227,7 +226,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
227
226
  setCompareIds(ids);
228
227
  },
229
228
  },
230
- ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8", surface: "subtle", rounded: "default" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No pre-orders found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx(Div, { className: gridClass, children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
229
+ ] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Grid, { cols: "cards", gap: "md", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8", surface: "subtle", rounded: "default" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No pre-orders found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx(Grid, { cols: "cards", gap: "md", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
231
230
  setCompareIds([]);
232
231
  selection.clearSelection();
233
232
  }, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));