@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
|
@@ -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
|
-
|
|
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
|
-
|
|
76
|
+
// Subscribe through the LEASE's provider — see useConversation.
|
|
77
|
+
unsubscribe = lease.provider.subscribe(userConversationsPingPath(userId), () => {
|
|
66
78
|
void refetch();
|
|
67
79
|
}, (err) => {
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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
|
-
|
|
41
|
-
setError(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
safeRead(() => productFeaturesRepository.listPlatform(), {
|
|
14
|
+
route: "productFeatures", key: "productFeatures.platform", fallback: [],
|
|
15
|
+
}),
|
|
10
16
|
storeId
|
|
11
|
-
? productFeaturesRepository.listForStore(storeId)
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
79
|
-
|
|
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 (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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((
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
82
|
+
showToast("Failed to update default.", "error");
|
|
83
83
|
}
|
|
84
84
|
finally {
|
|
85
85
|
setSettingDefaultId(null);
|