@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.
- 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/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
- 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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { normalizeError } from "../errors/normalize";
|
|
2
|
-
import { getClientRealtimeProvider } from "./client-realtime";
|
|
2
|
+
import { getClientRealtimeProvider, } from "./client-realtime";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* PER-SCOPE, reference-counted sign-in for the client realtime channels.
|
|
5
|
+
*
|
|
6
|
+
* Each claim scope gets its own Firebase app (via `provider.forScope`), because
|
|
7
|
+
* Firebase Auth state is per-app and two channels legitimately need two
|
|
8
|
+
* different claim sets at the same time.
|
|
5
9
|
*
|
|
6
10
|
* WHY THIS EXISTS — two distinct bugs, one cause.
|
|
7
11
|
*
|
|
@@ -21,98 +25,89 @@ import { getClientRealtimeProvider } from "./client-realtime";
|
|
|
21
25
|
* bulk token (carrying `bulkJobId`), killing an in-flight job subscription.
|
|
22
26
|
* Whoever finished last broke whoever was still working.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
* consumer may decide to sign out
|
|
26
|
-
*
|
|
28
|
+
* (2) needed BOTH halves, and only one was done at first:
|
|
29
|
+
* - sign-OUT: no consumer may decide to sign out. Each takes a lease; the
|
|
30
|
+
* session ends only when the last lease for that scope is gone.
|
|
31
|
+
* - sign-IN: ref-counting cannot help here, because nothing is being shared
|
|
32
|
+
* by agreement — a bulk job and a message thread need different claims
|
|
33
|
+
* simultaneously. The answer is separate apps, not coordination.
|
|
27
34
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* 🛑 Subscribe through `lease.provider`, never `getClientRealtimeProvider()`.
|
|
36
|
+
* The global one is the root app; the lease's is the app your scope actually
|
|
37
|
+
* authenticated on.
|
|
30
38
|
*/
|
|
31
39
|
/** Refresh a little early — a token that expires mid-subscription is a silent drop. */
|
|
32
40
|
const EXPIRY_SKEW_MS = 60000;
|
|
33
|
-
let leases = 0;
|
|
34
|
-
let currentScope = null;
|
|
35
|
-
let expiresAt = 0;
|
|
36
|
-
let inFlight = null;
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
42
|
+
* One entry per claim scope, each on its OWN Firebase app.
|
|
39
43
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* 🛑 This used to be three module-level singletons — `currentScope`,
|
|
45
|
+
* `expiresAt`, `leases` — describing ONE shared app. That shape could not
|
|
46
|
+
* express the real situation: two channels legitimately need two different
|
|
47
|
+
* claim sets at the same time, and Firebase Auth state is per-app, so every
|
|
48
|
+
* sign-in replaced the previous one's claims. Ref-counting only stopped a
|
|
49
|
+
* finishing channel from signing the others OUT; it could not stop a starting
|
|
50
|
+
* channel from signing itself IN over them.
|
|
46
51
|
*/
|
|
52
|
+
const scopes = new Map();
|
|
47
53
|
export async function acquireRealtimeSession(scope, getToken) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
54
|
+
const root = getClientRealtimeProvider();
|
|
55
|
+
let state = scopes.get(scope);
|
|
56
|
+
if (!state) {
|
|
57
|
+
// `forScope` is optional on the contract: a provider that cannot isolate
|
|
58
|
+
// falls back to the shared app, which is the old behaviour rather than a
|
|
59
|
+
// crash.
|
|
60
|
+
state = {
|
|
61
|
+
leases: 0,
|
|
62
|
+
expiresAt: 0,
|
|
63
|
+
inFlight: null,
|
|
64
|
+
provider: root.forScope?.(scope) ?? root,
|
|
65
|
+
};
|
|
66
|
+
scopes.set(scope, state);
|
|
67
|
+
}
|
|
68
|
+
state.leases++;
|
|
69
|
+
if (Date.now() > state.expiresAt - EXPIRY_SKEW_MS) {
|
|
70
|
+
// Collapse concurrent acquires for the SAME scope — several hooks mount in
|
|
71
|
+
// one tick and would otherwise each fetch a token and race to sign in.
|
|
72
|
+
state.inFlight ?? (state.inFlight = (async () => {
|
|
55
73
|
try {
|
|
56
74
|
const token = await getToken();
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
expiresAt = token.expiresAt;
|
|
75
|
+
await state.provider.signInWithToken(token.customToken);
|
|
76
|
+
state.expiresAt = token.expiresAt;
|
|
60
77
|
}
|
|
61
78
|
finally {
|
|
62
|
-
inFlight = null;
|
|
79
|
+
state.inFlight = null;
|
|
63
80
|
}
|
|
64
81
|
})());
|
|
65
|
-
await inFlight;
|
|
82
|
+
await state.inFlight;
|
|
66
83
|
}
|
|
67
84
|
let released = false;
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* `useRealtimeEvent` is the case: its token carries a job-scoped `bulkJobId`
|
|
89
|
-
* claim it receives from the caller, so it cannot use `acquireRealtimeSession`'s
|
|
90
|
-
* fetcher. It still must not be able to sign the shared app out from under
|
|
91
|
-
* another channel, which is precisely what its `cleanup()` used to do.
|
|
92
|
-
*/
|
|
93
|
-
export function acquireExternalRealtimeLease() {
|
|
94
|
-
leases++;
|
|
95
|
-
// Its own sign-in has replaced whatever scope was active; forget the cached
|
|
96
|
-
// one so the next acquire() re-authenticates rather than trusting a token
|
|
97
|
-
// that is no longer installed.
|
|
98
|
-
currentScope = null;
|
|
99
|
-
expiresAt = 0;
|
|
100
|
-
let released = false;
|
|
101
|
-
return () => {
|
|
102
|
-
if (released)
|
|
103
|
-
return;
|
|
104
|
-
released = true;
|
|
105
|
-
leases = Math.max(0, leases - 1);
|
|
106
|
-
if (leases > 0)
|
|
107
|
-
return;
|
|
108
|
-
getClientRealtimeProvider()
|
|
109
|
-
.signOut()
|
|
110
|
-
.catch((err) => {
|
|
111
|
-
void normalizeError(err);
|
|
112
|
-
});
|
|
85
|
+
return {
|
|
86
|
+
provider: state.provider,
|
|
87
|
+
release: () => {
|
|
88
|
+
if (released)
|
|
89
|
+
return; // React StrictMode double-invokes cleanups
|
|
90
|
+
released = true;
|
|
91
|
+
const cur = scopes.get(scope);
|
|
92
|
+
if (!cur)
|
|
93
|
+
return;
|
|
94
|
+
cur.leases = Math.max(0, cur.leases - 1);
|
|
95
|
+
if (cur.leases > 0)
|
|
96
|
+
return;
|
|
97
|
+
// Last holder of THIS scope. Signing out touches only this scope's app,
|
|
98
|
+
// so it cannot disturb any other channel.
|
|
99
|
+
cur.expiresAt = 0;
|
|
100
|
+
scopes.delete(scope);
|
|
101
|
+
cur.provider.signOut().catch((err) => {
|
|
102
|
+
void normalizeError(err);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
113
105
|
};
|
|
114
106
|
}
|
|
115
|
-
/** Test/diagnostic only. */
|
|
107
|
+
/** Test/diagnostic only — total leases across every scope. */
|
|
116
108
|
export function activeRealtimeLeases() {
|
|
117
|
-
|
|
109
|
+
let total = 0;
|
|
110
|
+
for (const s of scopes.values())
|
|
111
|
+
total += s.leases;
|
|
112
|
+
return total;
|
|
118
113
|
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { DatabaseError } from "./database-error";
|
|
|
12
12
|
export { RazorpayUnreachableError } from "./razorpay-unreachable";
|
|
13
13
|
export { handleApiError, logError, isAppError } from "./error-handler";
|
|
14
14
|
export { mapToHttpError, HTTP_ERROR_CODES, type MappedError, type HttpErrorCode, } from "./error-mapping";
|
|
15
|
-
export { ERROR_DISPLAY_MAP, getErrorDisplay, type ErrorDisplayEntry, } from "./error-display-map";
|
|
15
|
+
export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, type ErrorDisplayEntry, } from "./error-display-map";
|
|
16
16
|
export { safeRead, installDegradedReadReporter, type DegradedReadReport, } from "./safe-read";
|
|
17
17
|
export { normalizeError, getErrorMessage, isAuthError, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./normalize";
|
|
18
18
|
export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./normalize";
|
package/dist/errors/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { RazorpayUnreachableError } from "./razorpay-unreachable";
|
|
|
17
17
|
export { handleApiError, logError, isAppError } from "./error-handler";
|
|
18
18
|
// Code→HTTP mapper (server) — and the i18n / inline-field map (client-safe)
|
|
19
19
|
export { mapToHttpError, HTTP_ERROR_CODES, } from "./error-mapping";
|
|
20
|
-
export { ERROR_DISPLAY_MAP, getErrorDisplay, } from "./error-display-map";
|
|
20
|
+
export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, } from "./error-display-map";
|
|
21
21
|
// safeRead is server-only (uses the persisted log reporter)
|
|
22
22
|
export { safeRead, installDegradedReadReporter, } from "./safe-read";
|
|
23
23
|
// W3 — catch-clause normalizer. Every catch (e: unknown) funnels through
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
|
+
import { serverLogger } from "../../../monitoring/server-logger";
|
|
3
4
|
import { Anchor, Avatar, Badge, Div, Heading, Row, Section, Span, Stack, Text, TextLink } from "../../../ui";
|
|
4
5
|
import { ROUTES } from "../../../constants";
|
|
5
6
|
import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
|
|
@@ -21,8 +22,12 @@ export async function DeveloperView({ labels = {} }) {
|
|
|
21
22
|
milestones = Array.isArray(aboutContent.milestones) ? aboutContent.milestones : [];
|
|
22
23
|
}
|
|
23
24
|
catch (_err) {
|
|
24
|
-
|
|
25
|
-
//
|
|
25
|
+
// Firestore unavailable — render the empty state below rather than crash,
|
|
26
|
+
// but say so: an empty Developers page is indistinguishable from a settings
|
|
27
|
+
// document that genuinely has no `isDeveloper` members.
|
|
28
|
+
serverLogger.warn("about.developerView: siteSettings.aboutContent read failed", {
|
|
29
|
+
error: normalizeError(_err).message,
|
|
30
|
+
});
|
|
26
31
|
}
|
|
27
32
|
const developers = teamMembers.filter((m) => m.isDeveloper);
|
|
28
33
|
return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [_jsx(Section, { color: "inverse", tone: "accent-banner", padding: "banner", children: _jsxs(Div, { className: `${page.container.sm}`, children: [_jsx(Heading, { color: "inverse", level: 1, variant: "none", className: "mb-3", children: labels.title ?? "Meet the Developer" }), _jsx(Text, { color: "inverse", variant: "none", className: "/80", size: "sm", children: labels.subtitle ?? "The person building LetItRip, one feature at a time." })] }) }), _jsxs(Div, { className: `${page.container.sm}`, padding: "content-banner", children: [developers.length === 0 ? (_jsx(Text, { variant: "secondary", children: labels.empty ?? "Developer profile coming soon." })) : (_jsx(Stack, { gap: "xl", children: developers.map((dev) => (_jsxs(Stack, { gap: "md", surface: "default", padding: "lg", rounded: "xl", border: "default", children: [_jsxs(Row, { gap: "md", align: "center", children: [_jsx(Avatar, { src: dev.photoUrl, name: dev.name, alt: dev.name, size: "lg" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, children: dev.name }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: dev.role }), dev.isFounder && _jsx(Badge, { variant: "primary", size: "xs", children: "Founder" }), _jsx(Badge, { variant: "secondary", size: "xs", children: "Developer" })] })] })] }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", children: dev.bio }), dev.githubUrl && (_jsx(Anchor, { href: dev.githubUrl, tone: "brand", size: "sm", underline: "hover", children: "GitHub \u2197" }))] }, dev.name))) })), milestones.length > 0 && (_jsxs(Div, { className: "mt-14 border-t", border: "default", padding: "t-xl", children: [_jsx(Heading, { level: 3, className: "mb-6", color: "muted", size: "sm", weight: "semibold", transform: "uppercase", children: labels.journeyTitle ?? "The Journey" }), _jsx(Stack, { className: "relative border-l-2 border-primary/30 max-w-2xl pl-[2rem]", gap: "lg", children: milestones.map(({ year, text }) => (_jsxs(Div, { className: "relative", children: [_jsx(Div, { className: "absolute -left-10 top-1 w-4 h-4 bg-primary border-2 border-white", rounded: "full" }), _jsx(Span, { size: "xs", weight: "bold", className: "text-primary tracking-wide", transform: "uppercase", children: year }), _jsx(Text, { className: "mt-1", children: text })] }, year))) })] })), _jsx(Div, { className: "mt-10", children: _jsx(TextLink, { href: String(ROUTES.PUBLIC.ABOUT), variant: "muted", size: "sm", children: labels.backToTeam ?? "← Meet the whole team" }) })] })] }));
|
|
@@ -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 { Row } from "@mohasinac/appkit/ui";
|
|
4
5
|
import { ROUTES } from "../../../constants";
|
|
5
6
|
import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
|
|
@@ -72,7 +73,14 @@ export async function PolicyPageView({ policy, }) {
|
|
|
72
73
|
}
|
|
73
74
|
catch (_err) {
|
|
74
75
|
void normalizeError(_err);
|
|
75
|
-
//
|
|
76
|
+
// Falls back to the bundled i18n copy below, which is a complete policy in
|
|
77
|
+
// its own right — but an admin who has overridden this page would silently
|
|
78
|
+
// see the old text, so the failure has to be visible to an operator.
|
|
79
|
+
serverLogger.warn("about.policyPageView: siteSettings.legalPages read failed", {
|
|
80
|
+
policy,
|
|
81
|
+
field: meta.firestoreField,
|
|
82
|
+
error: normalizeError(_err).message,
|
|
83
|
+
});
|
|
76
84
|
}
|
|
77
85
|
// i18n fallback sections
|
|
78
86
|
const rawSections = t.raw("sections");
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { getPublicUserProfile, getProfileStoreProducts, getReviewsAuthoredBy, getReviewsReceivedBy } from "../../auth/actions/profile-actions";
|
|
4
4
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
5
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
5
6
|
import { ROUTES } from "../../../constants";
|
|
6
7
|
import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
|
|
7
8
|
import { FLEX_CENTER } from "../../../_internal/shared/styles/themed";
|
|
@@ -64,13 +65,30 @@ export async function PublicProfileView({ userId, tab, }) {
|
|
|
64
65
|
const flex = { center: FLEX_CENTER };
|
|
65
66
|
const { getTranslations } = await import("next-intl/server");
|
|
66
67
|
const t = await getTranslations("publicProfile");
|
|
67
|
-
|
|
68
|
+
// The profile IS this page's subject — a read failure must surface, not turn
|
|
69
|
+
// into the same `null` a genuinely missing user produces. The `null` return
|
|
70
|
+
// from getPublicUserProfile still means "no such user".
|
|
71
|
+
const profile = await getPublicUserProfile(userId);
|
|
68
72
|
const storeId = profile?.storeSlug ?? null;
|
|
69
73
|
const [products, store, reviewsAuthored, reviewsReceivedAsBuyer] = await Promise.all([
|
|
70
|
-
storeId
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
storeId
|
|
75
|
+
? safeRead(() => getProfileStoreProducts(storeId), {
|
|
76
|
+
route: "/profile/[userId]",
|
|
77
|
+
key: "profile.storeProducts",
|
|
78
|
+
fallback: [],
|
|
79
|
+
})
|
|
80
|
+
: Promise.resolve([]),
|
|
81
|
+
storeId
|
|
82
|
+
? safeRead(() => storeRepository.findById(storeId), {
|
|
83
|
+
route: "/profile/[userId]",
|
|
84
|
+
key: "profile.store",
|
|
85
|
+
fallback: null,
|
|
86
|
+
})
|
|
87
|
+
: Promise.resolve(null),
|
|
88
|
+
// Both already degrade internally via safeRead — anything still thrown here
|
|
89
|
+
// is a mapping bug, which should surface rather than render an empty rail.
|
|
90
|
+
getReviewsAuthoredBy(userId),
|
|
91
|
+
getReviewsReceivedBy(userId),
|
|
74
92
|
]);
|
|
75
93
|
const isSeller = isSellerUser(profile) || isAdminUser(profile);
|
|
76
94
|
// For sellers: lead with store identity (name + logo). For buyers: user identity.
|
|
@@ -102,7 +102,7 @@ export function AddressForm({ initialData, onSubmit, onCancel, isLoading = false
|
|
|
102
102
|
}
|
|
103
103
|
catch (err) {
|
|
104
104
|
void normalizeError(err);
|
|
105
|
-
showToast(
|
|
105
|
+
showToast("Failed to save address", "error");
|
|
106
106
|
}
|
|
107
107
|
}, [formData, onSubmit, showToast, setFieldError, clearErrors, markSubmitAttempted]);
|
|
108
108
|
useEffect(() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
+
import { toUserMessage } from "../../../errors/error-display-map";
|
|
4
5
|
import { useCallback, useRef, useState } from "react";
|
|
5
6
|
import { Button, Div, Heading, Li, Row, Span, Spinner, Stack, Text, TextLink, Ul, useToast } from "../../../ui";
|
|
6
7
|
import { useClickOutside, useMessage } from "../../../react";
|
|
@@ -52,7 +53,7 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
|
|
|
52
53
|
}
|
|
53
54
|
catch (err) {
|
|
54
55
|
void normalizeError(err);
|
|
55
|
-
showToast(
|
|
56
|
+
showToast("Failed to mark notification as read", "error");
|
|
56
57
|
}
|
|
57
58
|
}, [markRead, refetch, showToast]);
|
|
58
59
|
const emitSuccess = onMarkAllReadSuccess ?? showSuccess;
|
|
@@ -63,12 +64,12 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
|
|
|
63
64
|
emitSuccess(labels.markAllRead);
|
|
64
65
|
}
|
|
65
66
|
catch (err) {
|
|
66
|
-
|
|
67
|
+
const e = normalizeError(err);
|
|
67
68
|
if (onMarkAllReadError) {
|
|
68
69
|
onMarkAllReadError(labels.error);
|
|
69
70
|
}
|
|
70
71
|
else {
|
|
71
|
-
showToast(
|
|
72
|
+
showToast(toUserMessage(e.code, undefined, { fallback: labels.error }), "error");
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}, [
|
|
@@ -79,7 +79,7 @@ export function NotificationPreferencesPanel({ fetchUrl = ACCOUNT_ENDPOINTS.NOTI
|
|
|
79
79
|
}
|
|
80
80
|
catch (err) {
|
|
81
81
|
void normalizeError(err);
|
|
82
|
-
showToast(
|
|
82
|
+
showToast("Failed to save preferences.", "error");
|
|
83
83
|
}
|
|
84
84
|
finally {
|
|
85
85
|
setSaving(false);
|
|
@@ -40,12 +40,15 @@ export function AdminFulfillmentView({ initialStoreId, initialStoreName, }) {
|
|
|
40
40
|
setOrders(res.orders ?? []);
|
|
41
41
|
}
|
|
42
42
|
catch (err) {
|
|
43
|
-
|
|
43
|
+
// An empty fulfilment queue is a normal, expected state, so a swallowed
|
|
44
|
+
// failure here reads as "nothing to pack" — matching the rest of this
|
|
45
|
+
// file, surface it instead.
|
|
46
|
+
showToast(normalizeError(err).message, "error");
|
|
44
47
|
}
|
|
45
48
|
finally {
|
|
46
49
|
setIsLoading(false);
|
|
47
50
|
}
|
|
48
|
-
}, []);
|
|
51
|
+
}, [showToast]);
|
|
49
52
|
useEffect(() => {
|
|
50
53
|
if (storeId)
|
|
51
54
|
void fetchOrders(storeId);
|
|
@@ -48,7 +48,7 @@ function MediaBrowser({ onCopy }) {
|
|
|
48
48
|
}
|
|
49
49
|
catch (err) {
|
|
50
50
|
void normalizeError(err);
|
|
51
|
-
setError(
|
|
51
|
+
setError("Failed to load");
|
|
52
52
|
}
|
|
53
53
|
finally {
|
|
54
54
|
setIsLoading(false);
|
|
@@ -108,7 +108,7 @@ export function AdminMediaView({ labels = {}, children, ...rest }) {
|
|
|
108
108
|
catch (err) {
|
|
109
109
|
void normalizeError(err);
|
|
110
110
|
setOperationMessage("Failed to discard staged media uploads.");
|
|
111
|
-
showToast(
|
|
111
|
+
showToast("Failed to clear uploads.", "error");
|
|
112
112
|
}
|
|
113
113
|
}, [cleanup, stagedUrls, showToast]);
|
|
114
114
|
const copyToClipboard = React.useCallback(async (url) => {
|
|
@@ -121,7 +121,7 @@ export function AdminMediaView({ labels = {}, children, ...rest }) {
|
|
|
121
121
|
catch (err) {
|
|
122
122
|
void normalizeError(err);
|
|
123
123
|
setOperationMessage("Could not copy URL to clipboard.");
|
|
124
|
-
showToast(
|
|
124
|
+
showToast("Failed to copy.", "error");
|
|
125
125
|
}
|
|
126
126
|
}, [showToast]);
|
|
127
127
|
if (hasChildren) {
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* page-views.repository.ts (server-only, imports firebase-admin) so the
|
|
3
3
|
* client-side tracker component can import just this without pulling
|
|
4
4
|
* firebase-admin into the client bundle. */
|
|
5
|
-
export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle", "grouped-listing"];
|
|
5
|
+
export declare const PAGE_VIEW_ENTITY_TYPES: readonly ["product", "store", "category", "homepage", "auction", "review", "user-profile", "blog", "event", "pre-order", "prize-draw", "classified", "digital-code", "live", "bundle", "grouped-listing", "listing"];
|
|
6
6
|
export type PageViewEntityType = (typeof PAGE_VIEW_ENTITY_TYPES)[number];
|
|
@@ -21,4 +21,17 @@ export const PAGE_VIEW_ENTITY_TYPES = [
|
|
|
21
21
|
/** A `groupedListings` document's own page. Distinct from "bundle" — a
|
|
22
22
|
* bundle is a priced, all-or-nothing set; a group is pick-as-you-wish. */
|
|
23
23
|
"grouped-listing",
|
|
24
|
+
/**
|
|
25
|
+
* A public BROWSE page — `/products`, `/auctions`, `/stores`, … — as opposed
|
|
26
|
+
* to one entity's detail page.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 `entityId` must be the bare route name ("products"), NEVER the filtered
|
|
29
|
+
* URL. `pageViews` doc ids are `${date}_${entityType}_${entityId}` with a
|
|
30
|
+
* `FieldValue.increment`, so a bounded id costs ONE write per day per page,
|
|
31
|
+
* while keying on a query string would create a new document per filter
|
|
32
|
+
* combination — unbounded doc creation on the highest-traffic pages, against
|
|
33
|
+
* the Firestore write budget in Rule #6. The full URL still travels in the
|
|
34
|
+
* `url` field, which is recorded but not part of the key.
|
|
35
|
+
*/
|
|
36
|
+
"listing",
|
|
24
37
|
];
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { productRepository } from "../../../repositories";
|
|
4
4
|
import { listBidsByProduct } from "../../auctions/actions/bid-actions";
|
|
5
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
5
6
|
const __P = {
|
|
6
7
|
p3: "p-[var(--appkit-space-3)]",
|
|
7
8
|
p4: "p-[var(--appkit-space-4)]",
|
|
@@ -62,11 +63,14 @@ function renderAuctionStoreReviews(storeSlug) {
|
|
|
62
63
|
return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2", color: "primary", size: "xl", weight: "semibold", children: "Store Reviews" }), _jsx(ReviewsListingPanel, { source: { kind: "store", storeSlug }, stateMode: "local", context: "store", emptyLabel: "This store has no reviews yet." })] }));
|
|
63
64
|
}
|
|
64
65
|
export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
|
|
66
|
+
// The auction IS this page's subject. A read failure must surface rather than
|
|
67
|
+
// become the same `undefined` a genuinely missing slug produces — the
|
|
68
|
+
// "Auction Not Found" branch below still handles that real case.
|
|
65
69
|
const product = initialAuction !== undefined
|
|
66
70
|
? (initialAuction ?? undefined)
|
|
67
|
-
: await productRepository.findByIdOrSlug(id)
|
|
71
|
+
: await productRepository.findByIdOrSlug(id);
|
|
68
72
|
const bidsResult = product
|
|
69
|
-
? await listBidsByProduct(String(product.id), { pageSize: 5 }).
|
|
73
|
+
? await safeRead(() => listBidsByProduct(String(product.id), { pageSize: 5 }), { route: "/auctions/[slug]", key: "auction.bidHistory", fallback: null })
|
|
70
74
|
: null;
|
|
71
75
|
const storeId = product?.storeId;
|
|
72
76
|
if (!product) {
|
|
@@ -163,8 +163,10 @@ export function MarketplaceAuctionCard({ product, buyoutPrice, className = "", v
|
|
|
163
163
|
await toggle();
|
|
164
164
|
}
|
|
165
165
|
catch (_err) {
|
|
166
|
+
// useWishlistToggle rolls back its optimistic state and raises its own
|
|
167
|
+
// toast on failure, so the heart returns to its true value on screen.
|
|
168
|
+
// Reporting again here would double-notify for one user action.
|
|
166
169
|
void normalizeError(_err);
|
|
167
|
-
// The hook rolls back optimistic state when the mutation fails.
|
|
168
170
|
}
|
|
169
171
|
}, [toggle]);
|
|
170
172
|
const countdownClass = isEnded
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Auth, rate-limiting, and Next.js specifics are handled by the consumer.
|
|
6
6
|
*/
|
|
7
7
|
import { maskPublicReview } from "../../../security";
|
|
8
|
+
import { safeRead } from "../../../errors/safe-read";
|
|
8
9
|
import { finalizeStagedMediaField } from "../../media/finalize";
|
|
9
10
|
import { userRepository } from "../repository/user.repository";
|
|
10
11
|
import { sessionRepository } from "../repository/session.repository";
|
|
@@ -65,7 +66,7 @@ export async function getPublicUserProfile(userId) {
|
|
|
65
66
|
}
|
|
66
67
|
/** Fetch approved reviews for a store. storeId === storeSlug in this project. */
|
|
67
68
|
export async function getSellerReviews(storeId) {
|
|
68
|
-
const snapshot = await reviewRepository.findApprovedByStore(storeId).
|
|
69
|
+
const snapshot = await safeRead(() => reviewRepository.findApprovedByStore(storeId), { route: "/profile/[userId]", key: "reviews.findApprovedByStore", fallback: [] });
|
|
69
70
|
return snapshot.map((r) => ({
|
|
70
71
|
id: r.id,
|
|
71
72
|
productId: r.productId,
|
|
@@ -94,7 +95,7 @@ export async function getProfileStoreProducts(storeId) {
|
|
|
94
95
|
}
|
|
95
96
|
/** Approved product reviews written by this user as a buyer. Excludes seller→buyer ratings. */
|
|
96
97
|
export async function getReviewsAuthoredBy(userId) {
|
|
97
|
-
const all = await reviewRepository.findApprovedByUser(userId).
|
|
98
|
+
const all = await safeRead(() => reviewRepository.findApprovedByUser(userId), { route: "/profile/[userId]", key: "reviews.findApprovedByUser", fallback: [] });
|
|
98
99
|
// reviewerRole:"seller" means this user rated a buyer — not a product review
|
|
99
100
|
const snapshot = all.filter((r) => r.reviewerRole !== "seller");
|
|
100
101
|
return snapshot.map((r) => ({
|
|
@@ -121,7 +122,7 @@ export async function getReviewsAuthoredBy(userId) {
|
|
|
121
122
|
}
|
|
122
123
|
/** Approved seller→buyer reviews received by this buyer. */
|
|
123
124
|
export async function getReviewsReceivedBy(userId) {
|
|
124
|
-
const snapshot = await reviewRepository.findByReviewee(userId).
|
|
125
|
+
const snapshot = await safeRead(() => reviewRepository.findByReviewee(userId), { route: "/profile/[userId]", key: "reviews.findByReviewee", fallback: [] });
|
|
125
126
|
return snapshot.map((r) => ({
|
|
126
127
|
id: r.id,
|
|
127
128
|
productId: r.productId,
|
|
@@ -22,7 +22,7 @@ export function ForgotPasswordView({ onSubmit, isLoading = false, error, success
|
|
|
22
22
|
}
|
|
23
23
|
catch (err) {
|
|
24
24
|
void normalizeError(err);
|
|
25
|
-
showToast(
|
|
25
|
+
showToast("Failed to send reset link", "error");
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
}
|
|
@@ -27,7 +27,7 @@ export function LoginForm({ onSubmit, onGoogleLogin, isLoading = false, error, l
|
|
|
27
27
|
}
|
|
28
28
|
catch (err) {
|
|
29
29
|
void normalizeError(err);
|
|
30
|
-
showToast(
|
|
30
|
+
showToast("Sign in failed", "error");
|
|
31
31
|
}
|
|
32
32
|
}, children: isLoading
|
|
33
33
|
? (labels.submittingLabel ?? "Signing in…")
|
|
@@ -39,7 +39,7 @@ export function RegisterForm({ onSubmit, isLoading = false, error, success, labe
|
|
|
39
39
|
}
|
|
40
40
|
catch (err) {
|
|
41
41
|
void normalizeError(err);
|
|
42
|
-
showToast(
|
|
42
|
+
showToast("Registration failed", "error");
|
|
43
43
|
}
|
|
44
44
|
}, children: isLoading
|
|
45
45
|
? (labels.submittingLabel ?? "Creating account…")
|
|
@@ -30,7 +30,7 @@ export function ResetPasswordView({ oobCode, onSubmit, isLoading = false, error,
|
|
|
30
30
|
}
|
|
31
31
|
catch (err) {
|
|
32
32
|
void normalizeError(err);
|
|
33
|
-
showToast(
|
|
33
|
+
showToast("Password reset failed", "error");
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
36
|
}
|
|
@@ -315,6 +315,9 @@ export function useRegister(options) {
|
|
|
315
315
|
// fail registration — the user can re-request verification later.
|
|
316
316
|
try {
|
|
317
317
|
await authProvider.sendEmailVerification();
|
|
318
|
+
// Non-fatal by design: the account already exists, and the user can
|
|
319
|
+
// re-request verification from Settings at any time. Firebase surfaces
|
|
320
|
+
// its own send failures; failing registration here would be worse.
|
|
318
321
|
}
|
|
319
322
|
catch (err) {
|
|
320
323
|
void normalizeError(err);
|
|
@@ -72,10 +72,12 @@ class BlogRepository extends BaseRepository {
|
|
|
72
72
|
.collection(this.collection)
|
|
73
73
|
.doc(id)
|
|
74
74
|
.update({ [BLOG_POST_FIELDS.VIEWS]: increment(1) });
|
|
75
|
+
// Fire-and-forget analytics: a lost view increment changes nothing that
|
|
76
|
+
// renders and cannot usefully be retried, so it must never fail the blog
|
|
77
|
+
// post read that triggered it — the count is approximate by design.
|
|
75
78
|
}
|
|
76
79
|
catch (_err) {
|
|
77
80
|
void normalizeError(_err);
|
|
78
|
-
// Fire-and-forget by design.
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
async findRelated(category, excludeId, limit = 3) {
|