@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
@@ -1,7 +1,11 @@
1
1
  import { normalizeError } from "../errors/normalize";
2
- import { getClientRealtimeProvider } from "./client-realtime";
2
+ import { getClientRealtimeProvider, } from "./client-realtime";
3
3
  /**
4
- * Shared, REFERENCE-COUNTED sign-in for the one client realtime app.
4
+ * PER-SCOPE, reference-counted sign-in for the client realtime channels.
5
+ *
6
+ * Each claim scope gets its own Firebase app (via `provider.forScope`), because
7
+ * Firebase Auth state is per-app and two channels legitimately need two
8
+ * different claim sets at the same time.
5
9
  *
6
10
  * WHY THIS EXISTS — two distinct bugs, one cause.
7
11
  *
@@ -21,98 +25,89 @@ import { getClientRealtimeProvider } from "./client-realtime";
21
25
  * bulk token (carrying `bulkJobId`), killing an in-flight job subscription.
22
26
  * Whoever finished last broke whoever was still working.
23
27
  *
24
- * The fix for (2) is not "sign out more carefully" — it is that **no individual
25
- * consumer may decide to sign out at all**. Each acquires a lease and releases
26
- * it; the session ends only when the last lease is gone.
28
+ * (2) needed BOTH halves, and only one was done at first:
29
+ * - sign-OUT: no consumer may decide to sign out. Each takes a lease; the
30
+ * session ends only when the last lease for that scope is gone.
31
+ * - sign-IN: ref-counting cannot help here, because nothing is being shared
32
+ * by agreement — a bulk job and a message thread need different claims
33
+ * simultaneously. The answer is separate apps, not coordination.
27
34
  *
28
- * Tokens differ per channel (different claims), so `acquire` takes a fetcher and
29
- * re-signs when the token it holds is for a different scope or has expired.
35
+ * 🛑 Subscribe through `lease.provider`, never `getClientRealtimeProvider()`.
36
+ * The global one is the root app; the lease's is the app your scope actually
37
+ * authenticated on.
30
38
  */
31
39
  /** Refresh a little early — a token that expires mid-subscription is a silent drop. */
32
40
  const EXPIRY_SKEW_MS = 60000;
33
- let leases = 0;
34
- let currentScope = null;
35
- let expiresAt = 0;
36
- let inFlight = null;
37
41
  /**
38
- * Ensure the shared realtime app is signed in for `scope`, and take a lease.
42
+ * One entry per claim scope, each on its OWN Firebase app.
39
43
  *
40
- * @param scope an identifier for the claim set (e.g. `"messages"`,
41
- * `"chat:<id>"`). Signing in again for the SAME scope while a
42
- * valid token is held is a no-op.
43
- * @param getToken fetches a fresh custom token. Called only when needed.
44
- * @returns a release function. **Always call it** — the session is only torn
45
- * down when every lease is released.
44
+ * 🛑 This used to be three module-level singletons — `currentScope`,
45
+ * `expiresAt`, `leases` — describing ONE shared app. That shape could not
46
+ * express the real situation: two channels legitimately need two different
47
+ * claim sets at the same time, and Firebase Auth state is per-app, so every
48
+ * sign-in replaced the previous one's claims. Ref-counting only stopped a
49
+ * finishing channel from signing the others OUT; it could not stop a starting
50
+ * channel from signing itself IN over them.
46
51
  */
52
+ const scopes = new Map();
47
53
  export async function acquireRealtimeSession(scope, getToken) {
48
- const lease = { scope };
49
- leases++;
50
- const needsSignIn = currentScope !== lease.scope || Date.now() > expiresAt - EXPIRY_SKEW_MS;
51
- if (needsSignIn) {
52
- // Collapse concurrent acquires — several hooks mount in the same tick and
53
- // would otherwise each fetch a token and race to sign in.
54
- inFlight ?? (inFlight = (async () => {
54
+ const root = getClientRealtimeProvider();
55
+ let state = scopes.get(scope);
56
+ if (!state) {
57
+ // `forScope` is optional on the contract: a provider that cannot isolate
58
+ // falls back to the shared app, which is the old behaviour rather than a
59
+ // crash.
60
+ state = {
61
+ leases: 0,
62
+ expiresAt: 0,
63
+ inFlight: null,
64
+ provider: root.forScope?.(scope) ?? root,
65
+ };
66
+ scopes.set(scope, state);
67
+ }
68
+ state.leases++;
69
+ if (Date.now() > state.expiresAt - EXPIRY_SKEW_MS) {
70
+ // Collapse concurrent acquires for the SAME scope — several hooks mount in
71
+ // one tick and would otherwise each fetch a token and race to sign in.
72
+ state.inFlight ?? (state.inFlight = (async () => {
55
73
  try {
56
74
  const token = await getToken();
57
- await getClientRealtimeProvider().signInWithToken(token.customToken);
58
- currentScope = lease.scope;
59
- expiresAt = token.expiresAt;
75
+ await state.provider.signInWithToken(token.customToken);
76
+ state.expiresAt = token.expiresAt;
60
77
  }
61
78
  finally {
62
- inFlight = null;
79
+ state.inFlight = null;
63
80
  }
64
81
  })());
65
- await inFlight;
82
+ await state.inFlight;
66
83
  }
67
84
  let released = false;
68
- return () => {
69
- if (released)
70
- return; // React StrictMode double-invokes cleanups
71
- released = true;
72
- leases = Math.max(0, leases - 1);
73
- if (leases > 0)
74
- return;
75
- currentScope = null;
76
- expiresAt = 0;
77
- getClientRealtimeProvider()
78
- .signOut()
79
- .catch((err) => {
80
- void normalizeError(err);
81
- });
82
- };
83
- }
84
- /**
85
- * Take a lease WITHOUT signing in — for a consumer that manages its own
86
- * sign-in but must still participate in the reference count.
87
- *
88
- * `useRealtimeEvent` is the case: its token carries a job-scoped `bulkJobId`
89
- * claim it receives from the caller, so it cannot use `acquireRealtimeSession`'s
90
- * fetcher. It still must not be able to sign the shared app out from under
91
- * another channel, which is precisely what its `cleanup()` used to do.
92
- */
93
- export function acquireExternalRealtimeLease() {
94
- leases++;
95
- // Its own sign-in has replaced whatever scope was active; forget the cached
96
- // one so the next acquire() re-authenticates rather than trusting a token
97
- // that is no longer installed.
98
- currentScope = null;
99
- expiresAt = 0;
100
- let released = false;
101
- return () => {
102
- if (released)
103
- return;
104
- released = true;
105
- leases = Math.max(0, leases - 1);
106
- if (leases > 0)
107
- return;
108
- getClientRealtimeProvider()
109
- .signOut()
110
- .catch((err) => {
111
- void normalizeError(err);
112
- });
85
+ return {
86
+ provider: state.provider,
87
+ release: () => {
88
+ if (released)
89
+ return; // React StrictMode double-invokes cleanups
90
+ released = true;
91
+ const cur = scopes.get(scope);
92
+ if (!cur)
93
+ return;
94
+ cur.leases = Math.max(0, cur.leases - 1);
95
+ if (cur.leases > 0)
96
+ return;
97
+ // Last holder of THIS scope. Signing out touches only this scope's app,
98
+ // so it cannot disturb any other channel.
99
+ cur.expiresAt = 0;
100
+ scopes.delete(scope);
101
+ cur.provider.signOut().catch((err) => {
102
+ void normalizeError(err);
103
+ });
104
+ },
113
105
  };
114
106
  }
115
- /** Test/diagnostic only. */
107
+ /** Test/diagnostic only — total leases across every scope. */
116
108
  export function activeRealtimeLeases() {
117
- return leases;
109
+ let total = 0;
110
+ for (const s of scopes.values())
111
+ total += s.leases;
112
+ return total;
118
113
  }
@@ -12,7 +12,7 @@ export { DatabaseError } from "./database-error";
12
12
  export { RazorpayUnreachableError } from "./razorpay-unreachable";
13
13
  export { handleApiError, logError, isAppError } from "./error-handler";
14
14
  export { mapToHttpError, HTTP_ERROR_CODES, type MappedError, type HttpErrorCode, } from "./error-mapping";
15
- export { ERROR_DISPLAY_MAP, getErrorDisplay, type ErrorDisplayEntry, } from "./error-display-map";
15
+ export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, type ErrorDisplayEntry, } from "./error-display-map";
16
16
  export { safeRead, installDegradedReadReporter, type DegradedReadReport, } from "./safe-read";
17
17
  export { normalizeError, getErrorMessage, isAuthError, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./normalize";
18
18
  export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./normalize";
@@ -17,7 +17,7 @@ export { RazorpayUnreachableError } from "./razorpay-unreachable";
17
17
  export { handleApiError, logError, isAppError } from "./error-handler";
18
18
  // Code→HTTP mapper (server) — and the i18n / inline-field map (client-safe)
19
19
  export { mapToHttpError, HTTP_ERROR_CODES, } from "./error-mapping";
20
- export { ERROR_DISPLAY_MAP, getErrorDisplay, } from "./error-display-map";
20
+ export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, } from "./error-display-map";
21
21
  // safeRead is server-only (uses the persisted log reporter)
22
22
  export { safeRead, installDegradedReadReporter, } from "./safe-read";
23
23
  // W3 — catch-clause normalizer. Every catch (e: unknown) funnels through
@@ -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) {