@mohasinac/appkit 4.30.17 → 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 (163) 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/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
  46. package/dist/client/api/useApiQuery.js +17 -6
  47. package/dist/client.d.ts +1 -0
  48. package/dist/client.js +3 -0
  49. package/dist/contracts/client-realtime.d.ts +6 -0
  50. package/dist/contracts/realtime-session.d.ts +12 -12
  51. package/dist/contracts/realtime-session.js +72 -77
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +1 -1
  54. package/dist/features/about/components/DeveloperView.js +7 -2
  55. package/dist/features/about/components/PolicyPageView.js +9 -1
  56. package/dist/features/about/components/PublicProfileView.js +23 -5
  57. package/dist/features/account/components/AddressForm.js +1 -1
  58. package/dist/features/account/components/NotificationBell.js +4 -3
  59. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  60. package/dist/features/account/components/UserOffersPanel.js +1 -1
  61. package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
  62. package/dist/features/admin/components/AdminMediaView.js +3 -3
  63. package/dist/features/analytics/types.d.ts +1 -1
  64. package/dist/features/analytics/types.js +13 -0
  65. package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
  66. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
  67. package/dist/features/auth/actions/profile-actions.js +4 -3
  68. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  69. package/dist/features/auth/components/LoginForm.js +1 -1
  70. package/dist/features/auth/components/RegisterForm.js +1 -1
  71. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  72. package/dist/features/auth/hooks/useAuth.js +3 -0
  73. package/dist/features/blog/repository/blog.repository.js +3 -1
  74. package/dist/features/cart/actions/cart-actions.js +13 -4
  75. package/dist/features/cart/utils/pending-ops.js +1 -1
  76. package/dist/features/categories/components/BrandDetailPageView.js +28 -12
  77. package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
  78. package/dist/features/categories/repository/categories.repository.js +3 -1
  79. package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
  80. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
  82. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
  83. package/dist/features/events/components/EventBanner.js +5 -2
  84. package/dist/features/events/components/EventOfferCard.js +3 -1
  85. package/dist/features/events/components/EventPollWidget.js +1 -1
  86. package/dist/features/events/components/SpinWheelView.js +3 -2
  87. package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
  88. package/dist/features/history/hooks/useHistory.js +29 -27
  89. package/dist/features/homepage/components/AdSlot.js +3 -1
  90. package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
  91. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
  92. package/dist/features/homepage/components/SocialFeedSection.js +4 -1
  93. package/dist/features/homepage/lib/live-stats.js +8 -1
  94. package/dist/features/homepage/repository/carousel.repository.js +2 -1
  95. package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
  96. package/dist/features/media/finalize.js +3 -1
  97. package/dist/features/media/modals/ImageEditor.js +1 -1
  98. package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
  99. package/dist/features/media/modals/VideoTrimModal.js +5 -2
  100. package/dist/features/media/upload/MediaUploadField.js +2 -2
  101. package/dist/features/messages/hooks/useConversation.js +18 -4
  102. package/dist/features/messages/hooks/useConversations.js +25 -8
  103. package/dist/features/orders/components/RefundRequestView.js +5 -2
  104. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
  105. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +8 -2
  109. package/dist/features/products/components/ShareButton.js +3 -1
  110. package/dist/features/products/repository/loadProductFeatures.js +10 -2
  111. package/dist/features/products/repository/products.repository.js +2 -1
  112. package/dist/features/promotions/components/CouponCard.js +3 -1
  113. package/dist/features/reviews/actions/review-actions.js +8 -1
  114. package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
  115. package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
  116. package/dist/features/scams/actions/scam-actions.d.ts +9 -1
  117. package/dist/features/scams/actions/scam-actions.js +52 -12
  118. package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
  119. package/dist/features/scams/components/SellerTrustBadge.js +13 -0
  120. package/dist/features/scams/repository/scammer.repository.js +2 -1
  121. package/dist/features/seller/actions/offer-actions.js +6 -1
  122. package/dist/features/seller/components/FulfillmentView.js +5 -2
  123. package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
  124. package/dist/features/seller/components/PrintCenterView.js +3 -1
  125. package/dist/features/seller/components/SellerBidsView.js +1 -1
  126. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  127. package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
  128. package/dist/features/seller/components/SellerOrdersView.js +3 -3
  129. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  130. package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
  131. package/dist/features/seller/components/SellerProductShell.js +15 -5
  132. package/dist/features/seller/components/SellerProductsView.js +24 -10
  133. package/dist/features/seller/components/SellerReviewsView.js +47 -16
  134. package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
  135. package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
  136. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  137. package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
  138. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
  139. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  140. package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.js +5 -2
  143. package/dist/next/api/routeHandler.js +3 -1
  144. package/dist/providers/auth-firebase/session.js +3 -1
  145. package/dist/providers/db-firebase/admin-app-lite.js +8 -8
  146. package/dist/providers/db-firebase/admin.js +12 -12
  147. package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
  148. package/dist/providers/db-firebase/browser-stub.js +6 -11
  149. package/dist/providers/db-firebase/index.d.ts +0 -1
  150. package/dist/providers/db-firebase/index.js +0 -1
  151. package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
  152. package/dist/providers/db-firebase/rtdb-paths.js +15 -6
  153. package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
  154. package/dist/providers/db-firebase/rtdb-url.js +50 -0
  155. package/dist/providers/firebase-client/realtime.d.ts +20 -0
  156. package/dist/providers/firebase-client/realtime.js +34 -1
  157. package/dist/providers.d.ts +1 -1
  158. package/dist/providers.js +1 -1
  159. package/dist/react/hooks/use-action-dispatch.js +8 -2
  160. package/dist/react/hooks/useFormStatePreservation.js +3 -1
  161. package/dist/react/hooks/useRealtimeEvent.js +20 -9
  162. package/dist/ui/components/toolbar-collapse.js +1 -2
  163. package/package.json +1 -1
@@ -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 ?? [])
@@ -480,10 +480,12 @@ export class CategoriesRepository extends BaseRepository {
480
480
  .collection(this.collection)
481
481
  .doc(categoryId)
482
482
  .update({ [CATEGORY_FIELDS.VIEW_COUNT]: increment(1) });
483
+ // Fire-and-forget analytics: a lost view increment changes nothing that
484
+ // renders, so it must never break the category read path it rides along
485
+ // with — the count is approximate by design.
483
486
  }
484
487
  catch (_err) {
485
488
  void normalizeError(_err);
486
- // View analytics must not break category read path.
487
489
  }
488
490
  }
489
491
  generateSublistingId(name) {
@@ -9,6 +9,7 @@
9
9
  import { timingSafeEqual } from "crypto";
10
10
  import { ValidationError } from "../../../errors";
11
11
  import { normalizeError } from "../../../errors/normalize";
12
+ import { safeRead } from "../../../errors/safe-read";
12
13
  import { serverLogger } from "../../../monitoring";
13
14
  import { sendEmail } from "../../contact/email";
14
15
  import { getAdminDb } from "../../../providers/db-firebase";
@@ -47,7 +48,15 @@ export async function sendCheckoutValueOtp(userId, userEmail, channel = "email",
47
48
  if (!addressId) {
48
49
  throw new ValidationError("Select a delivery address before sending the code via WhatsApp.");
49
50
  }
50
- const settings = await siteSettingsRepository.getSingleton().catch(() => null);
51
+ // A failed settings read makes the WhatsApp channel look deliberately
52
+ // switched off ("send the code by email instead") — a defensible fallback,
53
+ // but one an operator has to be able to see, since it is indistinguishable
54
+ // from an admin having disabled it.
55
+ const settings = await safeRead(() => siteSettingsRepository.getSingleton(), {
56
+ route: "checkout/value-otp",
57
+ key: "checkoutValueOtp.siteSettings",
58
+ fallback: null,
59
+ });
51
60
  const creds = settings?.credentials ?? {};
52
61
  const whatsappOtpEnabled = settings?.notificationChannels?.whatsapp?.otpEnabled === true;
53
62
  const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
@@ -36,7 +36,7 @@ export function ClassifiedContactSellerPanel({ productId, onContactSeller }) {
36
36
  }
37
37
  catch (e) {
38
38
  void normalizeError(e);
39
- setError(e instanceof Error ? e.message : "Something went wrong");
39
+ setError("Something went wrong");
40
40
  }
41
41
  finally {
42
42
  setPending(false);
@@ -22,6 +22,7 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
22
22
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
23
23
  import { ClassifiedContactSellerPanel } from "./ClassifiedContactSellerPanel";
24
24
  import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
25
+ import { safeRead } from "../../../errors/safe-read";
25
26
  import { canMakeOffer } from "../../../_internal/shared/listing-types/capabilities";
26
27
  import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/offers/config";
27
28
  import { ListingBottomActions } from "../../products/components/ListingBottomActions";
@@ -34,7 +35,10 @@ function toDescriptionHtml(raw) {
34
35
  export async function ClassifiedDetailPageView({ slug, initialProduct, renderOfferAction }) {
35
36
  const product = initialProduct !== undefined
36
37
  ? (initialProduct ?? undefined)
37
- : await getClassifiedForDetail(slug).catch(() => undefined);
38
+ // The listing IS this page's subject — a read failure must surface rather
39
+ // than become the same `undefined` a removed listing produces, which the
40
+ // "Not Found" branch below already handles.
41
+ : await getClassifiedForDetail(slug);
38
42
  if (!product) {
39
43
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Classified Listing Not Found" }), _jsx(Text, { color: "muted", children: "The classified listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLASSIFIED), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Classifieds" })] }) }) }) }));
40
44
  }
@@ -58,7 +62,11 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
58
62
  const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, siteSettings] = await Promise.all([
59
63
  computeRelatedItems(product),
60
64
  getGroupsWithItemsForProduct(product.id),
61
- getSiteSettingsGlobal().catch(() => null),
65
+ safeRead(() => getSiteSettingsGlobal(), {
66
+ route: "/classified/[slug]",
67
+ key: "classified.siteSettings",
68
+ fallback: null,
69
+ }),
62
70
  ]);
63
71
  // Classifieds are the one non-standard type where haggling is the norm, so
64
72
  // `canMakeOffer` is true for them — but the seller still opts in per listing
@@ -30,7 +30,10 @@ function toDescriptionHtml(raw) {
30
30
  export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPrimaryActions }) {
31
31
  const product = initialProduct !== undefined
32
32
  ? (initialProduct ?? undefined)
33
- : await getDigitalCodeForDetail(slug).catch(() => undefined);
33
+ // The listing IS this page's subject — a read failure must surface rather
34
+ // than become the same `undefined` a removed listing produces, which the
35
+ // "Not Found" branch below already handles.
36
+ : await getDigitalCodeForDetail(slug);
34
37
  if (!product) {
35
38
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Digital Code Listing Not Found" }), _jsx(Text, { color: "muted", children: "The digital code listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Digital Codes" })] }) }) }) }));
36
39
  }
@@ -13,10 +13,12 @@ export function EventBanner({ storageKey = "dismissed_event_banners", className
13
13
  const stored = sessionStorage.getItem(storageKey);
14
14
  if (stored)
15
15
  setDismissedIds(JSON.parse(stored));
16
+ // sessionStorage is unavailable in private mode and over quota in some
17
+ // embedded browsers. Losing the dismissal list only means the banner
18
+ // reappears — nothing an operator can act on, and nothing the user loses.
16
19
  }
17
20
  catch (_err) {
18
21
  void normalizeError(_err);
19
- // ignore session storage failures
20
22
  }
21
23
  setMounted(true);
22
24
  }, [storageKey]);
@@ -38,10 +40,11 @@ export function EventBanner({ storageKey = "dismissed_event_banners", className
38
40
  setDismissedIds(next);
39
41
  try {
40
42
  sessionStorage.setItem(storageKey, JSON.stringify(next));
43
+ // Same as the read above: the banner is already hidden in local state, so
44
+ // a failed persist only means it returns on the next page load.
41
45
  }
42
46
  catch (_err) {
43
47
  void normalizeError(_err);
44
- // ignore
45
48
  }
46
49
  };
47
50
  return (_jsxs(Section, { tone: "accent-banner", className: [
@@ -24,10 +24,12 @@ export function EventOfferCard({ couponCode, offerDescription, title = "Exclusiv
24
24
  await navigator.clipboard.writeText(couponCode);
25
25
  setCopied(true);
26
26
  setTimeout(() => setCopied(false), 2000);
27
+ // Clipboard access is denied outright on insecure origins and by user
28
+ // permission. The button simply never flips to "Copied", and the code is
29
+ // still visible on screen to select by hand.
27
30
  }
28
31
  catch (_err) {
29
32
  void normalizeError(_err);
30
- /* clipboard unavailable — leave button as-is */
31
33
  }
32
34
  };
33
35
  return (_jsx(Div, { className: `${__P.p6}`, rounded: "2xl", border: "default", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, weight: "semibold", size: "2xl", children: title }), offerDescription ? (_jsx(Text, { size: "sm", color: "muted", children: offerDescription })) : null] }), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", gap: "3", rounded: "lg", children: [_jsx(Code, { className: "flex-1", weight: "bold", size: "base", color: "primary", children: couponCode }), _jsx(Button, { type: "button", variant: "outline", onClick: copyCode, children: copied ? "Copied!" : "Copy code" })] }), _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "primary", children: "Apply at checkout" }) }) })] }) }));
@@ -53,7 +53,7 @@ export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries
53
53
  }
54
54
  catch (err) {
55
55
  void normalizeError(err);
56
- setError(err instanceof Error ? err.message : "Something went wrong");
56
+ setError("Something went wrong");
57
57
  }
58
58
  finally {
59
59
  setIsLoading(false);
@@ -4,6 +4,7 @@ import { useCallback, useMemo, useState } from "react";
4
4
  import { Button, ClaimCouponButton, Div, Heading, LoginRequiredModal, Row, Span, Stack, Text, useToast } from "../../../ui";
5
5
  import { isAuthError } from "../../../utils/auth-error";
6
6
  import { normalizeError } from "../../../errors/normalize";
7
+ import { toUserMessage } from "../../../errors/error-display-map";
7
8
  const CLS_WHEEL = "relative mx-auto aspect-square w-64 overflow-hidden rounded-full border-4 border-warning bg-[image:var(--appkit-gradient-promotion)]";
8
9
  // audit-content-alignment-ok: self-contained circular spin-wheel game widget, all text centered under the wheel by design
9
10
  const CLS_PRIZE_BOX = "rounded-xl border border-success bg-success-surface dark:border-success p-[var(--appkit-space-4)] text-center";
@@ -55,14 +56,14 @@ export function SpinWheelView({ eventId, prizes, alreadyUsed, initialPrizeId, in
55
56
  }, ANIMATION_MS);
56
57
  }
57
58
  catch (err) {
58
- void normalizeError(err);
59
+ const e = normalizeError(err);
59
60
  setSpinning(false);
60
61
  if (isAuthError(err)) {
61
62
  setShowLoginModal(true);
62
63
  }
63
64
  else {
64
65
  setError(l.errorFallback);
65
- showToast(err instanceof Error ? err.message : l.errorFallback, "error");
66
+ showToast(toUserMessage(e.code, undefined, { fallback: l.errorFallback }), "error");
66
67
  }
67
68
  }
68
69
  }, [disabled, eventId, l.errorFallback, onSpin, showToast]);
@@ -46,6 +46,7 @@ import { apiClient } from "../../../http";
46
46
  import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
47
47
  import { ROUTES } from "../../../next/routing/route-map";
48
48
  import { normalizeError } from "../../../errors/normalize";
49
+ import { toUserMessage } from "../../../errors/error-display-map";
49
50
  import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
50
51
  import { groupedListingFormSchema, } from "../schemas/grouped-listing-form";
51
52
  const EMPTY = {
@@ -173,7 +174,7 @@ export function GroupedListingEditorView({ scope, groupId, fixedStoreId, onSaved
173
174
  }
174
175
  catch (err) {
175
176
  const e = normalizeError(err);
176
- showToast(e.message || "Failed to save the group.", "error");
177
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to save the group." }), "error");
177
178
  }
178
179
  finally {
179
180
  setSaving(false);
@@ -38,35 +38,28 @@ async function postAuthTrack(args) {
38
38
  headers: { "Content-Type": "application/json" },
39
39
  body: JSON.stringify(args),
40
40
  });
41
+ // Recently-viewed tracking is a side effect of browsing, fired from a
42
+ // debounced timer with no UI attached. A lost entry costs the user nothing
43
+ // and there is no caller in a position to retry or report it.
41
44
  }
42
45
  catch (_err) {
43
46
  void normalizeError(_err);
44
- // best-effort tracking
45
47
  }
46
48
  }
49
+ // Both deletes below are user-initiated and deliberately let failures throw:
50
+ // swallowing them made `remove`/`clear` report success for a DELETE that never
51
+ // landed, and the row would silently return on the next load.
47
52
  async function deleteAuthItem(productId) {
48
- try {
49
- await fetch(ACCOUNT_ENDPOINTS.HISTORY_ITEM(productId), {
50
- method: "DELETE",
51
- credentials: "include",
52
- });
53
- }
54
- catch (_err) {
55
- void normalizeError(_err);
56
- // ignore
57
- }
53
+ await fetch(ACCOUNT_ENDPOINTS.HISTORY_ITEM(productId), {
54
+ method: "DELETE",
55
+ credentials: "include",
56
+ });
58
57
  }
59
58
  async function deleteAuthAll() {
60
- try {
61
- await fetch(ACCOUNT_ENDPOINTS.HISTORY, {
62
- method: "DELETE",
63
- credentials: "include",
64
- });
65
- }
66
- catch (_err) {
67
- void normalizeError(_err);
68
- // ignore
69
- }
59
+ await fetch(ACCOUNT_ENDPOINTS.HISTORY, {
60
+ method: "DELETE",
61
+ credentials: "include",
62
+ });
70
63
  }
71
64
  export function useHistory(userId) {
72
65
  const isAuth = !!userId;
@@ -81,6 +74,9 @@ export function useHistory(userId) {
81
74
  else {
82
75
  setItems(getGuestHistory());
83
76
  }
77
+ // fetchAuthHistory already degrades to [] on its own, and getGuestHistory
78
+ // is a localStorage read that fails only in private mode — either way the
79
+ // list stays as it was, which is the correct visible outcome here.
84
80
  }
85
81
  catch (err) {
86
82
  void normalizeError(err);
@@ -131,14 +127,20 @@ export function useHistory(userId) {
131
127
  trackTimers.current.set(key, timer);
132
128
  }, [isAuth]);
133
129
  const remove = useCallback(async (productId) => {
134
- if (isAuth) {
135
- await deleteAuthItem(productId);
136
- await loadItems();
130
+ try {
131
+ if (isAuth) {
132
+ await deleteAuthItem(productId);
133
+ await loadItems();
134
+ }
135
+ else {
136
+ setItems(removeGuestHistoryItem(productId));
137
+ }
137
138
  }
138
- else {
139
- setItems(removeGuestHistoryItem(productId));
139
+ catch (_err) {
140
+ void normalizeError(_err);
141
+ showToast("Failed to remove this item from history.", "error");
140
142
  }
141
- }, [isAuth, loadItems]);
143
+ }, [isAuth, loadItems, showToast]);
142
144
  const clear = useCallback(async () => {
143
145
  try {
144
146
  if (isAuth) {
@@ -16,10 +16,12 @@ function AdsenseAd({ config }) {
16
16
  return;
17
17
  try {
18
18
  (window.adsbygoogle = window.adsbygoogle || []).push({});
19
+ // adsbygoogle is injected by a third-party script that ad blockers and
20
+ // consent walls routinely prevent from loading. The <ins> below simply
21
+ // stays unfilled — there is no failure the site can act on.
19
22
  }
20
23
  catch (_err) {
21
24
  void normalizeError(_err);
22
- // AdSense not loaded yet — no-op
23
25
  }
24
26
  }, []);
25
27
  return (_jsx(Div, { ref: ref, children: _jsx("ins", { className: "adsbygoogle block", "data-ad-client": config.adsenseClient ?? "", "data-ad-slot": config.adsenseSlot ?? "", "data-ad-format": config.adsenseFormat ?? "auto", "data-full-width-responsive": "true" }) }));
@@ -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 { Anchor, Div, GRID_MAP, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
4
5
  import { MediaImage } from "../../media/MediaImage";
5
6
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
@@ -60,8 +61,13 @@ export async function GoogleReviewsSection(config) {
60
61
  totalRatings = result.totalRatings;
61
62
  }
62
63
  catch (_err) {
63
- void normalizeError(_err);
64
- // Fail silently — empty state shown
64
+ // The section renders its empty state below, which is indistinguishable
65
+ // from a place that genuinely has no reviews — and Google Places calls are
66
+ // billed, so a persistent failure is worth an operator seeing.
67
+ serverLogger.warn("homepage.googleReviews: Places fetch failed", {
68
+ placeId,
69
+ error: normalizeError(_err).message,
70
+ });
65
71
  }
66
72
  const gridClass = layout === "carousel"
67
73
  ? "flex gap-[var(--appkit-space-4)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"