@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.
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
- package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
- package/dist/_internal/server/features/auctions/actions.js +14 -3
- package/dist/_internal/server/features/auctions/service.js +1 -1
- package/dist/_internal/server/features/blog/data.js +2 -2
- package/dist/_internal/server/features/blog/service.js +3 -3
- package/dist/_internal/server/features/brands/data.js +1 -1
- package/dist/_internal/server/features/brands/service.js +3 -3
- package/dist/_internal/server/features/bundles/actions.js +6 -1
- package/dist/_internal/server/features/bundles/data.js +21 -4
- package/dist/_internal/server/features/cart/data.js +3 -1
- package/dist/_internal/server/features/cart/service.js +13 -2
- package/dist/_internal/server/features/categories/data.js +12 -3
- package/dist/_internal/server/features/events/actions.js +17 -4
- package/dist/_internal/server/features/events/data.js +1 -1
- package/dist/_internal/server/features/events/service.js +1 -1
- package/dist/_internal/server/features/grouped/data.js +7 -2
- package/dist/_internal/server/features/item-requests/data.js +1 -1
- package/dist/_internal/server/features/lottery/actions.js +2 -2
- package/dist/_internal/server/features/lottery/data.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +7 -7
- package/dist/_internal/server/features/orders/data.d.ts +1 -1
- package/dist/_internal/server/features/orders/data.js +14 -2
- package/dist/_internal/server/features/orders/service.js +1 -1
- package/dist/_internal/server/features/pre-orders/service.js +1 -1
- package/dist/_internal/server/features/prize-draws/actions.js +1 -1
- package/dist/_internal/server/features/products/data.js +7 -2
- package/dist/_internal/server/features/products/service.js +1 -1
- package/dist/_internal/server/features/promotions/actions.js +1 -1
- package/dist/_internal/server/features/promotions/data.js +1 -1
- package/dist/_internal/server/features/promotions/service.js +1 -1
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +6 -1
- package/dist/_internal/server/features/reviews/og-data.js +1 -1
- package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
- package/dist/_internal/server/features/stores/data.js +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
- package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
- package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
- package/dist/client/api/useApiQuery.js +17 -6
- package/dist/client.d.ts +1 -0
- package/dist/client.js +3 -0
- package/dist/contracts/client-realtime.d.ts +6 -0
- package/dist/contracts/realtime-session.d.ts +12 -12
- package/dist/contracts/realtime-session.js +72 -77
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/features/about/components/DeveloperView.js +7 -2
- package/dist/features/about/components/PolicyPageView.js +9 -1
- package/dist/features/about/components/PublicProfileView.js +23 -5
- package/dist/features/account/components/AddressForm.js +1 -1
- package/dist/features/account/components/NotificationBell.js +4 -3
- package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
- package/dist/features/account/components/UserOffersPanel.js +1 -1
- package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
- package/dist/features/admin/components/AdminMediaView.js +3 -3
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +13 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
- package/dist/features/auth/actions/profile-actions.js +4 -3
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/hooks/useAuth.js +3 -0
- package/dist/features/blog/repository/blog.repository.js +3 -1
- package/dist/features/cart/actions/cart-actions.js +13 -4
- package/dist/features/cart/utils/pending-ops.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +28 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
- package/dist/features/categories/repository/categories.repository.js +3 -1
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
- package/dist/features/events/components/EventBanner.js +5 -2
- package/dist/features/events/components/EventOfferCard.js +3 -1
- package/dist/features/events/components/EventPollWidget.js +1 -1
- package/dist/features/events/components/SpinWheelView.js +3 -2
- package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
- package/dist/features/history/hooks/useHistory.js +29 -27
- package/dist/features/homepage/components/AdSlot.js +3 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
- package/dist/features/homepage/components/SocialFeedSection.js +4 -1
- package/dist/features/homepage/lib/live-stats.js +8 -1
- package/dist/features/homepage/repository/carousel.repository.js +2 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
- package/dist/features/media/finalize.js +3 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
- package/dist/features/media/modals/VideoTrimModal.js +5 -2
- package/dist/features/media/upload/MediaUploadField.js +2 -2
- package/dist/features/messages/hooks/useConversation.js +18 -4
- package/dist/features/messages/hooks/useConversations.js +25 -8
- package/dist/features/orders/components/RefundRequestView.js +5 -2
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
- package/dist/features/products/components/ProductDetailPageView.js +8 -2
- package/dist/features/products/components/ShareButton.js +3 -1
- package/dist/features/products/repository/loadProductFeatures.js +10 -2
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -1
- package/dist/features/reviews/actions/review-actions.js +8 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
- package/dist/features/scams/actions/scam-actions.d.ts +9 -1
- package/dist/features/scams/actions/scam-actions.js +52 -12
- package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
- package/dist/features/scams/components/SellerTrustBadge.js +13 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +6 -1
- package/dist/features/seller/components/FulfillmentView.js +5 -2
- package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
- package/dist/features/seller/components/PrintCenterView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +1 -1
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +3 -3
- package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
- package/dist/features/seller/components/SellerProductShell.js +15 -5
- package/dist/features/seller/components/SellerProductsView.js +24 -10
- package/dist/features/seller/components/SellerReviewsView.js +47 -16
- package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -2
- package/dist/next/api/routeHandler.js +3 -1
- package/dist/providers/auth-firebase/session.js +3 -1
- package/dist/providers/db-firebase/admin-app-lite.js +8 -8
- package/dist/providers/db-firebase/admin.js +12 -12
- package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
- package/dist/providers/db-firebase/browser-stub.js +6 -11
- package/dist/providers/db-firebase/index.d.ts +0 -1
- package/dist/providers/db-firebase/index.js +0 -1
- package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
- package/dist/providers/db-firebase/rtdb-paths.js +15 -6
- package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
- package/dist/providers/db-firebase/rtdb-url.js +50 -0
- package/dist/providers/firebase-client/realtime.d.ts +20 -0
- package/dist/providers/firebase-client/realtime.js +34 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/react/hooks/use-action-dispatch.js +8 -2
- package/dist/react/hooks/useFormStatePreservation.js +3 -1
- package/dist/react/hooks/useRealtimeEvent.js +20 -9
- package/dist/ui/components/toolbar-collapse.js +1 -2
- package/package.json +1 -1
|
@@ -480,10 +480,12 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
480
480
|
.collection(this.collection)
|
|
481
481
|
.doc(categoryId)
|
|
482
482
|
.update({ [CATEGORY_FIELDS.VIEW_COUNT]: increment(1) });
|
|
483
|
+
// Fire-and-forget analytics: a lost view increment changes nothing that
|
|
484
|
+
// renders, so it must never break the category read path it rides along
|
|
485
|
+
// with — the count is approximate by design.
|
|
483
486
|
}
|
|
484
487
|
catch (_err) {
|
|
485
488
|
void normalizeError(_err);
|
|
486
|
-
// View analytics must not break category read path.
|
|
487
489
|
}
|
|
488
490
|
}
|
|
489
491
|
generateSublistingId(name) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { timingSafeEqual } from "crypto";
|
|
10
10
|
import { ValidationError } from "../../../errors";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
12
13
|
import { serverLogger } from "../../../monitoring";
|
|
13
14
|
import { sendEmail } from "../../contact/email";
|
|
14
15
|
import { getAdminDb } from "../../../providers/db-firebase";
|
|
@@ -47,7 +48,15 @@ export async function sendCheckoutValueOtp(userId, userEmail, channel = "email",
|
|
|
47
48
|
if (!addressId) {
|
|
48
49
|
throw new ValidationError("Select a delivery address before sending the code via WhatsApp.");
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
// A failed settings read makes the WhatsApp channel look deliberately
|
|
52
|
+
// switched off ("send the code by email instead") — a defensible fallback,
|
|
53
|
+
// but one an operator has to be able to see, since it is indistinguishable
|
|
54
|
+
// from an admin having disabled it.
|
|
55
|
+
const settings = await safeRead(() => siteSettingsRepository.getSingleton(), {
|
|
56
|
+
route: "checkout/value-otp",
|
|
57
|
+
key: "checkoutValueOtp.siteSettings",
|
|
58
|
+
fallback: null,
|
|
59
|
+
});
|
|
51
60
|
const creds = settings?.credentials ?? {};
|
|
52
61
|
const whatsappOtpEnabled = settings?.notificationChannels?.whatsapp?.otpEnabled === true;
|
|
53
62
|
const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
|
|
@@ -36,7 +36,7 @@ export function ClassifiedContactSellerPanel({ productId, onContactSeller }) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
|
38
38
|
void normalizeError(e);
|
|
39
|
-
setError(
|
|
39
|
+
setError("Something went wrong");
|
|
40
40
|
}
|
|
41
41
|
finally {
|
|
42
42
|
setPending(false);
|
|
@@ -22,6 +22,7 @@ import { GroupedListingsCarousel } from "../../grouped/components/GroupedListing
|
|
|
22
22
|
import { getGroupsWithItemsForProduct } from "../../../_internal/server/features/grouped/data";
|
|
23
23
|
import { ClassifiedContactSellerPanel } from "./ClassifiedContactSellerPanel";
|
|
24
24
|
import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
|
|
25
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
25
26
|
import { canMakeOffer } from "../../../_internal/shared/listing-types/capabilities";
|
|
26
27
|
import { DEFAULT_MIN_OFFER_PERCENT } from "../../../_internal/shared/features/offers/config";
|
|
27
28
|
import { ListingBottomActions } from "../../products/components/ListingBottomActions";
|
|
@@ -34,7 +35,10 @@ function toDescriptionHtml(raw) {
|
|
|
34
35
|
export async function ClassifiedDetailPageView({ slug, initialProduct, renderOfferAction }) {
|
|
35
36
|
const product = initialProduct !== undefined
|
|
36
37
|
? (initialProduct ?? undefined)
|
|
37
|
-
|
|
38
|
+
// The listing IS this page's subject — a read failure must surface rather
|
|
39
|
+
// than become the same `undefined` a removed listing produces, which the
|
|
40
|
+
// "Not Found" branch below already handles.
|
|
41
|
+
: await getClassifiedForDetail(slug);
|
|
38
42
|
if (!product) {
|
|
39
43
|
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: "Classified Listing Not Found" }), _jsx(Text, { color: "muted", children: "The classified listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.CLASSIFIED), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Classifieds" })] }) }) }) }));
|
|
40
44
|
}
|
|
@@ -58,7 +62,11 @@ export async function ClassifiedDetailPageView({ slug, initialProduct, renderOff
|
|
|
58
62
|
const [{ relatedItems, relatedByBrand, relatedByTags, relatedByStore }, groups, siteSettings] = await Promise.all([
|
|
59
63
|
computeRelatedItems(product),
|
|
60
64
|
getGroupsWithItemsForProduct(product.id),
|
|
61
|
-
|
|
65
|
+
safeRead(() => getSiteSettingsGlobal(), {
|
|
66
|
+
route: "/classified/[slug]",
|
|
67
|
+
key: "classified.siteSettings",
|
|
68
|
+
fallback: null,
|
|
69
|
+
}),
|
|
62
70
|
]);
|
|
63
71
|
// Classifieds are the one non-standard type where haggling is the norm, so
|
|
64
72
|
// `canMakeOffer` is true for them — but the seller still opts in per listing
|
|
@@ -30,7 +30,10 @@ function toDescriptionHtml(raw) {
|
|
|
30
30
|
export async function DigitalCodeDetailPageView({ slug, initialProduct, renderPrimaryActions }) {
|
|
31
31
|
const product = initialProduct !== undefined
|
|
32
32
|
? (initialProduct ?? undefined)
|
|
33
|
-
|
|
33
|
+
// The listing IS this page's subject — a read failure must surface rather
|
|
34
|
+
// than become the same `undefined` a removed listing produces, which the
|
|
35
|
+
// "Not Found" branch below already handles.
|
|
36
|
+
: await getDigitalCodeForDetail(slug);
|
|
34
37
|
if (!product) {
|
|
35
38
|
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: "Digital Code Listing Not Found" }), _jsx(Text, { color: "muted", children: "The digital code listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.DIGITAL_CODES), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Digital Codes" })] }) }) }) }));
|
|
36
39
|
}
|
|
@@ -13,10 +13,12 @@ export function EventBanner({ storageKey = "dismissed_event_banners", className
|
|
|
13
13
|
const stored = sessionStorage.getItem(storageKey);
|
|
14
14
|
if (stored)
|
|
15
15
|
setDismissedIds(JSON.parse(stored));
|
|
16
|
+
// sessionStorage is unavailable in private mode and over quota in some
|
|
17
|
+
// embedded browsers. Losing the dismissal list only means the banner
|
|
18
|
+
// reappears — nothing an operator can act on, and nothing the user loses.
|
|
16
19
|
}
|
|
17
20
|
catch (_err) {
|
|
18
21
|
void normalizeError(_err);
|
|
19
|
-
// ignore session storage failures
|
|
20
22
|
}
|
|
21
23
|
setMounted(true);
|
|
22
24
|
}, [storageKey]);
|
|
@@ -38,10 +40,11 @@ export function EventBanner({ storageKey = "dismissed_event_banners", className
|
|
|
38
40
|
setDismissedIds(next);
|
|
39
41
|
try {
|
|
40
42
|
sessionStorage.setItem(storageKey, JSON.stringify(next));
|
|
43
|
+
// Same as the read above: the banner is already hidden in local state, so
|
|
44
|
+
// a failed persist only means it returns on the next page load.
|
|
41
45
|
}
|
|
42
46
|
catch (_err) {
|
|
43
47
|
void normalizeError(_err);
|
|
44
|
-
// ignore
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
50
|
return (_jsxs(Section, { tone: "accent-banner", className: [
|
|
@@ -24,10 +24,12 @@ export function EventOfferCard({ couponCode, offerDescription, title = "Exclusiv
|
|
|
24
24
|
await navigator.clipboard.writeText(couponCode);
|
|
25
25
|
setCopied(true);
|
|
26
26
|
setTimeout(() => setCopied(false), 2000);
|
|
27
|
+
// Clipboard access is denied outright on insecure origins and by user
|
|
28
|
+
// permission. The button simply never flips to "Copied", and the code is
|
|
29
|
+
// still visible on screen to select by hand.
|
|
27
30
|
}
|
|
28
31
|
catch (_err) {
|
|
29
32
|
void normalizeError(_err);
|
|
30
|
-
/* clipboard unavailable — leave button as-is */
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
35
|
return (_jsx(Div, { className: `${__P.p6}`, rounded: "2xl", border: "default", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, weight: "semibold", size: "2xl", children: title }), offerDescription ? (_jsx(Text, { size: "sm", color: "muted", children: offerDescription })) : null] }), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", gap: "3", rounded: "lg", children: [_jsx(Code, { className: "flex-1", weight: "bold", size: "base", color: "primary", children: couponCode }), _jsx(Button, { type: "button", variant: "outline", onClick: copyCode, children: copied ? "Copied!" : "Copy code" })] }), _jsx(Div, { children: _jsx(Link, { href: href, children: _jsx(Button, { variant: "primary", children: "Apply at checkout" }) }) })] }) }));
|
|
@@ -53,7 +53,7 @@ export function EventPollWidget({ eventId, pollConfig, eventStatus, totalEntries
|
|
|
53
53
|
}
|
|
54
54
|
catch (err) {
|
|
55
55
|
void normalizeError(err);
|
|
56
|
-
setError(
|
|
56
|
+
setError("Something went wrong");
|
|
57
57
|
}
|
|
58
58
|
finally {
|
|
59
59
|
setIsLoading(false);
|
|
@@ -4,6 +4,7 @@ import { useCallback, useMemo, useState } from "react";
|
|
|
4
4
|
import { Button, ClaimCouponButton, Div, Heading, LoginRequiredModal, Row, Span, Stack, Text, useToast } from "../../../ui";
|
|
5
5
|
import { isAuthError } from "../../../utils/auth-error";
|
|
6
6
|
import { normalizeError } from "../../../errors/normalize";
|
|
7
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
7
8
|
const CLS_WHEEL = "relative mx-auto aspect-square w-64 overflow-hidden rounded-full border-4 border-warning bg-[image:var(--appkit-gradient-promotion)]";
|
|
8
9
|
// audit-content-alignment-ok: self-contained circular spin-wheel game widget, all text centered under the wheel by design
|
|
9
10
|
const CLS_PRIZE_BOX = "rounded-xl border border-success bg-success-surface dark:border-success p-[var(--appkit-space-4)] text-center";
|
|
@@ -55,14 +56,14 @@ export function SpinWheelView({ eventId, prizes, alreadyUsed, initialPrizeId, in
|
|
|
55
56
|
}, ANIMATION_MS);
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
|
-
|
|
59
|
+
const e = normalizeError(err);
|
|
59
60
|
setSpinning(false);
|
|
60
61
|
if (isAuthError(err)) {
|
|
61
62
|
setShowLoginModal(true);
|
|
62
63
|
}
|
|
63
64
|
else {
|
|
64
65
|
setError(l.errorFallback);
|
|
65
|
-
showToast(
|
|
66
|
+
showToast(toUserMessage(e.code, undefined, { fallback: l.errorFallback }), "error");
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}, [disabled, eventId, l.errorFallback, onSpin, showToast]);
|
|
@@ -46,6 +46,7 @@ import { apiClient } from "../../../http";
|
|
|
46
46
|
import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
47
47
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
48
48
|
import { normalizeError } from "../../../errors/normalize";
|
|
49
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
49
50
|
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
50
51
|
import { groupedListingFormSchema, } from "../schemas/grouped-listing-form";
|
|
51
52
|
const EMPTY = {
|
|
@@ -173,7 +174,7 @@ export function GroupedListingEditorView({ scope, groupId, fixedStoreId, onSaved
|
|
|
173
174
|
}
|
|
174
175
|
catch (err) {
|
|
175
176
|
const e = normalizeError(err);
|
|
176
|
-
showToast(e.
|
|
177
|
+
showToast(toUserMessage(e.code, undefined, { fallback: "Failed to save the group." }), "error");
|
|
177
178
|
}
|
|
178
179
|
finally {
|
|
179
180
|
setSaving(false);
|
|
@@ -38,35 +38,28 @@ async function postAuthTrack(args) {
|
|
|
38
38
|
headers: { "Content-Type": "application/json" },
|
|
39
39
|
body: JSON.stringify(args),
|
|
40
40
|
});
|
|
41
|
+
// Recently-viewed tracking is a side effect of browsing, fired from a
|
|
42
|
+
// debounced timer with no UI attached. A lost entry costs the user nothing
|
|
43
|
+
// and there is no caller in a position to retry or report it.
|
|
41
44
|
}
|
|
42
45
|
catch (_err) {
|
|
43
46
|
void normalizeError(_err);
|
|
44
|
-
// best-effort tracking
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
// Both deletes below are user-initiated and deliberately let failures throw:
|
|
50
|
+
// swallowing them made `remove`/`clear` report success for a DELETE that never
|
|
51
|
+
// landed, and the row would silently return on the next load.
|
|
47
52
|
async function deleteAuthItem(productId) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch (_err) {
|
|
55
|
-
void normalizeError(_err);
|
|
56
|
-
// ignore
|
|
57
|
-
}
|
|
53
|
+
await fetch(ACCOUNT_ENDPOINTS.HISTORY_ITEM(productId), {
|
|
54
|
+
method: "DELETE",
|
|
55
|
+
credentials: "include",
|
|
56
|
+
});
|
|
58
57
|
}
|
|
59
58
|
async function deleteAuthAll() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (_err) {
|
|
67
|
-
void normalizeError(_err);
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
59
|
+
await fetch(ACCOUNT_ENDPOINTS.HISTORY, {
|
|
60
|
+
method: "DELETE",
|
|
61
|
+
credentials: "include",
|
|
62
|
+
});
|
|
70
63
|
}
|
|
71
64
|
export function useHistory(userId) {
|
|
72
65
|
const isAuth = !!userId;
|
|
@@ -81,6 +74,9 @@ export function useHistory(userId) {
|
|
|
81
74
|
else {
|
|
82
75
|
setItems(getGuestHistory());
|
|
83
76
|
}
|
|
77
|
+
// fetchAuthHistory already degrades to [] on its own, and getGuestHistory
|
|
78
|
+
// is a localStorage read that fails only in private mode — either way the
|
|
79
|
+
// list stays as it was, which is the correct visible outcome here.
|
|
84
80
|
}
|
|
85
81
|
catch (err) {
|
|
86
82
|
void normalizeError(err);
|
|
@@ -131,14 +127,20 @@ export function useHistory(userId) {
|
|
|
131
127
|
trackTimers.current.set(key, timer);
|
|
132
128
|
}, [isAuth]);
|
|
133
129
|
const remove = useCallback(async (productId) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
try {
|
|
131
|
+
if (isAuth) {
|
|
132
|
+
await deleteAuthItem(productId);
|
|
133
|
+
await loadItems();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
setItems(removeGuestHistoryItem(productId));
|
|
137
|
+
}
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
catch (_err) {
|
|
140
|
+
void normalizeError(_err);
|
|
141
|
+
showToast("Failed to remove this item from history.", "error");
|
|
140
142
|
}
|
|
141
|
-
}, [isAuth, loadItems]);
|
|
143
|
+
}, [isAuth, loadItems, showToast]);
|
|
142
144
|
const clear = useCallback(async () => {
|
|
143
145
|
try {
|
|
144
146
|
if (isAuth) {
|
|
@@ -16,10 +16,12 @@ function AdsenseAd({ config }) {
|
|
|
16
16
|
return;
|
|
17
17
|
try {
|
|
18
18
|
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
|
19
|
+
// adsbygoogle is injected by a third-party script that ad blockers and
|
|
20
|
+
// consent walls routinely prevent from loading. The <ins> below simply
|
|
21
|
+
// stays unfilled — there is no failure the site can act on.
|
|
19
22
|
}
|
|
20
23
|
catch (_err) {
|
|
21
24
|
void normalizeError(_err);
|
|
22
|
-
// AdSense not loaded yet — no-op
|
|
23
25
|
}
|
|
24
26
|
}, []);
|
|
25
27
|
return (_jsx(Div, { ref: ref, children: _jsx("ins", { className: "adsbygoogle block", "data-ad-client": config.adsenseClient ?? "", "data-ad-slot": config.adsenseSlot ?? "", "data-ad-format": config.adsenseFormat ?? "auto", "data-full-width-responsive": "true" }) }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
+
import { serverLogger } from "../../../monitoring/server-logger";
|
|
3
4
|
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
|
|
4
5
|
import { MediaImage } from "../../media/MediaImage";
|
|
5
6
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
@@ -60,8 +61,13 @@ export async function GoogleReviewsSection(config) {
|
|
|
60
61
|
totalRatings = result.totalRatings;
|
|
61
62
|
}
|
|
62
63
|
catch (_err) {
|
|
63
|
-
|
|
64
|
-
//
|
|
64
|
+
// The section renders its empty state below, which is indistinguishable
|
|
65
|
+
// from a place that genuinely has no reviews — and Google Places calls are
|
|
66
|
+
// billed, so a persistent failure is worth an operator seeing.
|
|
67
|
+
serverLogger.warn("homepage.googleReviews: Places fetch failed", {
|
|
68
|
+
placeId,
|
|
69
|
+
error: normalizeError(_err).message,
|
|
70
|
+
});
|
|
65
71
|
}
|
|
66
72
|
const gridClass = layout === "carousel"
|
|
67
73
|
? "flex gap-[var(--appkit-space-4)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
|
|
@@ -3,6 +3,7 @@ import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
|
3
3
|
import { sortBy } from "@mohasinac/appkit";
|
|
4
4
|
import { Div, Main } from "../../../ui";
|
|
5
5
|
import { carouselRepository, faqsRepository, siteSettingsRepository } from "../../../repositories";
|
|
6
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
6
7
|
import { fetchLiveStats } from "../lib/live-stats";
|
|
7
8
|
import { renderSection, AnnouncementBar } from "../lib/section-renderer";
|
|
8
9
|
import { homepageSectionsRepository } from "../repository/homepage-sections.repository";
|
|
@@ -28,14 +29,25 @@ function toCarouselSlides(docs) {
|
|
|
28
29
|
}));
|
|
29
30
|
}
|
|
30
31
|
export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onBannerDismiss, } = {}) {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Every read on this page is a RAIL: the homepage must still render when one
|
|
33
|
+
// fails, but a failure has to be distinguishable from an empty catalogue —
|
|
34
|
+
// that is exactly Root Cause #59, and safeRead is what records it.
|
|
35
|
+
const slides = await safeRead(() => carouselRepository.getActiveSlides(), {
|
|
36
|
+
route: "/", key: "homepage.carouselSlides", fallback: [],
|
|
37
|
+
});
|
|
38
|
+
const siteSettings = await safeRead(() => siteSettingsRepository.getSingleton(), {
|
|
39
|
+
route: "/", key: "homepage.siteSettings", fallback: null,
|
|
40
|
+
});
|
|
33
41
|
const announcementMessage = siteSettings?.announcementBar?.message?.trim() ||
|
|
34
42
|
"🎉 Up to 15% Off on Pokémon TCG this week — Use code SAVE15";
|
|
35
43
|
const showAnnouncement = siteSettings?.announcementBar?.enabled ?? true;
|
|
36
44
|
const [allSections, rawFaqItems] = await Promise.all([
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
safeRead(() => homepageSectionsRepository.getEnabledSections(), {
|
|
46
|
+
route: "/", key: "homepage.enabledSections", fallback: [],
|
|
47
|
+
}),
|
|
48
|
+
safeRead(() => faqsRepository.getHomepageFAQs(), {
|
|
49
|
+
route: "/", key: "homepage.faqs", fallback: [],
|
|
50
|
+
}),
|
|
39
51
|
]);
|
|
40
52
|
// Respect the feature flags. This gate existed but lived inside a function
|
|
41
53
|
// with no call sites, so until 2026-08-24 turning off `auctions` hid auctions
|
|
@@ -87,12 +99,24 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
87
99
|
activeTypes.has("products") ? getFeaturedProducts(12) : null,
|
|
88
100
|
activeTypes.has("auctions") ? getFeaturedAuctions(12) : null,
|
|
89
101
|
activeTypes.has("pre-orders") ? getFeaturedPreOrders(12) : null,
|
|
90
|
-
activeTypes.has("categories")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
activeTypes.has("
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
activeTypes.has("categories")
|
|
103
|
+
? safeRead(() => listTopLevelCategories(12), { route: "/", key: "homepage.categories", fallback: null })
|
|
104
|
+
: null,
|
|
105
|
+
activeTypes.has("brands")
|
|
106
|
+
? safeRead(() => listBrandCategories(12), { route: "/", key: "homepage.brands", fallback: null })
|
|
107
|
+
: null,
|
|
108
|
+
activeTypes.has("featured-bundles")
|
|
109
|
+
? safeRead(() => listFeaturedBundles(8), { route: "/", key: "homepage.featuredBundles", fallback: null })
|
|
110
|
+
: null,
|
|
111
|
+
activeTypes.has("stores")
|
|
112
|
+
? safeRead(() => listStores({ pageSize: 8, sorts: sortBy("averageRating", "DESC") }), { route: "/", key: "homepage.stores", fallback: null })
|
|
113
|
+
: null,
|
|
114
|
+
activeTypes.has("blog-articles")
|
|
115
|
+
? safeRead(() => getFeaturedBlogPosts(6), { route: "/", key: "homepage.blogPosts", fallback: null })
|
|
116
|
+
: null,
|
|
117
|
+
activeTypes.has("events")
|
|
118
|
+
? safeRead(() => listPublicEvents({ filters: sieveFilter("status", SIEVE_OP.EQ, "active"), pageSize: 6 }), { route: "/", key: "homepage.events", fallback: null })
|
|
119
|
+
: null,
|
|
96
120
|
]);
|
|
97
121
|
// ProductDocument / BlogDocument / EventDocument have Date fields and extra Firestore-only
|
|
98
122
|
// fields absent from the corresponding *Item types. The casts below are safe: section
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
3
4
|
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
|
|
4
5
|
import { SocialPostCard } from "./SocialPostCard";
|
|
5
6
|
import { fetchInstagramPosts, fetchFacebookPosts, fetchTikTokPosts, fetchDeviantArtPosts, } from "../lib/social-feed-fetcher";
|
|
@@ -47,7 +48,9 @@ async function loadPosts(config) {
|
|
|
47
48
|
if (!handle)
|
|
48
49
|
return { posts: [], error: `A handle is required for ${PLATFORM_LABELS[platform]}.` };
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
+
// A failed credentials read is reported below as "token not configured in
|
|
52
|
+
// Site Settings", which is actionable but wrong — record the real cause.
|
|
53
|
+
const credentials = await safeRead(() => siteSettingsRepository.getDecryptedCredentials(), { route: "/", key: "homepage.socialFeed.credentials", fallback: null });
|
|
51
54
|
switch (platform) {
|
|
52
55
|
case "instagram": {
|
|
53
56
|
const token = credentials?.metaPageAccessToken;
|
|
@@ -81,7 +81,14 @@ export async function fetchLiveStats(requests) {
|
|
|
81
81
|
}
|
|
82
82
|
catch (_err) {
|
|
83
83
|
void normalizeError(_err);
|
|
84
|
-
//
|
|
84
|
+
// Falling back to the admin-authored static value is correct — but a
|
|
85
|
+
// stale number that looks live is exactly the failure this must not
|
|
86
|
+
// hide, so name the collection that could not be counted.
|
|
87
|
+
serverLogger.warn("homepage.liveStats: collection count failed", {
|
|
88
|
+
statKey: key,
|
|
89
|
+
collection,
|
|
90
|
+
error: normalizeError(_err).message,
|
|
91
|
+
});
|
|
85
92
|
}
|
|
86
93
|
})());
|
|
87
94
|
}
|
|
@@ -234,10 +234,11 @@ export class CarouselRepository extends BaseRepository {
|
|
|
234
234
|
"stats.views": increment(1),
|
|
235
235
|
"stats.lastViewed": new Date(),
|
|
236
236
|
});
|
|
237
|
+
// Fire-and-forget analytics: a lost view increment changes nothing that
|
|
238
|
+
// renders, and it must never break the carousel read it rides along with.
|
|
237
239
|
}
|
|
238
240
|
catch (_e) {
|
|
239
241
|
void normalizeError(_e);
|
|
240
|
-
// Swallow errors — analytics failures must not break the carousel response
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
}
|
|
@@ -33,7 +33,10 @@ function toDescriptionHtml(raw) {
|
|
|
33
33
|
export async function LiveItemDetailPageView({ slug, initialProduct, renderActions }) {
|
|
34
34
|
const product = initialProduct !== undefined
|
|
35
35
|
? (initialProduct ?? undefined)
|
|
36
|
-
|
|
36
|
+
// The listing IS this page's subject — a read failure must surface rather
|
|
37
|
+
// than become the same `undefined` a removed listing produces, which the
|
|
38
|
+
// "Not Found" branch below already handles.
|
|
39
|
+
: await getLiveItemForDetail(slug);
|
|
37
40
|
if (!product) {
|
|
38
41
|
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: "Live Listing Not Found" }), _jsx(Text, { color: "muted", children: "The live listing you are looking for may have been removed." }), _jsx(Link, { href: String(ROUTES.PUBLIC.LIVE), className: "text-[length:var(--appkit-text-sm)] font-medium text-primary-600 hover:underline", children: "Browse Live Listings" })] }) }) }) }));
|
|
39
42
|
}
|
|
@@ -44,10 +44,12 @@ export async function finalizeStagedMediaUrl(url) {
|
|
|
44
44
|
const finalPath = await moveToFinalPath(asset.storagePath);
|
|
45
45
|
await mediaAssetsRepository.promoteToFinalized(shortId, finalPath);
|
|
46
46
|
}
|
|
47
|
+
// Non-fatal by construction: the returned URL is the same before and after
|
|
48
|
+
// the move, so a failed promotion leaves the asset in tmp/ and the proxy
|
|
49
|
+
// keeps serving it from there until the tmp-cleanup job reclaims it.
|
|
47
50
|
}
|
|
48
51
|
catch (_err) {
|
|
49
52
|
void normalizeError(_err);
|
|
50
|
-
// Non-fatal: URL is still valid; proxy will serve whatever storagePath points to.
|
|
51
53
|
}
|
|
52
54
|
return url; // URL is stable — same before and after the file move
|
|
53
55
|
}
|
|
@@ -63,7 +63,7 @@ export function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, ou
|
|
|
63
63
|
}
|
|
64
64
|
catch (err) {
|
|
65
65
|
void normalizeError(err);
|
|
66
|
-
showToast(
|
|
66
|
+
showToast("Failed to save image.", "error");
|
|
67
67
|
}
|
|
68
68
|
finally {
|
|
69
69
|
setSaving(false);
|
|
@@ -10,6 +10,7 @@ import { useState, useRef } from "react";
|
|
|
10
10
|
import { useTranslations } from "next-intl";
|
|
11
11
|
import { Alert, Button, Div, Modal, Row, Span, Spinner, Stack, Text } from "../../../ui";
|
|
12
12
|
import { normalizeError } from "../../../errors/normalize";
|
|
13
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
13
14
|
const __O = {
|
|
14
15
|
hidden: "overflow-hidden",
|
|
15
16
|
};
|
|
@@ -45,8 +46,10 @@ export function VideoThumbnailSelector({ isOpen, videoUrl, onClose, onSelect, on
|
|
|
45
46
|
onClose();
|
|
46
47
|
}
|
|
47
48
|
catch (err) {
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
const e = normalizeError(err);
|
|
50
|
+
// `t` is namespaced ("mediaEditor"), so it cannot resolve the absolute
|
|
51
|
+
// `errors.codes.*` keys — omit it and let the authored copy stand.
|
|
52
|
+
setError(toUserMessage(e.code, undefined, { fallback: t("thumbnailError") }));
|
|
50
53
|
}
|
|
51
54
|
finally {
|
|
52
55
|
setIsCapturing(false);
|
|
@@ -11,6 +11,7 @@ import { Alert, Button, Div, Modal, Row, Span, Stack, Text } from "../../../ui";
|
|
|
11
11
|
import { MediaSlider } from "../components/MediaSlider";
|
|
12
12
|
import { useMediaTrim } from "../hooks/useMedia";
|
|
13
13
|
import { normalizeError } from "../../../errors/normalize";
|
|
14
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
14
15
|
function formatTime(seconds) {
|
|
15
16
|
const m = Math.floor(seconds / 60);
|
|
16
17
|
const s = Math.floor(seconds % 60);
|
|
@@ -62,8 +63,10 @@ export function VideoTrimModal({ isOpen, videoUrl, onClose, onSave, endpoint, })
|
|
|
62
63
|
onClose();
|
|
63
64
|
}
|
|
64
65
|
catch (err) {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const e = normalizeError(err);
|
|
67
|
+
// `t` is namespaced ("mediaEditor"), so it cannot resolve the absolute
|
|
68
|
+
// `errors.codes.*` keys — omit it and let the authored copy stand.
|
|
69
|
+
setError(toUserMessage(e.code, undefined, { fallback: t("trimError") }));
|
|
67
70
|
}
|
|
68
71
|
};
|
|
69
72
|
const qualityLabels = {
|
|
@@ -320,7 +320,7 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
320
320
|
}
|
|
321
321
|
catch (err) {
|
|
322
322
|
void normalizeError(err);
|
|
323
|
-
setError(
|
|
323
|
+
setError("Upload failed");
|
|
324
324
|
}
|
|
325
325
|
finally {
|
|
326
326
|
setIsLoading(false);
|
|
@@ -362,7 +362,7 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
362
362
|
}
|
|
363
363
|
catch (err) {
|
|
364
364
|
void normalizeError(err);
|
|
365
|
-
setError(
|
|
365
|
+
setError("Upload failed");
|
|
366
366
|
}
|
|
367
367
|
finally {
|
|
368
368
|
setIsLoading(false);
|
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
* canonical Firestore message ordering wins.
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
|
-
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
14
13
|
import { acquireRealtimeSession } from "../../../contracts/realtime-session";
|
|
14
|
+
// One scope for both message hooks, deliberately: /api/realtime/token issues a
|
|
15
|
+
// SINGLE token covering all of this user's conversationIds, so a per-id scope
|
|
16
|
+
// would make the list and the detail hook re-sign against each other on the
|
|
17
|
+
// same page for no gain.
|
|
18
|
+
//
|
|
19
|
+
// 🛑 Known limitation, not fixed here: because the scope is constant, a
|
|
20
|
+
// conversation created AFTER sign-in is not in the cached token's claims and
|
|
21
|
+
// its subscription is denied until the token's expiry skew forces a re-sign.
|
|
22
|
+
// The denial is now surfaced (see the error callback below) rather than
|
|
23
|
+
// swallowed. The durable fix is to re-acquire on a permission denial.
|
|
24
|
+
const MESSAGES_SCOPE = "messages";
|
|
15
25
|
import { conversationPingPath, userConversationsPingPath, } from "../realtime";
|
|
16
26
|
import { normalizeError } from "../../../errors/normalize";
|
|
17
27
|
import { apiClient, ApiClientError } from "../../../http/ApiClient";
|
|
@@ -83,12 +93,16 @@ export function useConversation(conversationId) {
|
|
|
83
93
|
// because `send()` calls `refetch()` directly.
|
|
84
94
|
void (async () => {
|
|
85
95
|
try {
|
|
86
|
-
|
|
96
|
+
const lease = await acquireRealtimeSession(MESSAGES_SCOPE, () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
|
|
97
|
+
release = lease.release;
|
|
87
98
|
if (cancelled) {
|
|
88
|
-
release
|
|
99
|
+
release();
|
|
89
100
|
return;
|
|
90
101
|
}
|
|
91
|
-
|
|
102
|
+
// Subscribe through the LEASE's provider — it is bound to the app this
|
|
103
|
+
// scope authenticated on. The global provider may be on a different app
|
|
104
|
+
// carrying another channel's claims.
|
|
105
|
+
unsubscribe = lease.provider.subscribe(conversationPingPath(conversationId), () => {
|
|
92
106
|
setIsConnected(true);
|
|
93
107
|
void refetch();
|
|
94
108
|
}, (err) => {
|