@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
@@ -74,6 +74,11 @@ export interface SellerProductDraft {
74
74
  /** Shape returned by `onSave`/`onPublish` — structurally compatible with appkit's `ActionResult<T>` failure/success arms, without importing the server-only type into this client component. */
75
75
  export interface ProductActionResult {
76
76
  ok: boolean;
77
+ /**
78
+ * Stable error code. This is what the failure toast resolves — `error` is the
79
+ * server's own sentence and is reported, never shown (audit-raw-error-text).
80
+ */
81
+ code?: string;
77
82
  error?: string;
78
83
  issues?: unknown[];
79
84
  }
@@ -13,6 +13,7 @@ import { BarcodeField } from "./BarcodeField";
13
13
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
14
14
  import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/offers/config";
15
15
  import { normalizeError } from "../../../errors/normalize";
16
+ import { toUserMessage } from "../../../errors/error-display-map";
16
17
  /**
17
18
  * "bundle" is a categoryType, not a ListingType, so it can't live in
18
19
  * LISTING_TYPE_REGISTRY — this is the single, explicit escape hatch that
@@ -346,18 +347,25 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
346
347
  return;
347
348
  }
348
349
  applyZodIssues(result.issues ?? [], setFieldError);
349
- if (!silent)
350
- showToast(result.error || "Fix the highlighted errors and try again.", "error");
350
+ if (!silent) {
351
+ showToast(toUserMessage(result.code, undefined, {
352
+ fallback: "Fix the highlighted errors and try again.",
353
+ }), "error");
354
+ }
351
355
  }
352
356
  catch (err) {
353
357
  void normalizeError(err);
354
358
  if (!silent)
355
- showToast(err instanceof Error ? err.message : "Failed to save.", "error");
359
+ showToast("Failed to save.", "error");
356
360
  }
357
361
  }, [draft, onSave, markClean, clearErrors, setFieldError, showToast]);
358
362
  const handleAutoSave = useCallback(async () => {
359
363
  try {
360
364
  await handleSave(true);
365
+ // handleSave was called with silent=true, which is the whole point of
366
+ // auto-save: it has already applied the field errors and populated
367
+ // FormErrorSummary, so a toast every two seconds while the seller is
368
+ // still typing would be worse than saying nothing at all here.
361
369
  }
362
370
  catch (err) {
363
371
  void normalizeError(err);
@@ -383,11 +391,13 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
383
391
  return;
384
392
  }
385
393
  applyZodIssues(result.issues ?? [], setFieldError);
386
- showToast(result.error || "Fix the highlighted errors and try again.", "error");
394
+ showToast(toUserMessage(result.code, undefined, {
395
+ fallback: "Fix the highlighted errors and try again.",
396
+ }), "error");
387
397
  }
388
398
  catch (err) {
389
399
  void normalizeError(err);
390
- showToast(err instanceof Error ? err.message : "Failed to publish.", "error");
400
+ showToast("Failed to publish.", "error");
391
401
  }
392
402
  }, [draft, onPublish, markClean, clearErrors, setFieldError, showToast]);
393
403
  const listingTypeLabel = pluginForMode(listingType).typeLabel;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
+ import { toUserMessage } from "../../../errors/error-display-map";
4
5
  import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
5
6
  import { sortBy } from "@mohasinac/appkit/client";
6
7
  import { useState, useCallback } from "react";
@@ -148,15 +149,23 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
148
149
  await performDelete(row.id);
149
150
  };
150
151
  const handleDuplicate = async (row) => {
151
- const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(row.id), {
152
- method: "POST",
153
- }).catch(() => null);
154
- if (res && res.ok) {
152
+ // A swallowed failure here does nothing at all and says nothing — the
153
+ // seller clicks Duplicate and no copy appears, with no way to tell whether
154
+ // the request even left the browser.
155
+ try {
156
+ const res = await fetch(SELLER_ENDPOINTS.PRODUCT_DUPLICATE(row.id), {
157
+ method: "POST",
158
+ });
159
+ if (!res.ok)
160
+ throw new Error(`Duplicate failed (${res.status})`);
155
161
  const json = await res.json().catch(() => null);
156
162
  const newId = json?.data?.id;
157
163
  if (newId)
158
164
  handleEdit({ ...row, id: newId });
159
165
  }
166
+ catch (err) {
167
+ showToast(normalizeError(err).message, "error");
168
+ }
160
169
  };
161
170
  const handleTogglePublish = async (row) => {
162
171
  const currentStatus = statusOverrides.get(row.id) ?? row.status;
@@ -167,10 +176,15 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
167
176
  method: "PATCH",
168
177
  headers: { "Content-Type": "application/json" },
169
178
  body: JSON.stringify({ status: newStatus }),
170
- }).catch(() => null);
171
- if (res?.ok) {
172
- setStatusOverrides((prev) => new Map([...prev, [row.id, newStatus]]));
173
- }
179
+ });
180
+ if (!res.ok)
181
+ throw new Error(`Could not change status (${res.status})`);
182
+ setStatusOverrides((prev) => new Map([...prev, [row.id, newStatus]]));
183
+ }
184
+ catch (err) {
185
+ // The row silently snapped back to its old status before this — a seller
186
+ // could not tell a rejected publish from a UI that had not repainted.
187
+ showToast(normalizeError(err).message, "error");
174
188
  }
175
189
  finally {
176
190
  setPublishingId(null);
@@ -191,8 +205,8 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
191
205
  setSetLocationOpen(false);
192
206
  }
193
207
  catch (err) {
194
- void normalizeError(err);
195
- showToast(err instanceof Error ? err.message : "Failed to update location.", "error");
208
+ const e = normalizeError(err);
209
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to update location." }), "error");
196
210
  }
197
211
  }, [showToast]);
198
212
  const config = {
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { normalizeError } from "../../../errors/normalize";
4
4
  import { useCallback, useState } from "react";
5
5
  import { useQueryClient } from "@tanstack/react-query";
6
- import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text, Textarea } from "../../../ui";
6
+ import { Alert, Badge, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text, Textarea, useToast } from "../../../ui";
7
7
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
8
8
  import { SELLER_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
9
9
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
@@ -34,6 +34,7 @@ function statusBadge(status) {
34
34
  }
35
35
  export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, replyApiBase = SELLER_ENDPOINTS.REVIEWS, }) {
36
36
  const queryClient = useQueryClient();
37
+ const { showToast } = useToast();
37
38
  const refetchReviews = useCallback(() => queryClient.invalidateQueries({ queryKey: ["seller", "reviews", "listing"] }), [queryClient]);
38
39
  // Reply drawer
39
40
  const [replyTarget, setReplyTarget] = useState(null);
@@ -80,45 +81,75 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
80
81
  setReplySaving(false);
81
82
  }
82
83
  };
84
+ // The three submits below all closed their modal unconditionally, so a
85
+ // rejected or unsent request looked exactly like a successful one — the
86
+ // seller believed the contest/feedback/reply had been filed.
83
87
  const submitContest = useCallback(async (onDone) => {
84
88
  if (!contestTarget)
85
89
  return;
86
- await fetch(SELLER_ENDPOINTS.REVIEW_CONTEST(contestTarget.id), {
87
- method: "POST",
88
- headers: { "Content-Type": "application/json" },
89
- body: JSON.stringify({ reason: contestReason }),
90
- }).catch(() => null);
90
+ try {
91
+ const res = await fetch(SELLER_ENDPOINTS.REVIEW_CONTEST(contestTarget.id), {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ reason: contestReason }),
95
+ });
96
+ if (!res.ok)
97
+ throw new Error(`Could not submit the contest (${res.status})`);
98
+ }
99
+ catch (err) {
100
+ showToast(normalizeError(err).message, "error");
101
+ return;
102
+ }
91
103
  setContestTarget(null);
92
104
  setContestReason("");
93
105
  onDone();
94
- }, [contestTarget, contestReason]);
106
+ }, [contestTarget, contestReason, showToast]);
95
107
  const submitFeedback = useCallback(async () => {
96
108
  if (!feedbackTarget)
97
109
  return;
98
- await fetch(SELLER_ENDPOINTS.REVIEW_FEEDBACK(feedbackTarget.id), {
99
- method: "POST",
100
- headers: { "Content-Type": "application/json" },
101
- body: JSON.stringify({ feedback: feedbackText }),
102
- }).catch(() => null);
110
+ try {
111
+ const res = await fetch(SELLER_ENDPOINTS.REVIEW_FEEDBACK(feedbackTarget.id), {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({ feedback: feedbackText }),
115
+ });
116
+ if (!res.ok)
117
+ throw new Error(`Could not send the feedback (${res.status})`);
118
+ }
119
+ catch (err) {
120
+ showToast(normalizeError(err).message, "error");
121
+ return;
122
+ }
103
123
  setFeedbackTarget(null);
104
124
  setFeedbackText("");
105
- }, [feedbackTarget, feedbackText]);
125
+ }, [feedbackTarget, feedbackText, showToast]);
106
126
  const submitBulkReply = useCallback(async () => {
107
127
  if (!bulkReplyText.trim() || bulkTargetIds.length === 0)
108
128
  return;
109
129
  setBulkSaving(true);
110
- await Promise.all(bulkTargetIds.map((id) => fetch(`${replyApiBase}/${id}/reply`, {
130
+ // Partial success is the normal outcome of a bulk write, so report the
131
+ // count that failed rather than throwing away the whole batch's result.
132
+ const outcomes = await Promise.all(bulkTargetIds.map((id) => fetch(`${replyApiBase}/${id}/reply`, {
111
133
  method: "POST",
112
134
  headers: { "Content-Type": "application/json" },
113
135
  body: JSON.stringify({ reply: bulkReplyText }),
114
- }).catch(() => null)));
136
+ })
137
+ .then((res) => res.ok)
138
+ .catch((err) => {
139
+ void normalizeError(err);
140
+ return false;
141
+ })));
142
+ const failed = outcomes.filter((ok) => !ok).length;
143
+ if (failed > 0) {
144
+ showToast(`${failed} of ${outcomes.length} replies could not be saved.`, "error");
145
+ }
115
146
  setBulkSaving(false);
116
147
  setBulkReplyOpen(false);
117
148
  setBulkReplyText("");
118
149
  bulkClearSelection?.();
119
150
  setBulkTargetIds([]);
120
151
  await refetchReviews();
121
- }, [bulkReplyText, bulkTargetIds, replyApiBase, bulkClearSelection, refetchReviews]);
152
+ }, [bulkReplyText, bulkTargetIds, replyApiBase, bulkClearSelection, refetchReviews, showToast]);
122
153
  const config = {
123
154
  portal: "seller",
124
155
  title: "Reviews",
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
+ import { toUserMessage } from "../../../errors/error-display-map";
4
5
  import { sortBy } from "@mohasinac/appkit/client";
5
6
  import { useState, useCallback } from "react";
6
7
  import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
@@ -53,8 +54,8 @@ export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete
53
54
  await performDelete(id);
54
55
  }
55
56
  catch (err) {
56
- void normalizeError(err);
57
- showToast(err instanceof Error ? err.message : "Failed to delete shipping config", "error");
57
+ const e = normalizeError(err);
58
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to delete shipping config." }), "error");
58
59
  }
59
60
  setDeleteTargetId(null);
60
61
  }, [performDelete, showToast]);
@@ -75,8 +76,8 @@ export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete
75
76
  showToast("Default shipping config updated.", "success");
76
77
  }
77
78
  catch (err) {
78
- void normalizeError(err);
79
- showToast(err instanceof Error ? err.message : "Failed to update default.", "error");
79
+ const e = normalizeError(err);
80
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to update default." }), "error");
80
81
  }
81
82
  finally {
82
83
  setSettingDefaultId(null);
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../errors/normalize";
4
+ import { toUserMessage } from "../../../errors/error-display-map";
4
5
  import { Badge, sortBy } from "@mohasinac/appkit/client";
5
6
  import { useState, useCallback } from "react";
6
7
  import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
@@ -49,8 +50,8 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
49
50
  await performDelete(id);
50
51
  }
51
52
  catch (err) {
52
- void normalizeError(err);
53
- showToast(err instanceof Error ? err.message : "Failed to delete category", "error");
53
+ const e = normalizeError(err);
54
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to delete category." }), "error");
54
55
  }
55
56
  setDeleteTargetId(null);
56
57
  }, [onDelete, performDelete, showToast]);
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type { ReactNode } from "react";
3
- export declare const getStoreBySlug: (slug: string) => Promise<import("@mohasinac/appkit").StoreDocument | null | undefined>;
3
+ export declare const getStoreBySlug: (slug: string) => Promise<import("@mohasinac/appkit").StoreDocument | null>;
4
4
  export interface StoreDetailLayoutViewProps {
5
5
  storeSlug: string;
6
6
  /** The current active tab value: "products" | "auctions" | "reviews" | "about" */
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { sieveFilter, sieveAnd, SIEVE_OP } from "@mohasinac/appkit";
3
3
  import { cache } from "react";
4
4
  import { storeRepository, siteSettingsRepository, reviewRepository, couponsRepository } from "../../../repositories";
5
+ import { safeRead } from "../../../errors/safe-read";
5
6
  import { ROUTES } from "../../../next";
6
7
  import { Container, Main, Section, Text } from "../../../ui";
7
8
  import { STORE_PAGE_TABS } from "../../products/constants/listing-tabs";
@@ -26,7 +27,10 @@ const STORE_LISTING_HREF = {
26
27
  // a real non-zero count and landed on a page with none of those items.
27
28
  art: (slug) => String(ROUTES.PUBLIC.STORE_ART(slug)),
28
29
  };
29
- export const getStoreBySlug = cache((slug) => storeRepository.findBySlug(slug).catch(() => undefined));
30
+ // The store IS this layout's subject, and `findBySlug` already returns null for
31
+ // "no such slug" — swallowing a read failure into the same value would render
32
+ // the "Store not found." branch below for a live store during an outage.
33
+ export const getStoreBySlug = cache((slug) => storeRepository.findBySlug(slug));
30
34
  function tabLabel(base, count) {
31
35
  if (!count)
32
36
  return base;
@@ -39,9 +43,14 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, sc
39
43
  }
40
44
  const storeId = store?.id;
41
45
  const [settings, trust] = await Promise.all([
42
- siteSettingsRepository.findById("global").catch(() => null),
46
+ safeRead(() => siteSettingsRepository.findById("global"), {
47
+ route: "/stores/[slug]", key: "store.siteSettings", fallback: null,
48
+ }),
43
49
  scamRegistryEnabled && storeId
44
- ? getSellerTrustStatus(storeId).catch(() => undefined)
50
+ // Fails open to "no trust verdict", which hides the badge entirely rather
51
+ // than asserting a seller is clear — but that has to be recorded, since a
52
+ // missing badge is exactly what a clean seller looks like.
53
+ ? safeRead(() => getSellerTrustStatus(storeId), { route: "/stores/[slug]", key: "store.sellerTrust", fallback: undefined })
45
54
  : Promise.resolve(undefined),
46
55
  ]);
47
56
  // One derived batch instead of nine copy-pasted per-type queries. The tabs
@@ -3728,7 +3728,22 @@ const rawTesterChecklistItems = [
3728
3728
  href: "/admin/site",
3729
3729
  },
3730
3730
  { key: "admin-dashboard-widgets", label: "Admin dashboard widgets show accurate data", href: "/admin/dashboard" },
3731
- { key: "analytics-admin", label: "Admin analytics dashboard shows accurate data", href: "/admin/analytics" },
3731
+ {
3732
+ key: "analytics-admin",
3733
+ label: "Admin analytics dashboard shows accurate data",
3734
+ href: "/admin/analytics",
3735
+ },
3736
+ {
3737
+ key: "analytics-traffic-card-has-numbers",
3738
+ label: "The \"Traffic today\" card shows a real number, and it MATCHES the Page Views tab beside it",
3739
+ description: "Fixed 2026-08-29. BEFORE: the card showed an em-dash forever — never even a 0. It subscribed to two Realtime Database paths written by a presence hook whose writes had been denied since the day it shipped (the RTDB rules are backend-write-only), and both subscriptions passed no error callback, so a permission denial fired nothing at all and the tiles simply never left their initial state. AFTER: the card reads the same Firestore pageview counter that backs the Page Views tab in the same tab strip, so the two figures must agree — compare them. If the card errors it now SAYS so instead of showing a dash.",
3740
+ },
3741
+ {
3742
+ key: "analytics-no-permission-denied",
3743
+ label: "Opening /admin/analytics logs no PERMISSION_DENIED in the browser console",
3744
+ description: "Fixed 2026-08-29. BEFORE: every admin page load produced two Firebase permission_denied warnings from the dead presence/analytics subscriptions. AFTER: none — those subscriptions are gone entirely. Open devtools, hard-reload the page, and confirm the console is clean of Firebase permission errors.",
3745
+ href: "/admin/analytics",
3746
+ },
3732
3747
  {
3733
3748
  key: "pageviews-report-listing-standard",
3734
3749
  label: "The \"Page Views\" tab in Admin Analytics is a full standard listing — a search box (by entity id or URL), an entity-type filter drawer, a \"Most views\"/\"Fewest views\" sort dropdown, and real pagination — not a bare unsearchable table",
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useApiMutation, useBulkEvent, RTDB_PATHS } from "@mohasinac/appkit/client";
4
4
  import React from "react";
5
5
  import { normalizeError } from "../../../errors/normalize";
6
+ import { toUserMessage } from "../../../errors/error-display-map";
6
7
  const __P = {
7
8
  p3: "p-[var(--appkit-space-3)]",
8
9
  };
@@ -156,9 +157,9 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
156
157
  }
157
158
  }
158
159
  catch (err) {
159
- void normalizeError(err);
160
+ const e = normalizeError(err);
160
161
  setImporting(false);
161
- showToast(err instanceof Error ? err.message : "Import failed", "error");
162
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to start catalog import." }), "error");
162
163
  }
163
164
  };
164
165
  React.useEffect(() => {
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { useCallback, useState } from "react";
3
3
  import { useToast } from "../../../ui";
4
+ import { normalizeError } from "../../../errors/normalize";
5
+ import { toUserMessage } from "../../../errors/error-display-map";
4
6
  export function useWishlistToggle(productId, initial = false, actions) {
5
7
  const [inWishlist, setInWishlist] = useState(initial);
6
8
  const [isLoading, setIsLoading] = useState(false);
@@ -25,8 +27,13 @@ export function useWishlistToggle(productId, initial = false, actions) {
25
27
  }
26
28
  }
27
29
  catch (err) {
30
+ const e = normalizeError(err);
28
31
  setInWishlist(prev);
29
- showToast(err instanceof Error ? err.message : "Something went wrong.", "error");
32
+ showToast(toUserMessage(e.code, undefined, {
33
+ fallback: prev
34
+ ? "Could not remove this item from your wishlist."
35
+ : "Could not add this item to your wishlist.",
36
+ }), "error");
30
37
  throw err;
31
38
  }
32
39
  finally {
package/dist/index.d.ts CHANGED
@@ -333,6 +333,7 @@ export { RazorpayUnreachableError } from "./errors/index";
333
333
  export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
334
334
  export type { MappedError, HttpErrorCode } from "./errors/index";
335
335
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/index";
336
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/index";
336
337
  export type { ErrorDisplayEntry } from "./errors/index";
337
338
  export { parseJsonBody } from "./next/api/parseJsonBody";
338
339
  export { SEO_CONFIG } from "./seo/index";
@@ -849,7 +850,6 @@ export { requireRoleUser } from "./providers/auth-firebase/index";
849
850
  export { revokeUserTokens } from "./providers/auth-firebase/index";
850
851
  export { verifyIdToken } from "./providers/auth-firebase/index";
851
852
  export { verifySessionCookie } from "./providers/auth-firebase/index";
852
- export { FirebaseRealtimeRepository } from "./providers/db-firebase/index";
853
853
  export { FirebaseRepository } from "./providers/db-firebase/index";
854
854
  export { FirebaseSieveRepository } from "./providers/db-firebase/index";
855
855
  export { RTDB_PATHS } from "./providers/db-firebase/index";
package/dist/index.js CHANGED
@@ -568,6 +568,11 @@ export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
568
568
  // [UTIL]-Pure utility — client-safe lookup of inline-field + i18n key per error code.
569
569
  // ERROR_DISPLAY_MAP - CLAUDE.md Rule #9 source-of-truth for client error rendering.
570
570
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/index";
571
+ // [UTIL]-Pure utility — the ONLY sanctioned way to turn a failure into user copy.
572
+ // toUserMessage - resolves a stable error code through errors.codes.*; terminates
573
+ // in a constant, so a thrown value's own .message can never reach a user
574
+ // (audit-raw-error-text).
575
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/index";
571
576
  // [SERVER-ONLY] parseJsonBody — strict JSON request-body parser for API routes.
572
577
  // Replaces the silent `request.json().catch(() => ({}))` anti-pattern. Throws
573
578
  // ValidationError on malformed JSON → routeHandler wrapper maps to 400.
@@ -1726,8 +1731,6 @@ export { verifyIdToken } from "./providers/auth-firebase/index";
1726
1731
  export { verifySessionCookie } from "./providers/auth-firebase/index";
1727
1732
  // ./providers/db-firebase/index
1728
1733
  // [DB]-Database layer â€" uses firebase-admin or another server-side DB SDK; can only run in a trusted server environment.
1729
- // FirebaseRealtimeRepository - Shared export for firebase realtime repository.
1730
- export { FirebaseRealtimeRepository } from "./providers/db-firebase/index";
1731
1734
  // [DB]-Database layer â€" uses firebase-admin or another server-side DB SDK; can only run in a trusted server environment.
1732
1735
  // FirebaseRepository - Shared export for firebase repository.
1733
1736
  export { FirebaseRepository } from "./providers/db-firebase/index";
@@ -182,10 +182,12 @@ export function createRouteHandler(options) {
182
182
  else if (options.authOptional) {
183
183
  try {
184
184
  user = await verifySession(request);
185
+ // Deliberate: this is the OPTIONAL-auth path. A bad or absent session means
186
+ // anonymous, which is a legitimate outcome here — routes that require a
187
+ // user use `auth: true`, which throws instead.
185
188
  }
186
189
  catch (_err) {
187
190
  void normalizeError(_err);
188
- // anonymous
189
191
  }
190
192
  }
191
193
  // -- Role check --------------------------------------------------------
@@ -71,8 +71,10 @@ export const firebaseSessionProvider = {
71
71
  await getAdminAuthLite().revokeRefreshTokens(decoded.uid);
72
72
  }
73
73
  catch (_err) {
74
+ // Best-effort teardown: an expired/revoked/malformed cookie cannot be
75
+ // verified, and that is the state destroySession is trying to reach —
76
+ // reporting it would alarm on the success case.
74
77
  void normalizeError(_err);
75
- // Best-effort: if cookie is already invalid, treat as already destroyed
76
78
  }
77
79
  },
78
80
  };
@@ -1,4 +1,9 @@
1
1
  import { initializeApp, getApps, cert } from "firebase-admin/app";
2
+ // One resolver, not a second copy — this file duplicated admin.ts's URL
3
+ // logic byte-for-byte, so the region-wrong fallback bug existed twice.
4
+ // Imported from its own dependency-free module, NOT from ./admin, because the
5
+ // whole point of admin-app-lite is not pulling the full admin module in.
6
+ import { resolveRtdbUrl } from "./rtdb-url";
2
7
  function nodePath() { return require("path"); }
3
8
  function nodeFs() { return require("fs"); }
4
9
  // process.cwd() is Node.js-only; access via module.require so the Edge
@@ -26,17 +31,14 @@ export function getAdminAppLite() {
26
31
  let app;
27
32
  if (nodeFs().existsSync(keyPath)) {
28
33
  const sa = JSON.parse(nodeFs().readFileSync(keyPath, "utf8"));
29
- const dbUrl = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
30
- `https://${sa.project_id}-default-rtdb.firebaseio.com`;
34
+ const dbUrl = resolveRtdbUrl();
31
35
  app = initializeApp({ credential: cert(keyPath), databaseURL: dbUrl, ...(storageBucket && { storageBucket }) });
32
36
  }
33
37
  else if (process.env.FIREBASE_ADMIN_PROJECT_ID &&
34
38
  process.env.FIREBASE_ADMIN_CLIENT_EMAIL &&
35
39
  process.env.FIREBASE_ADMIN_PRIVATE_KEY) {
36
40
  const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID.trim();
37
- const dbUrl = process.env.FIREBASE_ADMIN_DATABASE_URL?.trim() ??
38
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL?.trim() ??
39
- `https://${projectId}-default-rtdb.firebaseio.com`;
41
+ const dbUrl = resolveRtdbUrl();
40
42
  app = initializeApp({
41
43
  credential: cert({
42
44
  projectId,
@@ -63,9 +65,7 @@ export function getAdminAppLite() {
63
65
  : {};
64
66
  const projectId = firebaseConfig.projectId ?? process.env.GCLOUD_PROJECT ?? "";
65
67
  const dbUrl = firebaseConfig.databaseURL ??
66
- process.env.FIREBASE_ADMIN_DATABASE_URL ??
67
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
68
- (projectId ? `https://${projectId}-default-rtdb.firebaseio.com` : undefined);
68
+ resolveRtdbUrl();
69
69
  app = initializeApp({
70
70
  ...(dbUrl && { databaseURL: dbUrl }),
71
71
  ...(storageBucket && { storageBucket }),
@@ -1,4 +1,5 @@
1
1
  import { normalizeError } from "../../errors/normalize";
2
+ import { resolveRtdbUrl } from "./rtdb-url";
2
3
  function nodePath() { return require("path"); }
3
4
  function nodeFs() { return require("fs"); }
4
5
  // process.cwd() is Node.js-only; access via module.require so the Edge
@@ -69,17 +70,16 @@ export function getAdminApp() {
69
70
  try {
70
71
  if (nodeFs().existsSync(keyPath)) {
71
72
  const sa = JSON.parse(nodeFs().readFileSync(keyPath, "utf8"));
72
- const dbUrl = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
73
- `https://${sa.project_id}-default-rtdb.firebaseio.com`;
73
+ // Was NEXT_PUBLIC-only here while the other two branches honoured
74
+ // FIREBASE_ADMIN_DATABASE_URL, so setting it had no effect on this path.
75
+ const dbUrl = resolveRtdbUrl();
74
76
  app = initializeApp({ credential: cert(keyPath), databaseURL: dbUrl, ...(storageBucket && { storageBucket }) });
75
77
  }
76
78
  else if (process.env.FIREBASE_ADMIN_PROJECT_ID &&
77
79
  process.env.FIREBASE_ADMIN_CLIENT_EMAIL &&
78
80
  process.env.FIREBASE_ADMIN_PRIVATE_KEY) {
79
81
  const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID.trim();
80
- const dbUrl = process.env.FIREBASE_ADMIN_DATABASE_URL?.trim() ??
81
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL?.trim() ??
82
- `https://${projectId}-default-rtdb.firebaseio.com`;
82
+ const dbUrl = resolveRtdbUrl();
83
83
  app = initializeApp({
84
84
  credential: cert({
85
85
  projectId,
@@ -105,12 +105,9 @@ export function getAdminApp() {
105
105
  ? JSON.parse(process.env.FIREBASE_CONFIG)
106
106
  : {};
107
107
  const projectId = firebaseConfig.projectId ?? process.env.GCLOUD_PROJECT ?? "";
108
- const dbUrl = firebaseConfig.databaseURL ??
109
- process.env.FIREBASE_ADMIN_DATABASE_URL ??
110
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
111
- (projectId
112
- ? `https://${projectId}-default-rtdb.firebaseio.com`
113
- : undefined);
108
+ // FIREBASE_CONFIG is injected by the Functions runtime and is
109
+ // authoritative there; env vars are the fallback for other runtimes.
110
+ const dbUrl = firebaseConfig.databaseURL || resolveRtdbUrl();
114
111
  app = initializeApp({
115
112
  ...(dbUrl && { databaseURL: dbUrl }),
116
113
  ...(storageBucket && { storageBucket }),
@@ -153,8 +150,11 @@ export function getAdminDb() {
153
150
  db.settings({ ignoreUndefinedProperties: true });
154
151
  }
155
152
  catch (_err) {
153
+ // Firestore rejects a second settings() call on an already-initialised
154
+ // instance, which is the only way this throws (HMR reload / duplicate
155
+ // module instance re-entering the memoised bootstrap). The instance is
156
+ // fully usable either way, so there is nothing degraded to report.
156
157
  void normalizeError(_err);
157
- // Already configured — safe to ignore.
158
158
  }
159
159
  set("__mohasinac_firebase_admin_db__", db);
160
160
  return db;
@@ -22,17 +22,7 @@ export declare function prepareForFirestore(data: any): any;
22
22
  export declare function deserializeTimestamps<T>(data: any): T;
23
23
  export declare const FirebaseRepository: any;
24
24
  export declare const FirebaseSieveRepository: any;
25
- export declare const FirebaseRealtimeRepository: any;
26
25
  export declare const firebaseDbProvider: any;
27
- export declare const RTDB_PATHS: {
28
- readonly PRESENCE: "presence";
29
- readonly CHAT: "chat";
30
- readonly NOTIFICATIONS: "notifications";
31
- readonly LIVE_UPDATES: "live_updates";
32
- readonly AUTH_EVENTS: "auth_events";
33
- readonly PAYMENT_EVENTS: "payment_events";
34
- readonly BULK_EVENTS: "bulk_events";
35
- readonly SEED_EVENTS: "seed_events";
36
- };
26
+ export { RTDB_PATHS } from "./rtdb-paths";
37
27
  export { buildFirebaseClientConfig, normalizeFirebaseConfigValue } from './client-config';
38
28
  export type { FirebaseClientConfig } from './client-config';
@@ -40,17 +40,12 @@ export function deserializeTimestamps(data) {
40
40
  }
41
41
  export const FirebaseRepository = null;
42
42
  export const FirebaseSieveRepository = null;
43
- export const FirebaseRealtimeRepository = null;
44
43
  export const firebaseDbProvider = null;
45
- export const RTDB_PATHS = {
46
- PRESENCE: "presence",
47
- CHAT: "chat",
48
- NOTIFICATIONS: "notifications",
49
- LIVE_UPDATES: "live_updates",
50
- AUTH_EVENTS: "auth_events",
51
- PAYMENT_EVENTS: "payment_events",
52
- BULK_EVENTS: "bulk_events",
53
- SEED_EVENTS: "seed_events",
54
- };
44
+ // Re-exported, NOT re-declared. This file is the `browser` resolution target, so
45
+ // the byte-for-byte copy that used to live here meant client code and server
46
+ // code read two independently-maintained definitions of the same constant, with
47
+ // nothing guarding the drift. `rtdb-paths.ts` has no imports, so it is safe to
48
+ // pull into a browser bundle directly.
49
+ export { RTDB_PATHS } from "./rtdb-paths";
55
50
  // Client config builder � safe in all environments
56
51
  export { buildFirebaseClientConfig, normalizeFirebaseConfigValue } from './client-config';
@@ -20,7 +20,6 @@ export type { DocumentSnapshot } from "firebase-admin/firestore";
20
20
  export { FirebaseRepository } from "./base";
21
21
  export { BaseRepository } from "./base.repository";
22
22
  export { FirebaseSieveRepository } from "./sieve";
23
- export { FirebaseRealtimeRepository } from "./realtime";
24
23
  export { RTDB_PATHS } from "./rtdb-paths";
25
24
  import type { IDbProvider } from "../../contracts";
26
25
  export declare const firebaseDbProvider: IDbProvider;