@mohasinac/appkit 4.30.18 → 4.30.19

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 (162) hide show
  1. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
  4. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
  5. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
  6. package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
  7. package/dist/_internal/server/features/auctions/actions.js +14 -3
  8. package/dist/_internal/server/features/auctions/service.js +1 -1
  9. package/dist/_internal/server/features/blog/data.js +2 -2
  10. package/dist/_internal/server/features/blog/service.js +3 -3
  11. package/dist/_internal/server/features/brands/data.js +1 -1
  12. package/dist/_internal/server/features/brands/service.js +3 -3
  13. package/dist/_internal/server/features/bundles/actions.js +6 -1
  14. package/dist/_internal/server/features/bundles/data.js +21 -4
  15. package/dist/_internal/server/features/cart/data.js +3 -1
  16. package/dist/_internal/server/features/cart/service.js +13 -2
  17. package/dist/_internal/server/features/categories/data.js +12 -3
  18. package/dist/_internal/server/features/events/actions.js +17 -4
  19. package/dist/_internal/server/features/events/data.js +1 -1
  20. package/dist/_internal/server/features/events/service.js +1 -1
  21. package/dist/_internal/server/features/grouped/data.js +7 -2
  22. package/dist/_internal/server/features/item-requests/data.js +1 -1
  23. package/dist/_internal/server/features/lottery/actions.js +2 -2
  24. package/dist/_internal/server/features/lottery/data.js +1 -1
  25. package/dist/_internal/server/features/orders/actions.js +7 -7
  26. package/dist/_internal/server/features/orders/data.d.ts +1 -1
  27. package/dist/_internal/server/features/orders/data.js +14 -2
  28. package/dist/_internal/server/features/orders/service.js +1 -1
  29. package/dist/_internal/server/features/pre-orders/service.js +1 -1
  30. package/dist/_internal/server/features/prize-draws/actions.js +1 -1
  31. package/dist/_internal/server/features/products/data.js +7 -2
  32. package/dist/_internal/server/features/products/service.js +1 -1
  33. package/dist/_internal/server/features/promotions/actions.js +1 -1
  34. package/dist/_internal/server/features/promotions/data.js +1 -1
  35. package/dist/_internal/server/features/promotions/service.js +1 -1
  36. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  37. package/dist/_internal/server/features/reviews/data.js +6 -1
  38. package/dist/_internal/server/features/reviews/og-data.js +1 -1
  39. package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
  40. package/dist/_internal/server/features/stores/data.js +1 -1
  41. package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
  42. package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
  44. package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
  45. package/dist/client/api/useApiQuery.js +17 -6
  46. package/dist/client.d.ts +1 -0
  47. package/dist/client.js +3 -0
  48. package/dist/contracts/client-realtime.d.ts +6 -0
  49. package/dist/contracts/realtime-session.d.ts +12 -12
  50. package/dist/contracts/realtime-session.js +72 -77
  51. package/dist/errors/index.d.ts +1 -1
  52. package/dist/errors/index.js +1 -1
  53. package/dist/features/about/components/DeveloperView.js +7 -2
  54. package/dist/features/about/components/PolicyPageView.js +9 -1
  55. package/dist/features/about/components/PublicProfileView.js +23 -5
  56. package/dist/features/account/components/AddressForm.js +1 -1
  57. package/dist/features/account/components/NotificationBell.js +4 -3
  58. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  59. package/dist/features/account/components/UserOffersPanel.js +1 -1
  60. package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
  61. package/dist/features/admin/components/AdminMediaView.js +3 -3
  62. package/dist/features/analytics/types.d.ts +1 -1
  63. package/dist/features/analytics/types.js +13 -0
  64. package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
  65. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
  66. package/dist/features/auth/actions/profile-actions.js +4 -3
  67. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  68. package/dist/features/auth/components/LoginForm.js +1 -1
  69. package/dist/features/auth/components/RegisterForm.js +1 -1
  70. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  71. package/dist/features/auth/hooks/useAuth.js +3 -0
  72. package/dist/features/blog/repository/blog.repository.js +3 -1
  73. package/dist/features/cart/actions/cart-actions.js +13 -4
  74. package/dist/features/cart/utils/pending-ops.js +1 -1
  75. package/dist/features/categories/components/BrandDetailPageView.js +28 -12
  76. package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
  77. package/dist/features/categories/repository/categories.repository.js +3 -1
  78. package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
  79. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
  80. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
  81. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
  82. package/dist/features/events/components/EventBanner.js +5 -2
  83. package/dist/features/events/components/EventOfferCard.js +3 -1
  84. package/dist/features/events/components/EventPollWidget.js +1 -1
  85. package/dist/features/events/components/SpinWheelView.js +3 -2
  86. package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
  87. package/dist/features/history/hooks/useHistory.js +29 -27
  88. package/dist/features/homepage/components/AdSlot.js +3 -1
  89. package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
  90. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
  91. package/dist/features/homepage/components/SocialFeedSection.js +4 -1
  92. package/dist/features/homepage/lib/live-stats.js +8 -1
  93. package/dist/features/homepage/repository/carousel.repository.js +2 -1
  94. package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
  95. package/dist/features/media/finalize.js +3 -1
  96. package/dist/features/media/modals/ImageEditor.js +1 -1
  97. package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
  98. package/dist/features/media/modals/VideoTrimModal.js +5 -2
  99. package/dist/features/media/upload/MediaUploadField.js +2 -2
  100. package/dist/features/messages/hooks/useConversation.js +18 -4
  101. package/dist/features/messages/hooks/useConversations.js +25 -8
  102. package/dist/features/orders/components/RefundRequestView.js +5 -2
  103. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
  104. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  105. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  106. package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
  107. package/dist/features/products/components/ProductDetailPageView.js +8 -2
  108. package/dist/features/products/components/ShareButton.js +3 -1
  109. package/dist/features/products/repository/loadProductFeatures.js +10 -2
  110. package/dist/features/products/repository/products.repository.js +2 -1
  111. package/dist/features/promotions/components/CouponCard.js +3 -1
  112. package/dist/features/reviews/actions/review-actions.js +8 -1
  113. package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
  114. package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
  115. package/dist/features/scams/actions/scam-actions.d.ts +9 -1
  116. package/dist/features/scams/actions/scam-actions.js +52 -12
  117. package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
  118. package/dist/features/scams/components/SellerTrustBadge.js +13 -0
  119. package/dist/features/scams/repository/scammer.repository.js +2 -1
  120. package/dist/features/seller/actions/offer-actions.js +6 -1
  121. package/dist/features/seller/components/FulfillmentView.js +5 -2
  122. package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
  123. package/dist/features/seller/components/PrintCenterView.js +3 -1
  124. package/dist/features/seller/components/SellerBidsView.js +1 -1
  125. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  126. package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
  127. package/dist/features/seller/components/SellerOrdersView.js +3 -3
  128. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  129. package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
  130. package/dist/features/seller/components/SellerProductShell.js +15 -5
  131. package/dist/features/seller/components/SellerProductsView.js +24 -10
  132. package/dist/features/seller/components/SellerReviewsView.js +47 -16
  133. package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
  134. package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
  135. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  136. package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
  137. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
  138. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  139. package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
  140. package/dist/index.d.ts +1 -1
  141. package/dist/index.js +5 -2
  142. package/dist/next/api/routeHandler.js +3 -1
  143. package/dist/providers/auth-firebase/session.js +3 -1
  144. package/dist/providers/db-firebase/admin-app-lite.js +8 -8
  145. package/dist/providers/db-firebase/admin.js +12 -12
  146. package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
  147. package/dist/providers/db-firebase/browser-stub.js +6 -11
  148. package/dist/providers/db-firebase/index.d.ts +0 -1
  149. package/dist/providers/db-firebase/index.js +0 -1
  150. package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
  151. package/dist/providers/db-firebase/rtdb-paths.js +15 -6
  152. package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
  153. package/dist/providers/db-firebase/rtdb-url.js +50 -0
  154. package/dist/providers/firebase-client/realtime.d.ts +20 -0
  155. package/dist/providers/firebase-client/realtime.js +34 -1
  156. package/dist/providers.d.ts +1 -1
  157. package/dist/providers.js +1 -1
  158. package/dist/react/hooks/use-action-dispatch.js +8 -2
  159. package/dist/react/hooks/useFormStatePreservation.js +3 -1
  160. package/dist/react/hooks/useRealtimeEvent.js +20 -9
  161. package/dist/ui/components/toolbar-collapse.js +1 -2
  162. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { normalizeError } from "../../../errors/normalize";
3
+ import { serverLogger } from "../../../monitoring/server-logger";
3
4
  import { Anchor, Avatar, Badge, Div, Heading, Row, Section, Span, Stack, Text, TextLink } from "../../../ui";
4
5
  import { ROUTES } from "../../../constants";
5
6
  import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
@@ -21,8 +22,12 @@ export async function DeveloperView({ labels = {} }) {
21
22
  milestones = Array.isArray(aboutContent.milestones) ? aboutContent.milestones : [];
22
23
  }
23
24
  catch (_err) {
24
- void normalizeError(_err);
25
- // Firestore unavailable — render the empty state below rather than crash.
25
+ // Firestore unavailable — render the empty state below rather than crash,
26
+ // but say so: an empty Developers page is indistinguishable from a settings
27
+ // document that genuinely has no `isDeveloper` members.
28
+ serverLogger.warn("about.developerView: siteSettings.aboutContent read failed", {
29
+ error: normalizeError(_err).message,
30
+ });
26
31
  }
27
32
  const developers = teamMembers.filter((m) => m.isDeveloper);
28
33
  return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [_jsx(Section, { color: "inverse", tone: "accent-banner", padding: "banner", children: _jsxs(Div, { className: `${page.container.sm}`, children: [_jsx(Heading, { color: "inverse", level: 1, variant: "none", className: "mb-3", children: labels.title ?? "Meet the Developer" }), _jsx(Text, { color: "inverse", variant: "none", className: "/80", size: "sm", children: labels.subtitle ?? "The person building LetItRip, one feature at a time." })] }) }), _jsxs(Div, { className: `${page.container.sm}`, padding: "content-banner", children: [developers.length === 0 ? (_jsx(Text, { variant: "secondary", children: labels.empty ?? "Developer profile coming soon." })) : (_jsx(Stack, { gap: "xl", children: developers.map((dev) => (_jsxs(Stack, { gap: "md", surface: "default", padding: "lg", rounded: "xl", border: "default", children: [_jsxs(Row, { gap: "md", align: "center", children: [_jsx(Avatar, { src: dev.photoUrl, name: dev.name, alt: dev.name, size: "lg" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, children: dev.name }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: dev.role }), dev.isFounder && _jsx(Badge, { variant: "primary", size: "xs", children: "Founder" }), _jsx(Badge, { variant: "secondary", size: "xs", children: "Developer" })] })] })] }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", children: dev.bio }), dev.githubUrl && (_jsx(Anchor, { href: dev.githubUrl, tone: "brand", size: "sm", underline: "hover", children: "GitHub \u2197" }))] }, dev.name))) })), milestones.length > 0 && (_jsxs(Div, { className: "mt-14 border-t", border: "default", padding: "t-xl", children: [_jsx(Heading, { level: 3, className: "mb-6", color: "muted", size: "sm", weight: "semibold", transform: "uppercase", children: labels.journeyTitle ?? "The Journey" }), _jsx(Stack, { className: "relative border-l-2 border-primary/30 max-w-2xl pl-[2rem]", gap: "lg", children: milestones.map(({ year, text }) => (_jsxs(Div, { className: "relative", children: [_jsx(Div, { className: "absolute -left-10 top-1 w-4 h-4 bg-primary border-2 border-white", rounded: "full" }), _jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: year }), _jsx(Text, { className: "mt-1", children: text })] }, year))) })] })), _jsx(Div, { className: "mt-10", children: _jsx(TextLink, { href: String(ROUTES.PUBLIC.ABOUT), variant: "muted", size: "sm", children: labels.backToTeam ?? "← Meet the whole team" }) })] })] }));
@@ -1,5 +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 { serverLogger } from "../../../monitoring/server-logger";
3
4
  import { Row } from "@mohasinac/appkit/ui";
4
5
  import { ROUTES } from "../../../constants";
5
6
  import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
@@ -72,7 +73,14 @@ export async function PolicyPageView({ policy, }) {
72
73
  }
73
74
  catch (_err) {
74
75
  void normalizeError(_err);
75
- // Firestore unavailable — fall back to i18n
76
+ // Falls back to the bundled i18n copy below, which is a complete policy in
77
+ // its own right — but an admin who has overridden this page would silently
78
+ // see the old text, so the failure has to be visible to an operator.
79
+ serverLogger.warn("about.policyPageView: siteSettings.legalPages read failed", {
80
+ policy,
81
+ field: meta.firestoreField,
82
+ error: normalizeError(_err).message,
83
+ });
76
84
  }
77
85
  // i18n fallback sections
78
86
  const rawSections = t.raw("sections");
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import Link from "next/link";
3
3
  import { getPublicUserProfile, getProfileStoreProducts, getReviewsAuthoredBy, getReviewsReceivedBy } from "../../auth/actions/profile-actions";
4
4
  import { storeRepository } from "../../stores/repository/store.repository";
5
+ import { safeRead } from "../../../errors/safe-read";
5
6
  import { ROUTES } from "../../../constants";
6
7
  import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
7
8
  import { FLEX_CENTER } from "../../../_internal/shared/styles/themed";
@@ -64,13 +65,30 @@ export async function PublicProfileView({ userId, tab, }) {
64
65
  const flex = { center: FLEX_CENTER };
65
66
  const { getTranslations } = await import("next-intl/server");
66
67
  const t = await getTranslations("publicProfile");
67
- const profile = await getPublicUserProfile(userId).catch(() => null);
68
+ // The profile IS this page's subject — a read failure must surface, not turn
69
+ // into the same `null` a genuinely missing user produces. The `null` return
70
+ // from getPublicUserProfile still means "no such user".
71
+ const profile = await getPublicUserProfile(userId);
68
72
  const storeId = profile?.storeSlug ?? null;
69
73
  const [products, store, reviewsAuthored, reviewsReceivedAsBuyer] = await Promise.all([
70
- storeId ? getProfileStoreProducts(storeId).catch(() => []) : Promise.resolve([]),
71
- storeId ? storeRepository.findById(storeId).catch(() => null) : Promise.resolve(null),
72
- getReviewsAuthoredBy(userId).catch(() => []),
73
- getReviewsReceivedBy(userId).catch(() => []),
74
+ storeId
75
+ ? safeRead(() => getProfileStoreProducts(storeId), {
76
+ route: "/profile/[userId]",
77
+ key: "profile.storeProducts",
78
+ fallback: [],
79
+ })
80
+ : Promise.resolve([]),
81
+ storeId
82
+ ? safeRead(() => storeRepository.findById(storeId), {
83
+ route: "/profile/[userId]",
84
+ key: "profile.store",
85
+ fallback: null,
86
+ })
87
+ : Promise.resolve(null),
88
+ // Both already degrade internally via safeRead — anything still thrown here
89
+ // is a mapping bug, which should surface rather than render an empty rail.
90
+ getReviewsAuthoredBy(userId),
91
+ getReviewsReceivedBy(userId),
74
92
  ]);
75
93
  const isSeller = isSellerUser(profile) || isAdminUser(profile);
76
94
  // For sellers: lead with store identity (name + logo). For buyers: user identity.
@@ -102,7 +102,7 @@ export function AddressForm({ initialData, onSubmit, onCancel, isLoading = false
102
102
  }
103
103
  catch (err) {
104
104
  void normalizeError(err);
105
- showToast(err instanceof Error ? err.message : "Failed to save address", "error");
105
+ showToast("Failed to save address", "error");
106
106
  }
107
107
  }, [formData, onSubmit, showToast, setFieldError, clearErrors, markSubmitAttempted]);
108
108
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
+ import { toUserMessage } from "../../../errors/error-display-map";
4
5
  import { useCallback, useRef, useState } from "react";
5
6
  import { Button, Div, Heading, Li, Row, Span, Spinner, Stack, Text, TextLink, Ul, useToast } from "../../../ui";
6
7
  import { useClickOutside, useMessage } from "../../../react";
@@ -52,7 +53,7 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
52
53
  }
53
54
  catch (err) {
54
55
  void normalizeError(err);
55
- showToast(err instanceof Error ? err.message : "Failed to mark notification as read", "error");
56
+ showToast("Failed to mark notification as read", "error");
56
57
  }
57
58
  }, [markRead, refetch, showToast]);
58
59
  const emitSuccess = onMarkAllReadSuccess ?? showSuccess;
@@ -63,12 +64,12 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
63
64
  emitSuccess(labels.markAllRead);
64
65
  }
65
66
  catch (err) {
66
- void normalizeError(err);
67
+ const e = normalizeError(err);
67
68
  if (onMarkAllReadError) {
68
69
  onMarkAllReadError(labels.error);
69
70
  }
70
71
  else {
71
- showToast(err instanceof Error ? err.message : labels.error, "error");
72
+ showToast(toUserMessage(e.code, undefined, { fallback: labels.error }), "error");
72
73
  }
73
74
  }
74
75
  }, [
@@ -79,7 +79,7 @@ export function NotificationPreferencesPanel({ fetchUrl = ACCOUNT_ENDPOINTS.NOTI
79
79
  }
80
80
  catch (err) {
81
81
  void normalizeError(err);
82
- showToast(err instanceof Error ? err.message : "Failed to save preferences.", "error");
82
+ showToast("Failed to save preferences.", "error");
83
83
  }
84
84
  finally {
85
85
  setSaving(false);
@@ -55,7 +55,7 @@ function BuyerOfferCard({ offer, onAcceptCounter, onWithdraw, onCheckout, onUpda
55
55
  onNeedsLogin();
56
56
  }
57
57
  else {
58
- setError(err instanceof Error ? err.message : "Something went wrong.");
58
+ setError("Something went wrong.");
59
59
  }
60
60
  }
61
61
  });
@@ -40,12 +40,15 @@ export function AdminFulfillmentView({ initialStoreId, initialStoreName, }) {
40
40
  setOrders(res.orders ?? []);
41
41
  }
42
42
  catch (err) {
43
- void normalizeError(err);
43
+ // An empty fulfilment queue is a normal, expected state, so a swallowed
44
+ // failure here reads as "nothing to pack" — matching the rest of this
45
+ // file, surface it instead.
46
+ showToast(normalizeError(err).message, "error");
44
47
  }
45
48
  finally {
46
49
  setIsLoading(false);
47
50
  }
48
- }, []);
51
+ }, [showToast]);
49
52
  useEffect(() => {
50
53
  if (storeId)
51
54
  void fetchOrders(storeId);
@@ -48,7 +48,7 @@ function MediaBrowser({ onCopy }) {
48
48
  }
49
49
  catch (err) {
50
50
  void normalizeError(err);
51
- setError(err instanceof Error ? err.message : "Failed to load");
51
+ setError("Failed to load");
52
52
  }
53
53
  finally {
54
54
  setIsLoading(false);
@@ -108,7 +108,7 @@ export function AdminMediaView({ labels = {}, children, ...rest }) {
108
108
  catch (err) {
109
109
  void normalizeError(err);
110
110
  setOperationMessage("Failed to discard staged media uploads.");
111
- showToast(err instanceof Error ? err.message : "Failed to clear uploads.", "error");
111
+ showToast("Failed to clear uploads.", "error");
112
112
  }
113
113
  }, [cleanup, stagedUrls, showToast]);
114
114
  const copyToClipboard = React.useCallback(async (url) => {
@@ -121,7 +121,7 @@ export function AdminMediaView({ labels = {}, children, ...rest }) {
121
121
  catch (err) {
122
122
  void normalizeError(err);
123
123
  setOperationMessage("Could not copy URL to clipboard.");
124
- showToast(err instanceof Error ? err.message : "Failed to copy.", "error");
124
+ showToast("Failed to copy.", "error");
125
125
  }
126
126
  }, [showToast]);
127
127
  if (hasChildren) {
@@ -2,5 +2,5 @@
2
2
  * page-views.repository.ts (server-only, imports firebase-admin) so the
3
3
  * client-side tracker component can import just this without pulling
4
4
  * firebase-admin into the client bundle. */
5
- export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle", "grouped-listing"];
5
+ export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle", "grouped-listing", "listing"];
6
6
  export type PageViewEntityType = (typeof PAGE_VIEW_ENTITY_TYPES)[number];
@@ -21,4 +21,17 @@ export const PAGE_VIEW_ENTITY_TYPES = [
21
21
  /** A `groupedListings` document's own page. Distinct from "bundle" — a
22
22
  * bundle is a priced, all-or-nothing set; a group is pick-as-you-wish. */
23
23
  "grouped-listing",
24
+ /**
25
+ * A public BROWSE page — `/products`, `/auctions`, `/stores`, … — as opposed
26
+ * to one entity's detail page.
27
+ *
28
+ * 🛑 `entityId` must be the bare route name ("products"), NEVER the filtered
29
+ * URL. `pageViews` doc ids are `${date}_${entityType}_${entityId}` with a
30
+ * `FieldValue.increment`, so a bounded id costs ONE write per day per page,
31
+ * while keying on a query string would create a new document per filter
32
+ * combination — unbounded doc creation on the highest-traffic pages, against
33
+ * the Firestore write budget in Rule #6. The full URL still travels in the
34
+ * `url` field, which is recorded but not part of the key.
35
+ */
36
+ "listing",
24
37
  ];
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import Link from "next/link";
3
3
  import { productRepository } from "../../../repositories";
4
4
  import { listBidsByProduct } from "../../auctions/actions/bid-actions";
5
+ import { safeRead } from "../../../errors/safe-read";
5
6
  const __P = {
6
7
  p3: "p-[var(--appkit-space-3)]",
7
8
  p4: "p-[var(--appkit-space-4)]",
@@ -62,11 +63,14 @@ function renderAuctionStoreReviews(storeSlug) {
62
63
  return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsx(ReviewsListingPanel, { source: { kind: "store", storeSlug }, stateMode: "local", context: "store", emptyLabel: "This store has no reviews yet." })] }));
63
64
  }
64
65
  export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
66
+ // The auction IS this page's subject. A read failure must surface rather than
67
+ // become the same `undefined` a genuinely missing slug produces — the
68
+ // "Auction Not Found" branch below still handles that real case.
65
69
  const product = initialAuction !== undefined
66
70
  ? (initialAuction ?? undefined)
67
- : await productRepository.findByIdOrSlug(id).catch(() => undefined);
71
+ : await productRepository.findByIdOrSlug(id);
68
72
  const bidsResult = product
69
- ? await listBidsByProduct(String(product.id), { pageSize: 5 }).catch(() => null)
73
+ ? await safeRead(() => listBidsByProduct(String(product.id), { pageSize: 5 }), { route: "/auctions/[slug]", key: "auction.bidHistory", fallback: null })
70
74
  : null;
71
75
  const storeId = product?.storeId;
72
76
  if (!product) {
@@ -163,8 +163,10 @@ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", v
163
163
  await toggle();
164
164
  }
165
165
  catch (_err) {
166
+ // useWishlistToggle rolls back its optimistic state and raises its own
167
+ // toast on failure, so the heart returns to its true value on screen.
168
+ // Reporting again here would double-notify for one user action.
166
169
  void normalizeError(_err);
167
- // The hook rolls back optimistic state when the mutation fails.
168
170
  }
169
171
  }, [toggle]);
170
172
  const countdownClass = isEnded
@@ -5,6 +5,7 @@
5
5
  * Auth, rate-limiting, and Next.js specifics are handled by the consumer.
6
6
  */
7
7
  import { maskPublicReview } from "../../../security";
8
+ import { safeRead } from "../../../errors/safe-read";
8
9
  import { finalizeStagedMediaField } from "../../media/finalize";
9
10
  import { userRepository } from "../repository/user.repository";
10
11
  import { sessionRepository } from "../repository/session.repository";
@@ -65,7 +66,7 @@ export async function getPublicUserProfile(userId) {
65
66
  }
66
67
  /** Fetch approved reviews for a store. storeId === storeSlug in this project. */
67
68
  export async function getSellerReviews(storeId) {
68
- const snapshot = await reviewRepository.findApprovedByStore(storeId).catch(() => []);
69
+ const snapshot = await safeRead(() => reviewRepository.findApprovedByStore(storeId), { route: "/profile/[userId]", key: "reviews.findApprovedByStore", fallback: [] });
69
70
  return snapshot.map((r) => ({
70
71
  id: r.id,
71
72
  productId: r.productId,
@@ -94,7 +95,7 @@ export async function getProfileStoreProducts(storeId) {
94
95
  }
95
96
  /** Approved product reviews written by this user as a buyer. Excludes seller→buyer ratings. */
96
97
  export async function getReviewsAuthoredBy(userId) {
97
- const all = await reviewRepository.findApprovedByUser(userId).catch(() => []);
98
+ const all = await safeRead(() => reviewRepository.findApprovedByUser(userId), { route: "/profile/[userId]", key: "reviews.findApprovedByUser", fallback: [] });
98
99
  // reviewerRole:"seller" means this user rated a buyer — not a product review
99
100
  const snapshot = all.filter((r) => r.reviewerRole !== "seller");
100
101
  return snapshot.map((r) => ({
@@ -121,7 +122,7 @@ export async function getReviewsAuthoredBy(userId) {
121
122
  }
122
123
  /** Approved seller→buyer reviews received by this buyer. */
123
124
  export async function getReviewsReceivedBy(userId) {
124
- const snapshot = await reviewRepository.findByReviewee(userId).catch(() => []);
125
+ const snapshot = await safeRead(() => reviewRepository.findByReviewee(userId), { route: "/profile/[userId]", key: "reviews.findByReviewee", fallback: [] });
125
126
  return snapshot.map((r) => ({
126
127
  id: r.id,
127
128
  productId: r.productId,
@@ -22,7 +22,7 @@ export function ForgotPasswordView({ onSubmit, isLoading = false, error, success
22
22
  }
23
23
  catch (err) {
24
24
  void normalizeError(err);
25
- showToast(err instanceof Error ? err.message : "Failed to send reset link", "error");
25
+ showToast("Failed to send reset link", "error");
26
26
  }
27
27
  };
28
28
  }
@@ -27,7 +27,7 @@ export function LoginForm({ onSubmit, onGoogleLogin, isLoading = false, error, l
27
27
  }
28
28
  catch (err) {
29
29
  void normalizeError(err);
30
- showToast(err instanceof Error ? err.message : "Sign in failed", "error");
30
+ showToast("Sign in failed", "error");
31
31
  }
32
32
  }, children: isLoading
33
33
  ? (labels.submittingLabel ?? "Signing in…")
@@ -39,7 +39,7 @@ export function RegisterForm({ onSubmit, isLoading = false, error, success, labe
39
39
  }
40
40
  catch (err) {
41
41
  void normalizeError(err);
42
- showToast(err instanceof Error ? err.message : "Registration failed", "error");
42
+ showToast("Registration failed", "error");
43
43
  }
44
44
  }, children: isLoading
45
45
  ? (labels.submittingLabel ?? "Creating account…")
@@ -30,7 +30,7 @@ export function ResetPasswordView({ oobCode, onSubmit, isLoading = false, error,
30
30
  }
31
31
  catch (err) {
32
32
  void normalizeError(err);
33
- showToast(err instanceof Error ? err.message : "Password reset failed", "error");
33
+ showToast("Password reset failed", "error");
34
34
  }
35
35
  };
36
36
  }
@@ -315,6 +315,9 @@ export function useRegister(options) {
315
315
  // fail registration — the user can re-request verification later.
316
316
  try {
317
317
  await authProvider.sendEmailVerification();
318
+ // Non-fatal by design: the account already exists, and the user can
319
+ // re-request verification from Settings at any time. Firebase surfaces
320
+ // its own send failures; failing registration here would be worse.
318
321
  }
319
322
  catch (err) {
320
323
  void normalizeError(err);
@@ -72,10 +72,12 @@ class BlogRepository extends BaseRepository {
72
72
  .collection(this.collection)
73
73
  .doc(id)
74
74
  .update({ [BLOG_POST_FIELDS.VIEWS]: increment(1) });
75
+ // Fire-and-forget analytics: a lost view increment changes nothing that
76
+ // renders and cannot usefully be retried, so it must never fail the blog
77
+ // post read that triggered it — the count is approximate by design.
75
78
  }
76
79
  catch (_err) {
77
80
  void normalizeError(_err);
78
- // Fire-and-forget by design.
79
81
  }
80
82
  }
81
83
  async findRelated(category, excludeId, limit = 3) {
@@ -14,6 +14,7 @@ import { getDefaultCurrency } from "../../../core/baseline-resolver";
14
14
  import { CART_LANE, activeLane, canAddNewItems, } from "../../../_internal/shared/checkout/lanes";
15
15
  import { CART_MAX_ITEMS } from "../../../constants/limits";
16
16
  import { getExpandedDecrements, validateCartItemStock, } from "../../../_internal/server/features/checkout/bundle-expansion";
17
+ import { safeRead } from "../../../errors/safe-read";
17
18
  /**
18
19
  * Product documents don't always carry a denormalized `storeName` — the
19
20
  * create routes only set it when the caller explicitly passes one, so
@@ -26,7 +27,9 @@ import { getExpandedDecrements, validateCartItemStock, } from "../../../_interna
26
27
  async function resolveStoreName(storeId, candidate) {
27
28
  if (candidate)
28
29
  return candidate;
29
- const store = await storeRepository.findById(storeId).catch(() => null);
30
+ const store = await safeRead(() => storeRepository.findById(storeId), {
31
+ route: "/cart", key: "stores.findById", fallback: null,
32
+ });
30
33
  return store?.storeName ?? storeId;
31
34
  }
32
35
  /**
@@ -102,7 +105,9 @@ export async function addGroupLineToCart(userId, input) {
102
105
  const cart = await assertCanAddNewItems(userId);
103
106
  // One batched read for the whole selection — capped at 20 above, so well
104
107
  // inside the per-request Firestore budget.
105
- const docs = await Promise.all(members.map((m) => productRepository.findById(m.productId).catch(() => null)));
108
+ const docs = await Promise.all(members.map((m) => safeRead(() => productRepository.findById(m.productId), {
109
+ route: "/cart", key: "products.findById", fallback: null,
110
+ })));
106
111
  const productById = new Map(members.map((m, i) => [m.productId, docs[i]]));
107
112
  const { getListingRule } = await import("../../../_internal/shared/checkout/rules");
108
113
  const { ProductStatusValues } = await import("../../products/schemas/firestore");
@@ -209,14 +214,18 @@ export async function addGroupLineToCart(userId, input) {
209
214
  /** Is this product listed on that `groupedListings` document? */
210
215
  async function isGroupedListingMember(groupId, productId) {
211
216
  const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository");
212
- const group = await groupedListingsRepository.findById(groupId).catch(() => null);
217
+ const group = await safeRead(() => groupedListingsRepository.findById(groupId), {
218
+ route: "/cart", key: "groupedListings.findById", fallback: null,
219
+ });
213
220
  return Boolean(group?.productIds?.includes(productId));
214
221
  }
215
222
  /** Display name for the line. Falls back to the parent product's group title. */
216
223
  async function resolveGroupTitle(groupId, groupSource, sample) {
217
224
  if (groupSource === "grouped-listing") {
218
225
  const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository");
219
- const group = await groupedListingsRepository.findById(groupId).catch(() => null);
226
+ const group = await safeRead(() => groupedListingsRepository.findById(groupId), {
227
+ route: "/cart", key: "groupedListings.title", fallback: null,
228
+ });
220
229
  if (group?.title)
221
230
  return group.title;
222
231
  }
@@ -72,10 +72,10 @@ function write(key, items) {
72
72
  return;
73
73
  try {
74
74
  localStorage.setItem(key, JSON.stringify(items));
75
+ // Quota exceeded or storage blocked. Losing pending ops costs a retry, never data: the cart is canonical in Firestore.
75
76
  }
76
77
  catch (_err) {
77
78
  void normalizeError(_err);
78
- // storage quota exceeded — best effort
79
79
  }
80
80
  }
81
81
  function writeCartOps(items) {
@@ -16,24 +16,25 @@ import { listingTabCounts, } from "../../../_internal/server/features/products/l
16
16
  import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
17
17
  import { enabledCategoryTypes, enabledListingTypes } from "../../../_internal/shared/listing-types/feature-flags";
18
18
  import { siteSettingsRepository } from "../../../repositories";
19
+ import { safeRead } from "../../../errors/safe-read";
19
20
  const __O = {
20
21
  hidden: "overflow-hidden",
21
22
  };
22
23
  export async function BrandDetailPageView({ slug, initialBrand }) {
24
+ // The brand IS this page's subject — a read failure must surface rather than
25
+ // render an anonymous, empty brand page. `undefined` still means "no such
26
+ // brand slug".
23
27
  const brand = (initialBrand ?? (await categoriesRepository
24
- .getCategoryBySlug(slug)
25
- .catch(() => undefined)));
28
+ .getCategoryBySlug(slug)) ?? undefined);
26
29
  const brandName = brand?.name;
27
30
  const [productsResult, tabCounts, allBundles, activeBrands, groupedListings, settings] = await Promise.all([
28
31
  brandName
29
- ? productRepository
30
- .list({
32
+ ? safeRead(() => productRepository.list({
31
33
  filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("brand", SIEVE_OP.EQ, brandName), sieveFilter("listingType", SIEVE_OP.EQ, "standard")),
32
34
  sorts: sortBy("createdAt", "DESC"),
33
35
  page: 1,
34
36
  pageSize: 24,
35
- })
36
- .catch(() => null)
37
+ }), { route: "/brands/[slug]", key: "brand.products", fallback: null })
37
38
  : Promise.resolve(null),
38
39
  // One count per tab, derived from CATEGORY_PAGE_TABS. Four of the nine tabs
39
40
  // (classifieds, digital codes, live, art) had no count at all and could
@@ -47,16 +48,31 @@ export async function BrandDetailPageView({ slug, initialBrand }) {
47
48
  // We pull all bundle categories and filter client-side by brandSlug —
48
49
  // no dedicated repository query needed at this catalog's bundle volume.
49
50
  brand?.slug
50
- ? categoriesRepository
51
- .listByType("bundle", { activeOnly: true, limit: 50 })
52
- .catch(() => [])
51
+ ? safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit: 50 }), { route: "/brands/[slug]", key: "brand.bundles", fallback: [] })
53
52
  : Promise.resolve([]),
54
53
  // Related brands — every other active brand row, excluding this one.
55
- categoriesRepository.findActiveBrands().catch(() => []),
56
- brand?.slug ? getGroupsForBrand(brand.slug).catch(() => []) : Promise.resolve([]),
54
+ // `as unknown` is load-bearing only because CategoryDocument.createdAt is a
55
+ // Date while CategoryItem.createdAt is a string — a pre-existing mismatch
56
+ // the old `.catch(() => [])` union happened to paper over.
57
+ safeRead(() => categoriesRepository.findActiveBrands(), {
58
+ route: "/brands/[slug]",
59
+ key: "brand.relatedBrands",
60
+ fallback: [],
61
+ }),
62
+ brand?.slug
63
+ ? safeRead(() => getGroupsForBrand(brand.slug), {
64
+ route: "/brands/[slug]",
65
+ key: "brand.groupedListings",
66
+ fallback: [],
67
+ })
68
+ : Promise.resolve([]),
57
69
  // Feature flags — accepted by BrandDetailTabs all along but never passed,
58
70
  // so a disabled listing type still showed its tab here.
59
- siteSettingsRepository.findById("global").catch(() => null),
71
+ safeRead(() => siteSettingsRepository.findById("global"), {
72
+ route: "/brands/[slug]",
73
+ key: "brand.siteSettings",
74
+ fallback: null,
75
+ }),
60
76
  ]);
61
77
  const brandBundles = brand?.slug
62
78
  ? allBundles.filter((b) => b.brandSlug === brand.slug)
@@ -15,14 +15,17 @@ import { listingTabCounts, } from "../../../_internal/server/features/products/l
15
15
  import { CATEGORY_PAGE_TABS } from "../../products/constants/listing-tabs";
16
16
  import { enabledCategoryTypes, enabledListingTypes } from "../../../_internal/shared/listing-types/feature-flags";
17
17
  import { siteSettingsRepository } from "../../../repositories";
18
+ import { safeRead } from "../../../errors/safe-read";
18
19
  const __O = {
19
20
  hidden: "overflow-hidden",
20
21
  xAuto: "overflow-x-auto",
21
22
  };
22
23
  export async function CategoryDetailPageView({ slug }) {
23
- const category = await categoriesRepository
24
- .getCategoryBySlug(slug)
25
- .catch(() => undefined);
24
+ // The category IS this page's subject — a read failure must surface rather
25
+ // than render an anonymous, empty category page. `undefined` still means
26
+ // "no such category slug".
27
+ const category = ((await categoriesRepository
28
+ .getCategoryBySlug(slug)) ?? undefined);
26
29
  // Products carry their FULL ancestor chain in `categorySlugs`, so matching on
27
30
  // this category's own id already returns the whole subtree — no descendant
28
31
  // expansion needed. Expanding it was also a latent outage: `categoriesIn` is
@@ -33,14 +36,12 @@ export async function CategoryDetailPageView({ slug }) {
33
36
  const categoriesIn = category?.id ? [category.id] : null;
34
37
  const [productsResult, tabCounts, bundlesResult, childCategories, rootSiblingCategories, groupedListings, settings] = await Promise.all([
35
38
  categoriesIn
36
- ? productRepository
37
- .list({
39
+ ? safeRead(() => productRepository.list({
38
40
  filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("listingType", SIEVE_OP.EQ, "standard")),
39
41
  sorts: sortBy("createdAt", "DESC"),
40
42
  page: 1,
41
43
  pageSize: 24,
42
- }, { categoriesIn })
43
- .catch(() => null)
44
+ }, { categoriesIn }), { route: "/categories/[slug]", key: "category.products", fallback: null })
44
45
  : Promise.resolve(null),
45
46
  // One count per tab, derived from CATEGORY_PAGE_TABS itself — so every
46
47
  // listing type is counted and a type with nothing in this category hides.
@@ -51,24 +52,40 @@ export async function CategoryDetailPageView({ slug }) {
51
52
  // SB-UNI-D — bundles fetched from the categories collection. We pull
52
53
  // all active bundle rows; the carousel filters by category affinity.
53
54
  categoriesIn
54
- ? categoriesRepository
55
- .listByType("bundle", { activeOnly: true, limit: 50 })
56
- .catch(() => [])
55
+ ? safeRead(() => categoriesRepository.listByType("bundle", { activeOnly: true, limit: 50 }), { route: "/categories/[slug]", key: "category.bundles", fallback: [] })
57
56
  : Promise.resolve([]),
58
57
  category?.id
59
- ? categoriesRepository.getChildren(category.id).catch(() => [])
58
+ ? safeRead(() => categoriesRepository.getChildren(category.id), {
59
+ route: "/categories/[slug]",
60
+ key: "category.children",
61
+ fallback: [],
62
+ })
60
63
  : Promise.resolve([]),
61
64
  // Related categories — every other category sharing this category's root
62
65
  // (siblings + cousins across the tree, up to the tier-0/1/2 depth the
63
66
  // catalog actually uses), not just direct children.
64
67
  category?.rootId
65
- ? categoriesRepository.getCategoriesByRootId(category.rootId).catch(() => [])
68
+ ? safeRead(() => categoriesRepository.getCategoriesByRootId(category.rootId), {
69
+ route: "/categories/[slug]",
70
+ key: "category.rootSiblings",
71
+ fallback: [],
72
+ })
73
+ : Promise.resolve([]),
74
+ category?.slug
75
+ ? safeRead(() => getGroupsForCategory(category.slug), {
76
+ route: "/categories/[slug]",
77
+ key: "category.groupedListings",
78
+ fallback: [],
79
+ })
66
80
  : Promise.resolve([]),
67
- category?.slug ? getGroupsForCategory(category.slug).catch(() => []) : Promise.resolve([]),
68
81
  // Listing/category-type feature flags. This page accepted the props all
69
82
  // along but never passed them, so an admin disabling a listing type still
70
83
  // saw its tab here while the store page correctly hid it.
71
- siteSettingsRepository.findById("global").catch(() => null),
84
+ safeRead(() => siteSettingsRepository.findById("global"), {
85
+ route: "/categories/[slug]",
86
+ key: "category.siteSettings",
87
+ fallback: null,
88
+ }),
72
89
  ]);
73
90
  const relatedCategories = rootSiblingCategories.filter((c) => c.id !== category?.id && (!c.categoryType || c.categoryType === "category"));
74
91
  // Stores tab — stores whose storeCategory is this category or ANY descendant.
@@ -82,18 +99,18 @@ export async function CategoryDetailPageView({ slug }) {
82
99
  // which caps at 30 values, hence the chunking.
83
100
  const storeCategorySlugs = [
84
101
  slug,
85
- ...(await categoriesRepository
86
- .getDescendantIds(category?.id ?? "")
87
- .catch(() => [])),
102
+ ...(await safeRead(() => categoriesRepository.getDescendantIds(category?.id ?? ""), {
103
+ route: "/categories/[slug]",
104
+ key: "category.descendantIds",
105
+ fallback: [],
106
+ })),
88
107
  ].filter(Boolean);
89
108
  const SLUG_CHUNK = 30;
90
109
  const slugChunks = [];
91
110
  for (let i = 0; i < storeCategorySlugs.length; i += SLUG_CHUNK) {
92
111
  slugChunks.push(storeCategorySlugs.slice(i, i + SLUG_CHUNK));
93
112
  }
94
- const storeResults = await Promise.all(slugChunks.map((chunk) => storeRepository
95
- .listStores({ filters: sieveFilter("storeCategory", SIEVE_OP.EQ, chunk.join("|")), page: 1, pageSize: 50 }, true)
96
- .catch(() => null)));
113
+ const storeResults = await Promise.all(slugChunks.map((chunk) => safeRead(() => storeRepository.listStores({ filters: sieveFilter("storeCategory", SIEVE_OP.EQ, chunk.join("|")), page: 1, pageSize: 50 }, true), { route: "/categories/[slug]", key: "category.stores", fallback: null })));
97
114
  const seen = new Set();
98
115
  const categoryStores = storeResults
99
116
  .flatMap((r) => r?.items ?? [])