@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
@@ -5,8 +5,18 @@
5
5
  * conversations gets a new message.
6
6
  */
7
7
  import { useCallback, useEffect, useState } from "react";
8
- import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
9
8
  import { acquireRealtimeSession } from "../../../contracts/realtime-session";
9
+ // One scope for both message hooks, deliberately: /api/realtime/token issues a
10
+ // SINGLE token covering all of this user's conversationIds, so a per-id scope
11
+ // would make the list and the detail hook re-sign against each other on the
12
+ // same page for no gain.
13
+ //
14
+ // 🛑 Known limitation, not fixed here: because the scope is constant, a
15
+ // conversation created AFTER sign-in is not in the cached token's claims and
16
+ // its subscription is denied until the token's expiry skew forces a re-sign.
17
+ // The denial is now surfaced (see the error callback below) rather than
18
+ // swallowed. The durable fix is to re-acquire on a permission denial.
19
+ const MESSAGES_SCOPE = "messages";
10
20
  import { userConversationsPingPath } from "../realtime";
11
21
  import { normalizeError } from "../../../errors/normalize";
12
22
  import { apiClient } from "../../../http/ApiClient";
@@ -57,23 +67,30 @@ export function useConversations(userId, opts = {}) {
57
67
  // `// ignore`, so the whole live-list channel was inert while looking wired.
58
68
  void (async () => {
59
69
  try {
60
- release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
70
+ const lease = await acquireRealtimeSession(MESSAGES_SCOPE, () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
71
+ release = lease.release;
61
72
  if (cancelled) {
62
- release?.();
73
+ release();
63
74
  return;
64
75
  }
65
- unsubscribe = getClientRealtimeProvider().subscribe(userConversationsPingPath(userId), () => {
76
+ // Subscribe through the LEASE's provider — see useConversation.
77
+ unsubscribe = lease.provider.subscribe(userConversationsPingPath(userId), () => {
66
78
  void refetch();
67
79
  }, (err) => {
68
- // Surfaced, not ignored. A denied subscription is a real defect and
69
- // silence here is what hid it for months.
80
+ // A denied subscription is a real defect — silence is what hid this
81
+ // channel being dead for months — so it is logged, not swallowed.
82
+ // (This comment previously claimed "Surfaced, not ignored" while the
83
+ // body only called normalizeError, which classifies without
84
+ // reporting. That was the same documentation-that-lies pattern this
85
+ // workstream exists to remove, so it now actually logs.)
70
86
  void normalizeError(err);
87
+ console.warn("[messages] conversations live-update subscription denied; list falls back to fetch-on-load");
71
88
  });
89
+ // Provider unregistered or token fetch failed — non-fatal; the list
90
+ // still works via the one-shot fetch above, just without live updates.
72
91
  }
73
92
  catch (_err) {
74
93
  void normalizeError(_err);
75
- // Provider unregistered or token fetch failed — non-fatal; the list
76
- // still works via the one-shot fetch above, just without live updates.
77
94
  }
78
95
  })();
79
96
  return () => {
@@ -16,6 +16,7 @@ import { Button, Checkbox, Div, Span, Stack, Text, Textarea } from "../../../ui"
16
16
  import { REFUND_COPY } from "../../../_internal/shared/features/orders/refund-copy";
17
17
  import { formatCurrency } from "../../../utils/number.formatter";
18
18
  import { normalizeError } from "../../../errors/normalize";
19
+ import { toUserMessage } from "../../../errors/error-display-map";
19
20
  const ACK_COUNT = REFUND_COPY.request.acknowledgments.length;
20
21
  const CLS_WARN_PANEL = "rounded-xl border border-warning bg-warning-surface p-[var(--appkit-space-4)] dark:border-warning";
21
22
  export function RefundRequestView({ order, onSubmitRequest, isLoading = false, className = "", }) {
@@ -37,8 +38,10 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
37
38
  await onSubmitRequest(reason.trim());
38
39
  }
39
40
  catch (err) {
40
- void normalizeError(err);
41
- setError(err instanceof Error ? err.message : REFUND_COPY.request.errorFallback);
41
+ const e = normalizeError(err);
42
+ setError(toUserMessage(e.code, undefined, {
43
+ fallback: REFUND_COPY.request.errorFallback,
44
+ }));
42
45
  }
43
46
  };
44
47
  return (_jsxs(Stack, { gap: "md", className: className, children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx(Span, { weight: "bold", children: formatCurrency(order.totalPrice, order.currency ?? "INR") })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonLabel, " ", _jsx(Span, { "aria-hidden": true, children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: REFUND_COPY.request.reasonPlaceholder, rows: 3, "aria-required": "true", "aria-label": REFUND_COPY.request.reasonLabel })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", children: REFUND_COPY.request.acknowledgeHeading }), REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: acknowledged[i], onChange: () => handleToggle(i), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}` }, i)))] }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error })), _jsx(Button, { variant: "danger", onClick: handleSubmit, disabled: !canSubmit, "aria-busy": isLoading, children: isLoading ? REFUND_COPY.request.submittingLabel : REFUND_COPY.request.submitLabel })] }));
@@ -54,8 +54,10 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
54
54
  await toggle();
55
55
  }
56
56
  catch (_err) {
57
+ // useWishlistToggle rolls back its optimistic state and raises its own
58
+ // toast on failure, so the heart returns to its true value on screen.
59
+ // Reporting again here would double-notify for one user action.
57
60
  void normalizeError(_err);
58
- // The hook rolls back optimistic state when the mutation fails.
59
61
  }
60
62
  }, [toggle]);
61
63
  return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
@@ -24,7 +24,7 @@ export function PreOrderActionsClient({ productId, price, currency, depositAmoun
24
24
  setShowLoginModal(true);
25
25
  }
26
26
  else {
27
- setError(err instanceof Error ? err.message : "Failed to reserve. Please try again.");
27
+ setError("Failed to reserve. Please try again.");
28
28
  }
29
29
  }
30
30
  });
@@ -73,7 +73,10 @@ function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, produc
73
73
  export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow, productFeatures }) {
74
74
  const product = initialPreOrder !== undefined
75
75
  ? (initialPreOrder ?? undefined)
76
- : await productRepository.findByIdOrSlug(id).catch(() => undefined);
76
+ // The pre-order IS this page's subject — a read failure must surface rather
77
+ // than become the same `undefined` a removed listing produces, which the
78
+ // "Not Found" branch below already handles.
79
+ : await productRepository.findByIdOrSlug(id);
77
80
  if (!product) {
78
81
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Pre-Order Not Found" }), _jsx(Text, { color: "muted", children: "The pre-order item you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRE_ORDERS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Pre-Orders" })] }) }) }) }));
79
82
  }
@@ -68,7 +68,10 @@ function stripIsWon(items) {
68
68
  export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUserId, }) {
69
69
  const product = initialPrizeDraw !== undefined
70
70
  ? initialPrizeDraw ?? undefined
71
- : await productRepository.findByIdOrSlug(id).catch(() => undefined);
71
+ // The prize draw IS this page's subject — a read failure must surface
72
+ // rather than become the same `undefined` a removed listing produces,
73
+ // which the "Not Found" branch below already handles.
74
+ : await productRepository.findByIdOrSlug(id);
72
75
  if (!product) {
73
76
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Prize Draw Not Found" }), _jsx(Text, { color: "muted", children: "The prize draw you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Prize Draws" })] }) }) }) }));
74
77
  }
@@ -41,6 +41,7 @@ import { computeRelatedItems, getReviewPageForProduct } from "../../../_internal
41
41
  import { GroupedListingsCarousel } from "../../grouped/components/GroupedListingsCarousel";
42
42
  import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
43
43
  import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
44
+ import { safeRead } from "../../../errors/safe-read";
44
45
  import { canMakeOffer } from "../../../_internal/shared/listing-types/capabilities";
45
46
  import { normalizeListingType } from "../utils/listing-type";
46
47
  import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/offers/config";
@@ -71,7 +72,10 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
71
72
  // generateMetadata() and this component share the same in-flight promise.
72
73
  const product = initialProduct !== undefined
73
74
  ? (initialProduct ?? undefined)
74
- : await productRepository.findByIdOrSlug(slug).catch(() => undefined);
75
+ // The product IS this page's subject — a read failure must surface rather
76
+ // than become the same `undefined` a removed listing produces, which the
77
+ // "Not Found" branch below already handles.
78
+ : await productRepository.findByIdOrSlug(slug);
75
79
  if (!product) {
76
80
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsxs(Stack, { align: "start", gap: "md", className: "text-left", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "semibold", color: "primary", children: "Product Not Found" }), _jsx(Text, { color: "muted", children: "The product you are looking for may have been removed or the link is incorrect." }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCTS), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Products" })] }) }) }) }));
77
81
  }
@@ -180,7 +184,9 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
180
184
  getReviewPageForProduct(product.id),
181
185
  computeRelatedItems(product),
182
186
  getGroupsWithItemsForProduct(product.id),
183
- getSiteSettingsGlobal().catch(() => null),
187
+ safeRead(() => getSiteSettingsGlobal(), {
188
+ route: "/products/[slug]", key: "product.siteSettings", fallback: null,
189
+ }),
184
190
  ]);
185
191
  // Site-wide kill switch. Absent/unreadable settings mean ON — a failed
186
192
  // settings read must not silently remove a working feature.
@@ -11,10 +11,12 @@ export function ShareButton({ title, text, className = "" }) {
11
11
  try {
12
12
  await navigator.share({ title: title ?? document.title, text, url });
13
13
  return;
14
+ // navigator.share rejects on user cancellation exactly as it does on a
15
+ // real failure, so this is unreportable by construction — either way the
16
+ // clipboard fallback below still gives the user the link.
14
17
  }
15
18
  catch (_err) {
16
19
  void normalizeError(_err);
17
- // user cancelled or browser blocked — fall through to clipboard
18
20
  }
19
21
  }
20
22
  navigator.clipboard.writeText(url).catch(console.error);
@@ -4,11 +4,19 @@
4
4
  * pages to hand `productFeatures` down as a prop and avoid per-card waterfalls.
5
5
  */
6
6
  import { productFeaturesRepository } from "./product-features.repository";
7
+ import { safeRead } from "../../../errors/safe-read";
7
8
  export async function loadProductFeaturesForStore(storeId) {
9
+ // Feature badges decorate a card; a page without them is still correct, so
10
+ // both reads degrade — but a failure here silently strips "Free shipping" /
11
+ // "Authenticity guaranteed" from every card, which nobody would notice.
8
12
  const [platform, store] = await Promise.all([
9
- productFeaturesRepository.listPlatform().catch(() => []),
13
+ safeRead(() => productFeaturesRepository.listPlatform(), {
14
+ route: "productFeatures", key: "productFeatures.platform", fallback: [],
15
+ }),
10
16
  storeId
11
- ? productFeaturesRepository.listForStore(storeId).catch(() => [])
17
+ ? safeRead(() => productFeaturesRepository.listForStore(storeId), {
18
+ route: "productFeatures", key: "productFeatures.store", fallback: [],
19
+ })
12
20
  : Promise.resolve([]),
13
21
  ]);
14
22
  const seen = new Set();
@@ -452,10 +452,11 @@ export class ProductRepository extends BaseRepository {
452
452
  .update({
453
453
  [PRODUCT_FIELDS.VIEW_COUNT]: increment(1),
454
454
  });
455
+ // Fire-and-forget analytics: a lost view increment changes no rendered
456
+ // content, so it must never break the product read path it rides with.
455
457
  }
456
458
  catch (_err) {
457
459
  void normalizeError(_err);
458
- // View analytics must not break product read path.
459
460
  }
460
461
  }
461
462
  /**
@@ -131,10 +131,12 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
131
131
  credentials: "include",
132
132
  body: JSON.stringify({ couponCode: n.code, source: claimSource }),
133
133
  });
134
+ // An anonymous visitor gets a 401 here by design, so a failure cannot
135
+ // be distinguished from the expected case. The redirect below still
136
+ // pre-fills the code, which is what the user actually asked for.
134
137
  }
135
138
  catch (_err) {
136
139
  void normalizeError(_err);
137
- /* anon/network — non-fatal */
138
140
  }
139
141
  if (typeof window !== "undefined") {
140
142
  window.location.href = `/cart?coupon=${encodeURIComponent(n.code)}`;
@@ -10,6 +10,7 @@ import { serverLogger } from "../../../monitoring";
10
10
  import { maskPublicReview } from "../../../security";
11
11
  import { finalizeStagedMediaArray, finalizeStagedMediaUrl, } from "../../media/finalize";
12
12
  import { reviewRepository } from "../repository/reviews.repository";
13
+ import { safeRead } from "../../../errors/safe-read";
13
14
  import { productRepository } from "../../products/repository/products.repository";
14
15
  import { ProductStatusValues } from "../../products/schemas";
15
16
  import { userRepository } from "../../auth/repository/user.repository";
@@ -185,7 +186,13 @@ export async function listReviewsBySeller(sellerId) {
185
186
  }
186
187
  const reviewBatches = await Promise.all(productIds
187
188
  .slice(0, 20)
188
- .map((id) => reviewRepository.findApprovedByProduct(id, 50).catch(() => [])));
189
+ // One product's reviews failing must not empty the whole seller feed —
190
+ // but it silently thins it, so each miss is recorded.
191
+ .map((id) => safeRead(() => reviewRepository.findApprovedByProduct(id, 50), {
192
+ route: "reviews.bySeller",
193
+ key: "reviews.findApprovedByProduct",
194
+ fallback: [],
195
+ })));
189
196
  // Each per-product batch is newest-first, but concatenating them yields product order,
190
197
  // not chronological order — the flattened list has to be re-sorted or callers get an
191
198
  // arbitrarily interleaved "latest reviews" list.
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
3
  import { reviewRepository } from "../../../repositories";
4
+ import { safeRead } from "../../../errors/safe-read";
4
5
  import { Container, Div, Grid, Heading, Main, Nav, Section, Span, Text } from "../../../ui";
5
6
  import { ROUTES } from "../../../next";
6
7
  import { ReviewDetailShell } from "./ReviewDetailShell";
7
8
  import { ReviewCard } from "./ReviewsList";
8
9
  export async function ReviewDetailPageView({ id }) {
9
- const doc = await reviewRepository.findById(id).catch(() => null);
10
+ // The review IS this page's subject — a read failure must surface rather than
11
+ // become the same `null` a removed review produces, which the "Review not
12
+ // found" branch below already handles.
13
+ const doc = await reviewRepository.findById(id);
10
14
  const review = doc;
11
15
  // storeId === storeSlug for stores in this project (pure slug IDs)
12
16
  const storeSlug = doc?.storeId ?? null;
@@ -15,8 +19,14 @@ export async function ReviewDetailPageView({ id }) {
15
19
  }
16
20
  const [sameProductDocs, sameStoreDocs] = await Promise.all([
17
21
  // 12 = the 6 rendered below, with headroom for the current review + any filtered out.
18
- reviewRepository.findApprovedByProduct(review.productId, 12).catch(() => []),
19
- storeSlug ? reviewRepository.findApprovedByStore(storeSlug).catch(() => []) : Promise.resolve([]),
22
+ safeRead(() => reviewRepository.findApprovedByProduct(review.productId, 12), {
23
+ route: "/reviews/[id]", key: "review.sameProduct", fallback: [],
24
+ }),
25
+ storeSlug
26
+ ? safeRead(() => reviewRepository.findApprovedByStore(storeSlug), {
27
+ route: "/reviews/[id]", key: "review.sameStore", fallback: [],
28
+ })
29
+ : Promise.resolve([]),
20
30
  ]);
21
31
  const sameProductReviews = sameProductDocs.filter((r) => r.id !== review.id).slice(0, 6);
22
32
  const sameStoreReviews = sameStoreDocs.filter((r) => r.id !== review.id).slice(0, 6);
@@ -80,10 +80,12 @@ export function ReviewDetailShell({ review, storeHref }) {
80
80
  }
81
81
  setVoted(true);
82
82
  localStorage.setItem(storageKey, "1");
83
+ // The button stays un-voted and the count unchanged, so the user can
84
+ // simply press it again — this shell has no toast host of its own, and a
85
+ // failed helpful-vote costs nothing.
83
86
  }
84
87
  catch (_err) {
85
88
  void normalizeError(_err);
86
- // silently fail
87
89
  }
88
90
  finally {
89
91
  setVoting(false);
@@ -30,7 +30,15 @@ export declare function getPublicScammerById(id: string): Promise<ScammerDocumen
30
30
  * Returns null if not found or not verified.
31
31
  */
32
32
  export declare function getScammerProfilePageData(id: string): Promise<ScammerProfilePageData | null>;
33
- export type SellerTrustStatus = "clear" | "flagged";
33
+ /**
34
+ * `unknown` exists because `clear` is a POSITIVE SAFETY CLAIM — it renders as
35
+ * "✓ Verified Safe" next to a seller's name. Every read in
36
+ * `getSellerTrustStatus` used to fail open to `clear`, so a scam-registry
37
+ * outage actively endorsed a seller nobody had checked. Failing open is still
38
+ * right (an outage must not brand a legitimate seller a scammer), but the
39
+ * honest failure state is "we could not check", not "we checked and it's fine".
40
+ */
41
+ export type SellerTrustStatus = "clear" | "flagged" | "unknown";
34
42
  export interface SellerTrustResult {
35
43
  status: SellerTrustStatus;
36
44
  /** seoSlug of matched verified scammer profiles — links to /scams/[slug]. Empty when clear. */
@@ -1,6 +1,7 @@
1
1
  "use server";
2
2
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
3
  import { scammerRepository } from "../repository/scammer.repository";
4
+ import { safeRead } from "../../../errors/safe-read";
4
5
  import { userRepository } from "../../auth/repository/user.repository";
5
6
  import { storeRepository } from "../../stores/repository/store.repository";
6
7
  import { safeFireAndForget } from "../../../utils/safe-fire-forget";
@@ -39,9 +40,12 @@ export async function listVerifiedScammers(searchParams) {
39
40
  */
40
41
  export async function getPublicScammerById(id) {
41
42
  // Try direct id lookup first; fall back to seoSlug query.
42
- let doc = await scammerRepository.findById(id).catch(() => null);
43
+ // The profile IS the caller's subject, and both lookups already return null
44
+ // for "no such id/slug" — swallowing a read failure into that same null would
45
+ // report a real outage as a missing profile.
46
+ let doc = await scammerRepository.findById(id);
43
47
  if (!doc) {
44
- doc = await scammerRepository.findBySeoSlug(id).catch(() => null);
48
+ doc = await scammerRepository.findBySeoSlug(id);
45
49
  }
46
50
  if (!doc || doc.status !== "verified")
47
51
  return null;
@@ -66,27 +70,63 @@ export async function getScammerProfilePageData(id) {
66
70
  return { scammer, incidents, comments, relatedScammers, similarScamReports };
67
71
  }
68
72
  const TRUST_STATUS_CLEAR = { status: "clear", matchedProfileSlugs: [] };
73
+ const TRUST_STATUS_UNKNOWN = { status: "unknown", matchedProfileSlugs: [] };
74
+ /**
75
+ * Wrap a read so a FAILURE is distinguishable from a legitimate empty result.
76
+ *
77
+ * `safeRead` alone cannot express this: its fallback has the same type as a
78
+ * successful read, so `null` from a failed store lookup and `null` from a store
79
+ * that does not exist are the same value. Tagging the outcome is what lets the
80
+ * caller answer "unknown" instead of "clear".
81
+ */
82
+ async function readTagged(fn, key) {
83
+ return safeRead(async () => ({ ok: true, value: await fn() }), {
84
+ route: "sellerTrust",
85
+ key,
86
+ fallback: { ok: false, value: null },
87
+ });
88
+ }
69
89
  /**
70
90
  * P-12 — resolves a store owner's phone/email against the verified (admin-published)
71
91
  * scammer registry only. Never surfaces "under review" reports to buyers — a report
72
92
  * that hasn't been verified must not damage a seller's storefront reputation.
73
93
  */
74
94
  export async function getSellerTrustStatus(storeId) {
75
- const store = await storeRepository.findById(storeId).catch(() => null);
76
- if (!store?.ownerId)
95
+ // Every read below fails OPEN to "clear" — a deliberate choice, because a
96
+ // registry outage must not brand a legitimate seller a scammer. But "clear"
97
+ // is a positive safety claim, so a failure that produces one has to be
98
+ // recorded rather than inferred from a silent null.
99
+ const store = await readTagged(() => storeRepository.findById(storeId), "sellerTrust.store");
100
+ if (!store.ok)
101
+ return TRUST_STATUS_UNKNOWN;
102
+ if (!store.value?.ownerId)
77
103
  return TRUST_STATUS_CLEAR;
78
- const owner = await userRepository.findById(store.ownerId).catch(() => null);
79
- if (!owner)
104
+ const ownerId = store.value.ownerId;
105
+ const owner = await readTagged(() => userRepository.findById(ownerId), "sellerTrust.owner");
106
+ if (!owner.ok)
107
+ return TRUST_STATUS_UNKNOWN;
108
+ if (!owner.value)
80
109
  return TRUST_STATUS_CLEAR;
110
+ const ownerDoc = owner.value;
81
111
  const lookups = [];
82
- if (owner.phoneNumber)
83
- lookups.push(scammerRepository.findByContactField("phones", owner.phoneNumber));
84
- if (owner.email)
85
- lookups.push(scammerRepository.findByContactField("emails", owner.email));
112
+ if (ownerDoc.phoneNumber) {
113
+ const phone = ownerDoc.phoneNumber;
114
+ lookups.push(() => scammerRepository.findByContactField("phones", phone));
115
+ }
116
+ if (ownerDoc.email) {
117
+ const email = ownerDoc.email;
118
+ lookups.push(() => scammerRepository.findByContactField("emails", email));
119
+ }
86
120
  if (lookups.length === 0)
87
121
  return TRUST_STATUS_CLEAR;
88
- const results = await Promise.all(lookups.map((p) => p.catch(() => [])));
89
- const verified = results.flat().filter((s) => s.status === "verified");
122
+ const results = await Promise.all(lookups.map((fn) => readTagged(fn, "sellerTrust.registryLookup")));
123
+ // A partial answer is not an answer: if ANY registry lookup failed we cannot
124
+ // say the seller is clear, only that we could not check.
125
+ if (results.some((r) => !r.ok))
126
+ return TRUST_STATUS_UNKNOWN;
127
+ const verified = results
128
+ .flatMap((r) => r.value ?? [])
129
+ .filter((s) => s.status === "verified");
90
130
  if (verified.length === 0)
91
131
  return TRUST_STATUS_CLEAR;
92
132
  return {
@@ -13,4 +13,4 @@ export interface SellerTrustBadgeProps {
13
13
  * unrelated homepage-section `TrustBadge` config type in
14
14
  * features/homepage/index.ts (marketing trust-badge icons row).
15
15
  */
16
- export declare function SellerTrustBadge({ trust, className }: SellerTrustBadgeProps): import("react").JSX.Element;
16
+ export declare function SellerTrustBadge({ trust, className }: SellerTrustBadgeProps): import("react").JSX.Element | null;
@@ -13,6 +13,19 @@ import { ROUTES } from "../../../next";
13
13
  * features/homepage/index.ts (marketing trust-badge icons row).
14
14
  */
15
15
  export function SellerTrustBadge({ trust, className = "" }) {
16
+ // 🛑 `unknown` MUST be handled before the flagged fallthrough below, and must
17
+ // never render as "Verified Safe".
18
+ //
19
+ // Every read in getSellerTrustStatus used to fail open to "clear", so a
20
+ // scam-registry outage produced an explicit safety endorsement for a seller
21
+ // nobody had checked. Failing open is still right — an outage must not brand
22
+ // a legitimate seller a scammer — but the honest failure state is "we could
23
+ // not check", which is neither of the two claims this badge otherwise makes.
24
+ // Rendering nothing is deliberate: a neutral badge would still be a statement,
25
+ // and the absence of a badge already means "no verified match" everywhere the
26
+ // registry is not consulted.
27
+ if (trust.status === "unknown")
28
+ return null;
16
29
  if (trust.status === "clear") {
17
30
  return (_jsx(Badge, { variant: "success", className: className, children: "\u2713 Verified Safe" }));
18
31
  }
@@ -122,10 +122,11 @@ class ScammerRepository extends BaseRepository {
122
122
  await this.getCollection()
123
123
  .doc(id)
124
124
  .update({ [SCAMMER_FIELDS.VIEWS]: increment(1) });
125
+ // Fire-and-forget analytics: a lost view increment changes no rendered
126
+ // content, so it must never break the profile read it rides along with.
125
127
  }
126
128
  catch (_err) {
127
129
  void normalizeError(_err);
128
- // Fire-and-forget by design.
129
130
  }
130
131
  }
131
132
  /**
@@ -15,6 +15,7 @@ import { cartRepository } from "../../cart/repository/cart.repository";
15
15
  import { maskOfferForSeller } from "../../../security";
16
16
  import { ERROR_MESSAGES, AuthorizationError, ValidationError, NotFoundError, OFFER_ERROR_CODES, } from "../../../errors";
17
17
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
18
+ import { safeRead } from "../../../errors/safe-read";
18
19
  import { canMakeOffer } from "../../../_internal/shared/listing-types/capabilities";
19
20
  import { normalizeListingType } from "../../products/utils/listing-type";
20
21
  import { OfferStatusValues } from "../schemas";
@@ -36,7 +37,11 @@ export async function makeOffer(userId, userEmail, input) {
36
37
  * without stranding in-flight ones, so accept/decline/counter/withdraw and
37
38
  * `checkoutOffer` deliberately stay open.
38
39
  */
39
- const settings = await siteSettingsRepository.getSingleton().catch(() => null);
40
+ // Fails OPEN — a settings read that dies must not block offers site-wide. But
41
+ // `null` here is indistinguishable from "the flag is on", so it is recorded.
42
+ const settings = await safeRead(() => siteSettingsRepository.getSingleton(), {
43
+ route: "offers.makeOffer", key: "makeOffer.siteSettings", fallback: null,
44
+ });
40
45
  if (settings?.featureFlags?.offers === false) {
41
46
  throw new ValidationError(ERROR_MESSAGES.OFFER.DISABLED, {
42
47
  code: OFFER_ERROR_CODES.DISABLED,
@@ -37,12 +37,15 @@ export function FulfillmentView({ currentUserId, }) {
37
37
  setOrders(res.orders ?? []);
38
38
  }
39
39
  catch (err) {
40
- void normalizeError(err);
40
+ // An empty fulfilment queue is a normal, expected state, so a swallowed
41
+ // failure here reads as "nothing to pack" — matching the rest of this
42
+ // file, surface it instead.
43
+ showToast(normalizeError(err).message, "error");
41
44
  }
42
45
  finally {
43
46
  setIsLoadingOrders(false);
44
47
  }
45
- }, []);
48
+ }, [showToast]);
46
49
  useEffect(() => {
47
50
  void fetchOrders();
48
51
  }, [fetchOrders]);
@@ -22,7 +22,7 @@ export function PhysicalLocationModal({ count, onSave, onClose }) {
22
22
  }
23
23
  catch (e) {
24
24
  void normalizeError(e);
25
- setError(e instanceof Error ? e.message : "Failed to save location");
25
+ setError("Failed to save location");
26
26
  }
27
27
  finally {
28
28
  setSaving(false);
@@ -20,10 +20,12 @@ function ProductLabel({ product }) {
20
20
  fontSize: 12,
21
21
  margin: 8,
22
22
  });
23
+ // JsBarcode throws on a value CODE128 cannot encode. The <svg> is left
24
+ // empty and the label still prints its title and price — there is no
25
+ // recovery beyond fixing the barcodeId on the product itself.
23
26
  }
24
27
  catch (_err) {
25
28
  void normalizeError(_err);
26
- // Invalid barcode value — render nothing
27
29
  }
28
30
  }
29
31
  }, [product.barcodeId]);
@@ -88,7 +88,7 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
88
88
  }
89
89
  catch (err) {
90
90
  void normalizeError(err);
91
- showToast(err instanceof Error ? err.message : "Failed to cancel bids.", "error");
91
+ showToast("Failed to cancel bids.", "error");
92
92
  }
93
93
  }, [showToast]);
94
94
  const config = {
@@ -32,7 +32,7 @@ export function SellerCouponsView({ onCreateClick, onEditClick, onToggle, onDele
32
32
  }
33
33
  catch (err) {
34
34
  void normalizeError(err);
35
- showToast(err instanceof Error ? err.message : "Failed to update coupon.", "error");
35
+ showToast("Failed to update coupon.", "error");
36
36
  }
37
37
  finally {
38
38
  setTogglingId(null);
@@ -76,7 +76,7 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
76
76
  catch (err) {
77
77
  void normalizeError(err);
78
78
  setSaveMessage({ text: "Save failed. Please try again.", ok: false });
79
- showToast(err instanceof Error ? err.message : "Failed to save settings.", "error");
79
+ showToast("Failed to save settings.", "error");
80
80
  }
81
81
  finally {
82
82
  setSaving(false);
@@ -101,7 +101,7 @@ export function SellerGoogleReviewsView({ onSave, onSync, }) {
101
101
  catch (err) {
102
102
  void normalizeError(err);
103
103
  setSaveMessage({ text: "Sync failed. Please try again.", ok: false });
104
- showToast(err instanceof Error ? err.message : "Failed to sync reviews.", "error");
104
+ showToast("Failed to sync reviews.", "error");
105
105
  }
106
106
  finally {
107
107
  setSyncing(false);
@@ -210,7 +210,7 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
210
210
  }
211
211
  catch (err) {
212
212
  void normalizeError(err);
213
- showToast(err instanceof Error ? err.message : "Failed to mark order shipped.", "error");
213
+ showToast("Failed to mark order shipped.", "error");
214
214
  }
215
215
  finally {
216
216
  setShippingRowId(null);
@@ -232,7 +232,7 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
232
232
  }
233
233
  catch (err) {
234
234
  void normalizeError(err);
235
- showToast(err instanceof Error ? err.message : "Failed to update location.", "error");
235
+ showToast("Failed to update location.", "error");
236
236
  }
237
237
  }, [showToast]);
238
238
  const requestPayoutForSelection = useCallback(async (selection) => {
@@ -253,7 +253,7 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
253
253
  }
254
254
  catch (err) {
255
255
  void normalizeError(err);
256
- showToast(err instanceof Error ? err.message : "Failed to request payout.", "error");
256
+ showToast("Failed to request payout.", "error");
257
257
  }
258
258
  }, [showToast]);
259
259
  const [selectedIdsForLocation, setSelectedIdsForLocation] = useState([]);
@@ -57,7 +57,7 @@ export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete,
57
57
  }
58
58
  catch (err) {
59
59
  void normalizeError(err);
60
- showToast(err instanceof Error ? err.message : "Failed to delete payout method", "error");
60
+ showToast("Failed to delete payout method", "error");
61
61
  }
62
62
  setDeleteTargetId(null);
63
63
  }, [performDelete, showToast]);
@@ -79,7 +79,7 @@ export function SellerPayoutMethodsView({ onCreateClick, onEditClick, onDelete,
79
79
  }
80
80
  catch (err) {
81
81
  void normalizeError(err);
82
- showToast(err instanceof Error ? err.message : "Failed to update default.", "error");
82
+ showToast("Failed to update default.", "error");
83
83
  }
84
84
  finally {
85
85
  setSettingDefaultId(null);