@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,7 +1,12 @@
1
1
  import { cache } from "react";
2
2
  import { orderRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  export const getOrderForDetail = cache(async (orderId) => {
4
- return (await orderRepository.findById(orderId).catch(() => undefined)) ?? null;
5
+ // No catch: this order IS the page. Swallowing a Firestore failure to null
6
+ // renders "order not found" for a datastore outage — the caller cannot tell
7
+ // the two apart, which is Root Cause #59 on the money path. A throw reaches
8
+ // the route/error boundary and is now recorded.
9
+ return (await orderRepository.findById(orderId)) ?? null;
5
10
  });
6
11
  export const getOrdersForBuyer = cache(async (userId, page = 1, pageSize = 20) => {
7
12
  return orderRepository.listForUser(userId, { page, pageSize }).catch(() => ({
@@ -14,5 +19,12 @@ export const getOrdersForBuyer = cache(async (userId, page = 1, pageSize = 20) =
14
19
  }));
15
20
  });
16
21
  export const getRecentOrdersForBuyer = cache(async (userId) => {
17
- return orderRepository.findRecentByUser(userId).catch(() => []);
22
+ // Optional: a recent-orders rail. Degrading to empty is right, but it must
23
+ // be VISIBLE — safeRead records a DEGRADED_READ row instead of returning a
24
+ // value indistinguishable from "no recent orders".
25
+ return safeRead(() => orderRepository.findRecentByUser(userId), {
26
+ route: "/user/orders",
27
+ key: "orders.findRecentByUser",
28
+ fallback: [],
29
+ });
18
30
  });
@@ -2,7 +2,7 @@ import { orderRepository } from "../../../../repositories";
2
2
  import { OrderNotFoundError, OrderCancelError, OrderOwnershipError, OrderReturnWindowError, } from "../../../shared/features/orders/errors";
3
3
  import { ORDER_CANCELLABLE_STATUSES, ORDER_RETURN_WINDOW_MS, } from "../../../shared/features/orders/config";
4
4
  export async function assertOrderOwnership(orderId, userId) {
5
- const order = await orderRepository.findById(orderId).catch(() => null);
5
+ const order = await orderRepository.findById(orderId);
6
6
  if (!order)
7
7
  throw new OrderNotFoundError(orderId);
8
8
  if (order.userId !== userId)
@@ -4,7 +4,7 @@ import { roundRupees } from "../../../../utils/number.formatter";
4
4
  import { PreOrderNotFoundError, PreOrderSoldOutError, PreOrderNotOpenError, } from "../../../shared/features/pre-orders/errors";
5
5
  /** Assert a pre-order exists, is published, and has remaining capacity. */
6
6
  export async function assertPreOrderAvailable(preOrderId, requestedQty = 1) {
7
- const product = await productRepository.findByIdOrSlug(preOrderId).catch(() => null);
7
+ const product = await productRepository.findByIdOrSlug(preOrderId);
8
8
  if (!product || !isPreOrderListing(product))
9
9
  throw new PreOrderNotFoundError(preOrderId);
10
10
  if (product.status !== "published")
@@ -9,7 +9,7 @@ export async function enterPrizeDrawAction(input) {
9
9
  const { prizeDrawId } = input;
10
10
  if (!prizeDrawId)
11
11
  throw new Error("prizeDrawId is required.");
12
- const product = await productRepository.findByIdOrSlug(prizeDrawId).catch(() => null);
12
+ const product = await productRepository.findByIdOrSlug(prizeDrawId);
13
13
  if (!product)
14
14
  throw new Error("Prize draw not found.");
15
15
  assertPrizeDrawOpen(product);
@@ -8,6 +8,7 @@ import { productRepository } from "../../../../repositories";
8
8
  import { reviewRepository } from "../../../../repositories";
9
9
  import { getAdminDb } from "../../../../providers/db-firebase";
10
10
  import { normalizeError } from "../../../../errors/normalize";
11
+ import { safeRead } from "../../../../errors/safe-read";
11
12
  import { serverLogger } from "../../../../monitoring/server-logger";
12
13
  import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
13
14
  import { PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
@@ -23,7 +24,7 @@ import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/confi
23
24
  // ---------------------------------------------------------------------------
24
25
  /** Fetch a single product by slug or id, deduped per request. */
25
26
  export const getProductForDetail = cache(async (slugOrId) => {
26
- const product = (await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined)) ?? null;
27
+ const product = (await productRepository.findByIdOrSlug(slugOrId)) ?? null;
27
28
  // Fire-and-forget viewCount increment — must not affect response time
28
29
  if (product)
29
30
  void productRepository.incrementViewCount(product.id);
@@ -31,7 +32,11 @@ export const getProductForDetail = cache(async (slugOrId) => {
31
32
  });
32
33
  /** Fetch the first page of approved reviews for a product, deduped per request. */
33
34
  export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
34
- return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
35
+ return safeRead(() => reviewRepository.findApprovedByProduct(productId, limit), {
36
+ route: "/products",
37
+ key: "products.getReviewsForProduct",
38
+ fallback: [],
39
+ });
35
40
  });
36
41
  /** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
37
42
  export function toReview(doc) {
@@ -12,7 +12,7 @@ const ALLOWED_STATUS_TRANSITIONS = {
12
12
  };
13
13
  /** Assert a product exists and belongs to the given seller. Throws on failure. */
14
14
  export async function assertProductOwnership(productId, sellerId) {
15
- const product = await productRepository.findById(productId).catch(() => null);
15
+ const product = await productRepository.findById(productId);
16
16
  if (!product)
17
17
  throw new ProductNotFoundError(productId);
18
18
  if (product.storeId !== sellerId)
@@ -33,7 +33,7 @@ export async function createCouponAction(input) {
33
33
  export async function updateCouponAction(couponId, input) {
34
34
  return wrapAction(async () => {
35
35
  await requireRoleUser(["admin", "seller"]);
36
- const coupon = await couponsRepository.findById(couponId).catch(() => null);
36
+ const coupon = await couponsRepository.findById(couponId);
37
37
  if (!coupon)
38
38
  throw new CouponNotFoundError(couponId);
39
39
  const parsed = updateCouponSchema.safeParse(input);
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
2
  import { couponsRepository } from "../../../../repositories";
3
3
  export const getCouponByCode = cache(async (code) => {
4
- return (await couponsRepository.getCouponByCode(code).catch(() => undefined)) ?? null;
4
+ return (await couponsRepository.getCouponByCode(code)) ?? null;
5
5
  });
@@ -2,7 +2,7 @@ import { couponsRepository } from "../../../../repositories";
2
2
  import { roundRupees } from "../../../../utils/number.formatter";
3
3
  import { CouponNotFoundError, CouponExpiredError, CouponUsageLimitError, CouponPerUserLimitError, CouponMinPurchaseError, CouponScopeError, } from "../../../shared/features/promotions/errors";
4
4
  export async function validateCoupon(input, userId) {
5
- const coupon = await couponsRepository.getCouponByCode(input.code).catch(() => null);
5
+ const coupon = await couponsRepository.getCouponByCode(input.code);
6
6
  if (!coupon)
7
7
  throw new CouponNotFoundError(input.code);
8
8
  if (!coupon.validity?.isActive)
@@ -1,4 +1,4 @@
1
- export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<never[] | import("../../../..").ReviewDocument[]>;
1
+ export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<import("../../../..").ReviewDocument[]>;
2
2
  export declare const getReviewsForStore: (storeId: string, page?: number, pageSize?: number) => Promise<import("../../../..").FirebaseSieveResult<import("../../../..").ReviewDocument> | {
3
3
  items: never[];
4
4
  total: number;
@@ -1,8 +1,13 @@
1
1
  import { cache } from "react";
2
2
  import { reviewRepository, orderRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/config";
4
5
  export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
5
- return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
6
+ return safeRead(() => reviewRepository.findApprovedByProduct(productId, limit), {
7
+ route: "/products",
8
+ key: "reviews.getReviewsForProduct",
9
+ fallback: [],
10
+ });
6
11
  });
7
12
  export const getReviewsForStore = cache(async (storeId, page = 1, pageSize = 20) => {
8
13
  return reviewRepository.listForStore(storeId, { page, pageSize }).catch(() => ({
@@ -3,5 +3,5 @@ import { reviewRepository } from "../../../../repositories";
3
3
  export const getReviewById = cache(async (id) => {
4
4
  if (!id)
5
5
  return null;
6
- return reviewRepository.findById(id).catch(() => null);
6
+ return reviewRepository.findById(id);
7
7
  });
@@ -1,5 +1,6 @@
1
1
  import { cache } from "react";
2
2
  import { productRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  import { loadProductFeaturesForStore } from "../../../../features/products/repository/loadProductFeatures";
4
5
  // Returns a React.cache()-wrapped fetch gated on one listing type.
5
6
  // The guard prevents cross-type URL collisions (e.g. /auctions/classified-slug returning wrong product).
@@ -7,7 +8,7 @@ export function makeGetListingForDetail(type) {
7
8
  return cache(async (slugOrId) => {
8
9
  if (!slugOrId)
9
10
  return null;
10
- const product = await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined);
11
+ const product = await productRepository.findByIdOrSlug(slugOrId);
11
12
  if (!product || product.listingType !== type)
12
13
  return null;
13
14
  return product;
@@ -15,7 +16,11 @@ export function makeGetListingForDetail(type) {
15
16
  }
16
17
  // Product-feature badges are keyed by store, not listing type.
17
18
  // Per-type data.ts files re-export this under a type-specific name.
18
- export const getProductFeaturesForStore = cache(async (storeId) => loadProductFeaturesForStore(storeId).catch(() => []));
19
+ export const getProductFeaturesForStore = cache(async (storeId) => safeRead(() => loadProductFeaturesForStore(storeId), {
20
+ route: "/products",
21
+ key: "products.getProductFeaturesForStore",
22
+ fallback: [],
23
+ }));
19
24
  // Returns a React.cache()-wrapped function for store SSR first-page data.
20
25
  // Export names in stores/data.ts stay identical so consumers need no changes.
21
26
  export function makeGetStoreListingsInitial(type, pageSize) {
@@ -5,7 +5,7 @@ import { STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, } from "../../../share
5
5
  import { makeGetStoreListingsInitial } from "../shared/listing-data-factory";
6
6
  /** Full store document by slug — deduped per request via React.cache(). */
7
7
  export const getStoreForDetail = cache(async (slug) => {
8
- return (await storeRepository.findBySlug(slug).catch(() => undefined)) ?? null;
8
+ return (await storeRepository.findBySlug(slug)) ?? null;
9
9
  });
10
10
  // Collapse of three near-identical 14-line functions via factory — export names unchanged.
11
11
  export const listStoreProductsInitial = makeGetStoreListingsInitial("standard", STORES_PRODUCTS_PAGE_SIZE);
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
2
  import { categoriesRepository } from "../../../../repositories";
3
3
  export const getSublistingCategoryForDetail = cache(async (slug) => {
4
- return categoriesRepository.findBySlugAndType(slug, "sublisting").catch(() => null);
4
+ return categoriesRepository.findBySlugAndType(slug, "sublisting");
5
5
  });
@@ -1,6 +1,7 @@
1
1
  import { bidRepository, cartRepository, productRepository, storeRepository, } from "../../../../repositories";
2
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
3
3
  import { normalizeError } from "../../../../errors/normalize";
4
+ import { safeRead } from "../../../../errors/safe-read";
4
5
  import { AUCTION_MESSAGES, BID_MESSAGES } from "../handlers/messages";
5
6
  import { ROUTES } from "../../../../next/routing/route-map";
6
7
  import { CART_LANE, LANE_CHECKOUT_WINDOW_MS } from "../../../shared/checkout/lanes";
@@ -94,7 +95,11 @@ async function settleAuction(ctx, product) {
94
95
  productRepository.updateStatusInBatch(batch, product.id, "archived");
95
96
  await batch.commit();
96
97
  const reserveStore = product.storeId
97
- ? await storeRepository.findById(product.storeId).catch(() => null)
98
+ ? await safeRead(() => storeRepository.findById(product.storeId), {
99
+ route: "job:auctionSettlement",
100
+ key: "auctionSettlement.reserveNotMet.store",
101
+ fallback: null,
102
+ })
98
103
  : null;
99
104
  if (reserveStore?.ownerId) {
100
105
  await sendNotification({
@@ -1,5 +1,3 @@
1
- import { normalizeError } from "../../../../errors/normalize";
2
- import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
3
1
  import { decryptPii } from "../../../../security/index";
4
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
5
3
  import { ORDER_MESSAGES } from "../handlers/messages";
@@ -58,19 +56,13 @@ export async function handleOrderStatusChange(input, ctx) {
58
56
  userEmail,
59
57
  orderWhatsappAddonPaid: after.whatsappNotifyAddon === true,
60
58
  });
61
- try {
62
- await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({
63
- type: config.type,
64
- title: config.title,
65
- message: messageText,
66
- timestamp: Date.now(),
67
- read: false,
68
- });
69
- }
70
- catch (rtdbError) {
71
- void normalizeError(rtdbError);
72
- ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
73
- }
59
+ // The RTDB mirror that used to live here has been removed. `sendNotification`
60
+ // above is the real path (in-app + email + WhatsApp); the extra
61
+ // `notifications/{uid}` push was a duplicate that NOTHING read — no client
62
+ // subscription to that node has ever existed — and nothing pruned, so it
63
+ // accumulated one node per order transition per user forever. Its rule's
64
+ // `.type` validator also only allowed `info|success|warning|error`, none of
65
+ // which are the values written here (`order_confirmed`, `order_shipped`, …).
74
66
  ctx.logger.info(`Order ${orderId} status → ${newStatus}`, { userId: after.userId });
75
67
  }
76
68
  catch (error) {
@@ -12,7 +12,13 @@
12
12
  import { orderRepository } from "../../../../repositories";
13
13
  import { analyticsRollupRepository } from "../../../../repositories";
14
14
  export async function runRevenueRollup(ctx) {
15
- const deliveredOrders = await orderRepository.findByStatus("delivered").catch(() => []);
15
+ // No `.catch(() => [])` here on purpose: an empty list is a legitimate
16
+ // outcome that writes `totalRevenue: 0`, so swallowing a query failure would
17
+ // overwrite yesterday's correct rollup with a confident zero that the
18
+ // dashboard has no way to distinguish from "no delivered orders". Let it
19
+ // throw — the job runner records the failure and the stale-but-correct
20
+ // singleton stays put.
21
+ const deliveredOrders = await orderRepository.findByStatus("delivered");
16
22
  const totalRevenue = deliveredOrders.reduce((sum, order) => sum + (Number(order.totalPrice ?? 0) || 0), 0);
17
23
  await analyticsRollupRepository.setDashboardRollup({
18
24
  totalRevenue,
@@ -4,7 +4,13 @@ import * as React from "react";
4
4
  import { useQuery, } from "@tanstack/react-query";
5
5
  import { useToast } from "../../ui/components/Toast";
6
6
  import { ApiError } from "./ApiError";
7
- import { getErrorDisplay } from "../../errors/error-display-map";
7
+ import { toUserMessage } from "../../errors/error-display-map";
8
+ /**
9
+ * Appended to the "stale data" warning. Authored copy, never derived from the
10
+ * failure — a background refetch that dies on a `MODULE_NOT_FOUND` must not
11
+ * print a require stack into a toast (audit-raw-error-text).
12
+ */
13
+ const REFRESH_HINT = "Please check your connection and try again.";
8
14
  export function useApiQuery(options) {
9
15
  const { showToast } = useToast();
10
16
  const { silentBackgroundFailures, translate, reportClientError, ...rest } = options;
@@ -22,9 +28,10 @@ export function useApiQuery(options) {
22
28
  return;
23
29
  const err = result.error;
24
30
  if (err instanceof ApiError) {
25
- const display = getErrorDisplay(err.code);
26
- const message = translate?.(display.messageKey) ?? err.message;
27
- showToast(`Couldn't refresh latest data: ${message}`, "warning");
31
+ const message = toUserMessage(err.code, translate, {
32
+ fallback: REFRESH_HINT,
33
+ });
34
+ showToast(`Couldn't refresh latest data. ${message}`, "warning");
28
35
  if (reportClientError) {
29
36
  try {
30
37
  reportClientError({
@@ -39,8 +46,12 @@ export function useApiQuery(options) {
39
46
  }
40
47
  }
41
48
  else {
42
- const message = err instanceof Error ? err.message : "Unknown error";
43
- showToast(`Couldn't refresh latest data: ${message}`, "warning");
49
+ // No stable code on a non-ApiError rejection, so there is nothing to
50
+ // resolve — go straight to the authored hint rather than the raw text.
51
+ const message = toUserMessage(undefined, translate, {
52
+ fallback: REFRESH_HINT,
53
+ });
54
+ showToast(`Couldn't refresh latest data. ${message}`, "warning");
44
55
  }
45
56
  }, [
46
57
  result.isError,
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { HTTP_ERROR_CODES, mapToHttpError } from "./errors/error-mapping";
2
2
  export type { HttpErrorCode, MappedError } from "./errors/error-mapping";
3
3
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/error-display-map";
4
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/error-display-map";
4
5
  export type { ErrorDisplayEntry } from "./errors/error-display-map";
5
6
  export { RazorpayUnreachableError } from "./errors/razorpay-unreachable";
6
7
  export type { ActionResult } from "./utils/action-result";
package/dist/client.js CHANGED
@@ -4,6 +4,9 @@
4
4
  // Used by useApiMutation / useApiQuery / Button surface-error to route ApiError → toast or inline field error.
5
5
  export { HTTP_ERROR_CODES, mapToHttpError } from "./errors/error-mapping";
6
6
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/error-display-map";
7
+ // toUserMessage — the only sanctioned path from a failure to user-facing copy.
8
+ // It terminates in a constant, so a thrown value's own .message never surfaces.
9
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/error-display-map";
7
10
  export { RazorpayUnreachableError } from "./errors/razorpay-unreachable";
8
11
  export { isOk, unwrap } from "./utils/action-result";
9
12
  // [CLIENT-SAFE] Client error class + surface-error router + reporter primitives.
@@ -13,6 +13,12 @@ export interface RealtimeSnapshot {
13
13
  /** Unsubscribe function returned by subscribe */
14
14
  export type Unsubscribe = () => void;
15
15
  export interface IClientRealtimeProvider {
16
+ /**
17
+ * A provider on its own Firebase app for this claim scope, so concurrent
18
+ * channels do not overwrite each other's auth. Optional: a provider that
19
+ * cannot isolate returns itself, keeping the old shared-app behaviour.
20
+ */
21
+ forScope?(scope: string): IClientRealtimeProvider;
16
22
  /** Authenticate the realtime connection with a custom token */
17
23
  signInWithToken(token: string): Promise<void>;
18
24
  /** Sign out of the realtime connection */
@@ -1,3 +1,4 @@
1
+ import { type IClientRealtimeProvider } from "./client-realtime";
1
2
  export interface RealtimeToken {
2
3
  customToken: string;
3
4
  expiresAt: number;
@@ -12,16 +13,15 @@ export interface RealtimeToken {
12
13
  * @returns a release function. **Always call it** — the session is only torn
13
14
  * down when every lease is released.
14
15
  */
15
- export declare function acquireRealtimeSession(scope: string, getToken: () => Promise<RealtimeToken>): Promise<() => void>;
16
- /**
17
- * Take a lease WITHOUT signing in — for a consumer that manages its own
18
- * sign-in but must still participate in the reference count.
19
- *
20
- * `useRealtimeEvent` is the case: its token carries a job-scoped `bulkJobId`
21
- * claim it receives from the caller, so it cannot use `acquireRealtimeSession`'s
22
- * fetcher. It still must not be able to sign the shared app out from under
23
- * another channel, which is precisely what its `cleanup()` used to do.
24
- */
25
- export declare function acquireExternalRealtimeLease(): () => void;
26
- /** Test/diagnostic only. */
16
+ export interface RealtimeLease {
17
+ /**
18
+ * Subscribe through THIS provider, not the global one — it is bound to the
19
+ * app this scope signed in on. Using the global provider would read from an
20
+ * app with someone else's claims.
21
+ */
22
+ provider: IClientRealtimeProvider;
23
+ release: () => void;
24
+ }
25
+ export declare function acquireRealtimeSession(scope: string, getToken: () => Promise<RealtimeToken>): Promise<RealtimeLease>;
26
+ /** Test/diagnostic only — total leases across every scope. */
27
27
  export declare function activeRealtimeLeases(): number;
@@ -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