@mohasinac/appkit 4.30.17 → 4.30.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
  4. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
  5. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
  6. package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
  7. package/dist/_internal/server/features/auctions/actions.js +14 -3
  8. package/dist/_internal/server/features/auctions/service.js +1 -1
  9. package/dist/_internal/server/features/blog/data.js +2 -2
  10. package/dist/_internal/server/features/blog/service.js +3 -3
  11. package/dist/_internal/server/features/brands/data.js +1 -1
  12. package/dist/_internal/server/features/brands/service.js +3 -3
  13. package/dist/_internal/server/features/bundles/actions.js +6 -1
  14. package/dist/_internal/server/features/bundles/data.js +21 -4
  15. package/dist/_internal/server/features/cart/data.js +3 -1
  16. package/dist/_internal/server/features/cart/service.js +13 -2
  17. package/dist/_internal/server/features/categories/data.js +12 -3
  18. package/dist/_internal/server/features/events/actions.js +17 -4
  19. package/dist/_internal/server/features/events/data.js +1 -1
  20. package/dist/_internal/server/features/events/service.js +1 -1
  21. package/dist/_internal/server/features/grouped/data.js +7 -2
  22. package/dist/_internal/server/features/item-requests/data.js +1 -1
  23. package/dist/_internal/server/features/lottery/actions.js +2 -2
  24. package/dist/_internal/server/features/lottery/data.js +1 -1
  25. package/dist/_internal/server/features/orders/actions.js +7 -7
  26. package/dist/_internal/server/features/orders/data.d.ts +1 -1
  27. package/dist/_internal/server/features/orders/data.js +14 -2
  28. package/dist/_internal/server/features/orders/service.js +1 -1
  29. package/dist/_internal/server/features/pre-orders/service.js +1 -1
  30. package/dist/_internal/server/features/prize-draws/actions.js +1 -1
  31. package/dist/_internal/server/features/products/data.js +7 -2
  32. package/dist/_internal/server/features/products/service.js +1 -1
  33. package/dist/_internal/server/features/promotions/actions.js +1 -1
  34. package/dist/_internal/server/features/promotions/data.js +1 -1
  35. package/dist/_internal/server/features/promotions/service.js +1 -1
  36. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  37. package/dist/_internal/server/features/reviews/data.js +6 -1
  38. package/dist/_internal/server/features/reviews/og-data.js +1 -1
  39. package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
  40. package/dist/_internal/server/features/stores/data.js +1 -1
  41. package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
  42. package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
  44. package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
  45. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
  46. package/dist/client/api/useApiQuery.js +17 -6
  47. package/dist/client.d.ts +1 -0
  48. package/dist/client.js +3 -0
  49. package/dist/contracts/client-realtime.d.ts +6 -0
  50. package/dist/contracts/realtime-session.d.ts +12 -12
  51. package/dist/contracts/realtime-session.js +72 -77
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +1 -1
  54. package/dist/features/about/components/DeveloperView.js +7 -2
  55. package/dist/features/about/components/PolicyPageView.js +9 -1
  56. package/dist/features/about/components/PublicProfileView.js +23 -5
  57. package/dist/features/account/components/AddressForm.js +1 -1
  58. package/dist/features/account/components/NotificationBell.js +4 -3
  59. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  60. package/dist/features/account/components/UserOffersPanel.js +1 -1
  61. package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
  62. package/dist/features/admin/components/AdminMediaView.js +3 -3
  63. package/dist/features/analytics/types.d.ts +1 -1
  64. package/dist/features/analytics/types.js +13 -0
  65. package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
  66. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
  67. package/dist/features/auth/actions/profile-actions.js +4 -3
  68. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  69. package/dist/features/auth/components/LoginForm.js +1 -1
  70. package/dist/features/auth/components/RegisterForm.js +1 -1
  71. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  72. package/dist/features/auth/hooks/useAuth.js +3 -0
  73. package/dist/features/blog/repository/blog.repository.js +3 -1
  74. package/dist/features/cart/actions/cart-actions.js +13 -4
  75. package/dist/features/cart/utils/pending-ops.js +1 -1
  76. package/dist/features/categories/components/BrandDetailPageView.js +28 -12
  77. package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
  78. package/dist/features/categories/repository/categories.repository.js +3 -1
  79. package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
  80. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
  82. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
  83. package/dist/features/events/components/EventBanner.js +5 -2
  84. package/dist/features/events/components/EventOfferCard.js +3 -1
  85. package/dist/features/events/components/EventPollWidget.js +1 -1
  86. package/dist/features/events/components/SpinWheelView.js +3 -2
  87. package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
  88. package/dist/features/history/hooks/useHistory.js +29 -27
  89. package/dist/features/homepage/components/AdSlot.js +3 -1
  90. package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
  91. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
  92. package/dist/features/homepage/components/SocialFeedSection.js +4 -1
  93. package/dist/features/homepage/lib/live-stats.js +8 -1
  94. package/dist/features/homepage/repository/carousel.repository.js +2 -1
  95. package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
  96. package/dist/features/media/finalize.js +3 -1
  97. package/dist/features/media/modals/ImageEditor.js +1 -1
  98. package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
  99. package/dist/features/media/modals/VideoTrimModal.js +5 -2
  100. package/dist/features/media/upload/MediaUploadField.js +2 -2
  101. package/dist/features/messages/hooks/useConversation.js +18 -4
  102. package/dist/features/messages/hooks/useConversations.js +25 -8
  103. package/dist/features/orders/components/RefundRequestView.js +5 -2
  104. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
  105. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +8 -2
  109. package/dist/features/products/components/ShareButton.js +3 -1
  110. package/dist/features/products/repository/loadProductFeatures.js +10 -2
  111. package/dist/features/products/repository/products.repository.js +2 -1
  112. package/dist/features/promotions/components/CouponCard.js +3 -1
  113. package/dist/features/reviews/actions/review-actions.js +8 -1
  114. package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
  115. package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
  116. package/dist/features/scams/actions/scam-actions.d.ts +9 -1
  117. package/dist/features/scams/actions/scam-actions.js +52 -12
  118. package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
  119. package/dist/features/scams/components/SellerTrustBadge.js +13 -0
  120. package/dist/features/scams/repository/scammer.repository.js +2 -1
  121. package/dist/features/seller/actions/offer-actions.js +6 -1
  122. package/dist/features/seller/components/FulfillmentView.js +5 -2
  123. package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
  124. package/dist/features/seller/components/PrintCenterView.js +3 -1
  125. package/dist/features/seller/components/SellerBidsView.js +1 -1
  126. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  127. package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
  128. package/dist/features/seller/components/SellerOrdersView.js +3 -3
  129. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  130. package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
  131. package/dist/features/seller/components/SellerProductShell.js +15 -5
  132. package/dist/features/seller/components/SellerProductsView.js +24 -10
  133. package/dist/features/seller/components/SellerReviewsView.js +47 -16
  134. package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
  135. package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
  136. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  137. package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
  138. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
  139. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  140. package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.js +5 -2
  143. package/dist/next/api/routeHandler.js +3 -1
  144. package/dist/providers/auth-firebase/session.js +3 -1
  145. package/dist/providers/db-firebase/admin-app-lite.js +8 -8
  146. package/dist/providers/db-firebase/admin.js +12 -12
  147. package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
  148. package/dist/providers/db-firebase/browser-stub.js +6 -11
  149. package/dist/providers/db-firebase/index.d.ts +0 -1
  150. package/dist/providers/db-firebase/index.js +0 -1
  151. package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
  152. package/dist/providers/db-firebase/rtdb-paths.js +15 -6
  153. package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
  154. package/dist/providers/db-firebase/rtdb-url.js +50 -0
  155. package/dist/providers/firebase-client/realtime.d.ts +20 -0
  156. package/dist/providers/firebase-client/realtime.js +34 -1
  157. package/dist/providers.d.ts +1 -1
  158. package/dist/providers.js +1 -1
  159. package/dist/react/hooks/use-action-dispatch.js +8 -2
  160. package/dist/react/hooks/useFormStatePreservation.js +3 -1
  161. package/dist/react/hooks/useRealtimeEvent.js +20 -9
  162. package/dist/ui/components/toolbar-collapse.js +1 -2
  163. package/package.json +1 -1
@@ -1,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);
@@ -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