@mohasinac/appkit 4.30.15 → 4.30.17
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/server/functions/https.js +17 -2
- package/dist/_internal/server/jobs/core/listingProcessor.js +5 -1
- package/dist/_internal/shared/types/action-error-reporter.d.ts +40 -0
- package/dist/_internal/shared/types/action-error-reporter.js +27 -0
- package/dist/_internal/shared/types/action-result.d.ts +10 -0
- package/dist/_internal/shared/types/action-result.js +26 -0
- package/dist/client/api/surface-error.d.ts +6 -4
- package/dist/client/api/surface-error.js +19 -8
- package/dist/contracts/realtime-session.d.ts +27 -0
- package/dist/contracts/realtime-session.js +118 -0
- package/dist/core/unit-of-work.js +21 -13
- package/dist/errors/base-error.js +14 -1
- package/dist/errors/database-error.js +11 -1
- package/dist/errors/describe-cause.d.ts +5 -0
- package/dist/errors/describe-cause.js +76 -0
- package/dist/errors/error-display-map.d.ts +32 -1
- package/dist/errors/error-display-map.js +46 -0
- package/dist/errors/error-mapping.d.ts +47 -7
- package/dist/errors/error-mapping.js +46 -5
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +4 -0
- package/dist/features/about/components/PublicProfileView.js +4 -4
- package/dist/features/account/components/AddressBook.js +2 -2
- package/dist/features/account/components/AddressesIndexListing.js +2 -2
- package/dist/features/addresses/repository/addresses.repository.d.ts +12 -1
- package/dist/features/addresses/repository/addresses.repository.js +16 -7
- package/dist/features/admin/components/AdminAnalyticsView.js +2 -2
- package/dist/features/admin/components/AdminCouponsView.js +2 -2
- package/dist/features/admin/components/AdminGuideHubView.js +2 -2
- package/dist/features/admin/components/AdminMediaView.js +2 -2
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionGrid.d.ts +1 -2
- package/dist/features/auctions/components/MarketplaceAuctionGrid.js +8 -7
- package/dist/features/auctions/components/PlaceBidFormClient.js +32 -15
- package/dist/features/auth/repository/user.repository.d.ts +22 -1
- package/dist/features/auth/repository/user.repository.js +31 -28
- package/dist/features/before-after/components/BeforeAfterGallery.js +2 -2
- package/dist/features/blog/components/BlogIndexListing.js +3 -3
- package/dist/features/blog/components/BlogListView.js +3 -3
- package/dist/features/blog/components/BlogPostView.js +2 -2
- package/dist/features/cart/repository/cart.repository.d.ts +15 -2
- package/dist/features/cart/repository/cart.repository.js +15 -13
- package/dist/features/categories/components/BundleCollage.js +2 -2
- package/dist/features/categories/components/CategoriesIndexListing.js +3 -3
- package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
- package/dist/features/categories/components/CategoryGrid.js +2 -2
- package/dist/features/categories/components/CategoryProductsListing.js +2 -2
- package/dist/features/categories/components/CategoryStoresListing.js +2 -2
- package/dist/features/collections/components/CollectionCard.js +2 -2
- package/dist/features/events/components/EventRafflesSection.js +2 -2
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/components/EventsListView.js +3 -3
- package/dist/features/homepage/components/FeaturedBundlesSection.js +2 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HomepageSkeleton.js +1 -1
- package/dist/features/homepage/components/SecurityHighlightsSection.js +1 -1
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/StatsCounterSection.js +1 -1
- package/dist/features/homepage/components/TrustBadges.js +2 -2
- package/dist/features/homepage/components/TrustFeaturesSection.js +1 -1
- package/dist/features/messages/hooks/useConversation.js +39 -15
- package/dist/features/messages/hooks/useConversations.js +36 -14
- package/dist/features/messages/repository/conversations.repository.d.ts +23 -0
- package/dist/features/messages/repository/conversations.repository.js +53 -0
- package/dist/features/orders/repository/orders.repository.d.ts +12 -1
- package/dist/features/orders/repository/orders.repository.js +14 -5
- package/dist/features/payments/repository/payout.repository.d.ts +19 -9
- package/dist/features/payments/repository/payout.repository.js +26 -20
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -3
- package/dist/features/products/components/AuctionsIndexListing.js +2 -3
- package/dist/features/products/components/PrizeDrawCollage.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -3
- package/dist/features/products/components/PrizeDrawsSection.js +2 -2
- package/dist/features/products/components/ProductFilters.d.ts +19 -1
- package/dist/features/products/components/ProductFilters.js +11 -3
- package/dist/features/products/components/ProductGrid.js +2 -7
- package/dist/features/products/components/ProductsIndexListing.d.ts +4 -2
- package/dist/features/products/components/ProductsIndexListing.js +41 -28
- package/dist/features/products/components/RelatedProducts.js +2 -2
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -2
- package/dist/features/reviews/components/ReviewsListingPanel.js +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +10 -8
- package/dist/features/reviews/repository/reviews.repository.js +16 -19
- package/dist/features/seller/components/SellerCouponsView.js +2 -2
- package/dist/features/seller/components/SellerDashboardView.js +3 -3
- package/dist/features/server-errors/schemas/firestore.d.ts +9 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -3
- package/dist/features/stores/components/StoreClassifiedsListing.js +2 -3
- package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -3
- package/dist/features/stores/components/StoreGuideHubView.js +2 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +2 -3
- package/dist/features/stores/components/StorePreOrdersListing.js +2 -3
- package/dist/features/stores/components/StoreProductsListing.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +3 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +215 -18
- package/dist/next/api/routeHandler.js +27 -7
- package/dist/providers/db-firebase/base.repository.d.ts +12 -0
- package/dist/providers/db-firebase/base.repository.js +48 -20
- package/dist/react/hooks/usePendingFilters.d.ts +12 -2
- package/dist/react/hooks/usePendingFilters.js +2 -2
- package/dist/react/hooks/usePendingTable.d.ts +9 -2
- package/dist/react/hooks/useRealtimeEvent.js +22 -4
- package/dist/security/pii-schemas.d.ts +19 -1
- package/dist/security/pii-schemas.js +19 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +84 -20
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/tokens.css +7 -2
- package/dist/ui/components/Layout.d.ts +31 -18
- package/dist/ui/components/Layout.js +30 -19
- package/dist/ui/components/Layout.style.css +76 -17
- package/dist/ui/components/ListingLayout.d.ts +2 -0
- package/dist/ui/components/ListingLayout.js +16 -2
- package/dist/ui/components/StickyToolbar.js +3 -48
- package/dist/ui/components/toolbar-collapse.d.ts +44 -0
- package/dist/ui/components/toolbar-collapse.js +93 -0
- package/package.json +1 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Div, Pagination, Row, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Div, Grid, Pagination, Row, Stack, Text } from "../../../ui";
|
|
4
4
|
import { EventCard } from "./EventCard";
|
|
5
5
|
const __P = {
|
|
6
6
|
p4: "p-[var(--appkit-space-4)]",
|
|
7
7
|
};
|
|
8
8
|
export function EventsListView({ events, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onParticipate, emptyLabel = "No events found", slots, }) {
|
|
9
9
|
if (isLoading) {
|
|
10
|
-
return (_jsx(
|
|
10
|
+
return (_jsx(Grid, { cols: "cards", gap: "lg", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Div, { className: "animate-pulse overflow-hidden", surface: "subtle", rounded: "xl", border: "default", children: [_jsx(Div, { className: "aspect-video", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p4}`, gap: "sm", children: [_jsx(Div, { className: "h-4 w-16", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-5 w-full", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-3/4", surface: "subtle", rounded: "default" })] })] }, i))) }));
|
|
11
11
|
}
|
|
12
12
|
if (events.length === 0) {
|
|
13
13
|
if (slots?.renderEmptyState) {
|
|
@@ -17,7 +17,7 @@ export function EventsListView({ events, isLoading, totalPages = 1, currentPage
|
|
|
17
17
|
}
|
|
18
18
|
return (_jsxs(Stack, { gap: "xl", children: [slots?.renderHeader
|
|
19
19
|
? slots.renderHeader({ total })
|
|
20
|
-
: null, _jsx(
|
|
20
|
+
: null, _jsx(Grid, { cols: "cards", gap: "lg", children: events.map((event, i) => slots?.renderCard ? (_jsx(React.Fragment, { children: slots.renderCard(event, i) }, event.id)) : (_jsx(EventCard, { event: event, onParticipate: onParticipate ? (e) => onParticipate(e) : undefined }, event.id))) }), slots?.renderFooter ? slots.renderFooter({
|
|
21
21
|
page: currentPage,
|
|
22
22
|
totalPages,
|
|
23
23
|
}) : totalPages > 1 && onPageChange ? (_jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }) })) : null] }));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"use client";
|
|
10
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import Link from "next/link";
|
|
12
|
-
import { Badge, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
12
|
+
import { Badge, Div, Grid, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
13
13
|
import { MediaImage } from "../../media/MediaImage";
|
|
14
14
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
15
15
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
@@ -29,7 +29,7 @@ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitl
|
|
|
29
29
|
const items = initialItems.filter((c) => c.categoryType === "bundle");
|
|
30
30
|
if (items.length === 0)
|
|
31
31
|
return null;
|
|
32
|
-
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(
|
|
32
|
+
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, reverse: hand === "left", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Grid, { cols: "cards", gap: "3", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
|
|
33
33
|
}
|
|
34
34
|
function FeaturedBundleCard({ bundle, onBuyNow }) {
|
|
35
35
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
@@ -1,6 +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 { Anchor, Div, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Row, Section, Span, Stack, Text } from "../../../ui";
|
|
4
4
|
import { MediaImage } from "../../media/MediaImage";
|
|
5
5
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
6
6
|
import { fetchGoogleReviews } from "../lib/google-reviews-fetcher";
|
|
@@ -65,7 +65,7 @@ export async function GoogleReviewsSection(config) {
|
|
|
65
65
|
}
|
|
66
66
|
const gridClass = layout === "carousel"
|
|
67
67
|
? "flex gap-[var(--appkit-space-4)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
|
|
68
|
-
:
|
|
68
|
+
: `${GRID_MAP.cards} gap-[var(--appkit-space-4)]`;
|
|
69
69
|
const cardClass = layout === "carousel" ? "flex-shrink-0 w-[85%] sm:w-72 snap-start" : "";
|
|
70
70
|
const mapsHref = googleMapsUrl || (placeId ? `https://search.google.com/local/reviews?placeid=${placeId}` : "");
|
|
71
71
|
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", padding: "x-md", children: [_jsxs(HandModeRow, { className: "mb-8", align: "end", justify: "between", gap: "md", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, children: "What Our Customers Say" }), aggregateRating > 0 && (_jsxs(Row, { className: "mt-1", align: "center", gap: "sm", children: [_jsx(StarRating, { rating: Math.round(aggregateRating) }), _jsxs(Text, { size: "sm", variant: "muted", children: [aggregateRating.toFixed(1), " \u00B7 ", totalRatings.toLocaleString(), " reviews"] })] }))] }), linkToGoogleMaps && mapsHref && (_jsx(Anchor, { href: mapsHref, tone: "none", underline: "hover", className: CLS_GMAPS_LINK, children: "View on Google \u2192" }))] }), reviews.length === 0 ? (_jsx(EmptyState, {})) : (_jsx(Div, { className: gridClass, children: reviews.map((review, i) => (_jsx(Div, { className: cardClass, children: _jsx(ReviewCard, { review: review, showRating: showRating, showDate: showDate }) }, `${review.authorName}-${i}`))) }))] }) }));
|
|
@@ -17,5 +17,5 @@ export function HomepageSkeleton() {
|
|
|
17
17
|
const trustCardH = "h-[clamp(120px,18vh,170px)]";
|
|
18
18
|
const categoryTileH = "h-[clamp(112px,16vh,160px)]";
|
|
19
19
|
const newsletterH = "h-[clamp(220px,28vh,320px)]";
|
|
20
|
-
return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, {
|
|
20
|
+
return (_jsxs(Div, { className: `w-full ${__O.hidden}`, children: [_jsx(Div, { className: `${skeleton.card} w-full ${heroSkeletonH}`, "aria-hidden": "true" }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 4 }).map((_, i) => (_jsx(Stack, { className: `${skeleton.card} ${trustCardH}`, align: "center", gap: "3", padding: "lg" }, i))) }) }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsx(Div, { className: `${skeleton.heading} w-48 mx-auto mb-6` }), _jsx(Grid, { cols: "statTiles", children: Array.from({ length: 6 }).map((_, i) => (_jsx(Div, { className: `${skeleton.card} ${categoryTileH}` }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "muted", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-52` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsxs(Section, { className: `${__P.p8}`, surface: "subtle", children: [_jsxs(Row, { className: `${flex.between} mb-6`, children: [_jsx(Div, { className: `${skeleton.heading} w-56` }), _jsx(Div, { className: `${skeleton.text} w-24` })] }), _jsx(Grid, { cols: "cards", children: Array.from({ length: 5 }).map((_, i) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { className: `${skeleton.image} w-full pb-[100%]` }), _jsx(Div, { className: `${skeleton.text} w-3/4` }), _jsx(Div, { className: `${skeleton.text} w-1/2` })] }, i))) })] }), _jsx(Section, { className: `${__P.p8}`, surface: "muted", children: _jsx(Div, { className: `${skeleton.card} max-w-2xl mx-auto ${newsletterH}`, rounded: "2xl" }) })] }));
|
|
21
21
|
}
|
|
@@ -35,5 +35,5 @@ export function SecurityHighlightsSection({ title, subtitle, pillLabel, items, l
|
|
|
35
35
|
observer.observe(el);
|
|
36
36
|
return () => observer.disconnect();
|
|
37
37
|
}, []);
|
|
38
|
-
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, {
|
|
38
|
+
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, surface: "muted", paddingX: "x-md", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-10", children: [pillLabel && (_jsx(Span, { layout: "inline-flex", gap: "md", size: "xs", weight: "medium", className: "border border-primary-[500]/30 bg-primary-500/10 px-[1.25rem] py-[0.375rem] tracking-[0.2em] text-primary-700 dark:text-primary-400 backdrop-blur-sm", rounded: "full", transform: "uppercase", children: pillLabel })), _jsx(Heading, { level: 2, className: `mt-4 ${themed.textPrimary}`, size: "3xl", mdSize: "4xl", weight: "bold", children: title }), subtitle && (_jsx(Text, { variant: "secondary", className: "mt-2 max-w-xl", children: subtitle }))] }), _jsx(Grid, { cols: "cards", gap: "xl", children: items.map((item, index) => (_jsx(SecurityCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.key))) }), learnMoreHref && (_jsx(Div, { className: "text-left mt-8", children: _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel }) }))] }) }));
|
|
39
39
|
}
|
|
@@ -1,6 +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 { Anchor, Div, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
|
|
3
|
+
import { Anchor, Div, GRID_MAP, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
|
|
4
4
|
import { SocialPostCard } from "./SocialPostCard";
|
|
5
5
|
import { fetchInstagramPosts, fetchFacebookPosts, fetchTikTokPosts, fetchDeviantArtPosts, } from "../lib/social-feed-fetcher";
|
|
6
6
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
@@ -93,7 +93,7 @@ export async function SocialFeedSection(config) {
|
|
|
93
93
|
? "flex gap-[var(--appkit-space-3)] overflow-x-auto pb-[var(--appkit-space-2)] snap-x snap-mandatory"
|
|
94
94
|
: layout === "masonry"
|
|
95
95
|
? "columns-2 sm:columns-3 lg:columns-4 gap-[var(--appkit-space-3)] space-y-3"
|
|
96
|
-
:
|
|
96
|
+
: `${GRID_MAP.cards} gap-[var(--appkit-space-3)]`;
|
|
97
97
|
const cardClass = layout === "carousel"
|
|
98
98
|
? "flex-shrink-0 w-[85%] sm:w-56 snap-start"
|
|
99
99
|
: layout === "masonry"
|
|
@@ -31,5 +31,5 @@ export function StatsCounterSection({ stats, className = "", }) {
|
|
|
31
31
|
observer.observe(el);
|
|
32
32
|
return () => observer.disconnect();
|
|
33
33
|
}, []);
|
|
34
|
-
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, {
|
|
34
|
+
return (_jsx(Section, { border: "subtle", paddingY: "y-2-5xl", ref: sectionRef, className: `border-y ${className}`, paddingX: "x-md", surface: "default", children: _jsx(Div, { className: "mx-auto max-w-5xl", children: _jsx(Grid, { cols: "statTiles", gap: "none", children: stats.map((stat, i) => (_jsx(StatCard, { stat: stat, visible: visible, delayClass: STAT_DELAYS[Math.min(i, STAT_DELAYS.length - 1)], isLast: i === stats.length - 1 }, stat.key))) }) }) }));
|
|
35
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Div, Row, Section, Stack, Text } from "../../../ui";
|
|
2
|
+
import { Div, Grid, Row, Section, Stack, Text } from "../../../ui";
|
|
3
3
|
const BADGE_ICONS = {
|
|
4
4
|
shipping: (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: [_jsx("path", { d: "M5 12H19" }), _jsx("path", { d: "M15 7l5 5-5 5" }), _jsx("path", { d: "M1 17V7a4 4 0 0 1 4-4h10" }), _jsx("path", { d: "M22 17a4 4 0 0 1-4 4H5" })] })),
|
|
5
5
|
support: (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: "h-7 w-7", "aria-hidden": "true", children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) })),
|
|
@@ -42,5 +42,5 @@ const FALLBACK_BADGES = [
|
|
|
42
42
|
];
|
|
43
43
|
export function TrustBadges({ badges }) {
|
|
44
44
|
const items = badges && badges.length > 0 ? badges : FALLBACK_BADGES;
|
|
45
|
-
return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(
|
|
45
|
+
return (_jsx(Section, { className: "bg-[var(--section-bg)] [border-top:var(--section-border)] [border-bottom:var(--section-border)]", children: _jsx(Div, { className: "mx-auto max-w-7xl sm:py-[2.5rem]", paddingY: "y-lg", paddingX: "x-md", children: _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((badge) => (_jsxs(Stack, { className: "text-left [flex-direction:row] items-[center] gap-[1rem]", align: "center", gap: "sm", children: [_jsx(Row, { className: "h-12 w-12 shrink-0 bg-[rgba(240,196,23,0.12)] text-[var(--color-red)] [border:2px_solid_var(--border-ink)]", align: "center", justify: "center", rounded: "full", children: BADGE_ICONS[badge.iconKey] }), _jsxs(Div, { children: [_jsx(Text, { className: "font-black uppercase tracking-wide text-[var(--section-title-color)]", size: "sm", children: badge.title }), _jsx(Text, { className: "text-[var(--color-muted)]", size: "xs", weight: "medium", children: badge.sub })] })] }, badge.id))) }) }) }));
|
|
46
46
|
}
|
|
@@ -38,5 +38,5 @@ export function TrustFeaturesSection({ items, title, variant = "cards", classNam
|
|
|
38
38
|
const doubled = [...items, ...items];
|
|
39
39
|
return (_jsx(Section, { border: "subtle", ref: sectionRef, className: `overflow-hidden border-y ${className}`, padding: "y-md", children: _jsx(Div, { layout: "flex", className: "animate-marquee", children: doubled.map((item, i) => (_jsx(TrustStripItem, { item: item }, `${item.iconName}-${i}`))) }) }));
|
|
40
40
|
}
|
|
41
|
-
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, {
|
|
41
|
+
return (_jsx(Section, { ref: sectionRef, paddingY: "y-2-5xl", className: className, paddingX: "x-md", surface: "muted", children: _jsxs(Div, { className: "max-w-6xl mx-auto", children: [title && (_jsx(Div, { className: "text-left mb-8", children: _jsx(Heading, { level: 2, color: "primary", size: "3xl", weight: "bold", mdSize: "4xl", children: title }) })), _jsx(Grid, { cols: "statTiles", gap: "lg", children: items.map((item, index) => (_jsx(TrustFeatureCard, { item: item, visible: visible, delayClass: CARD_DELAYS[Math.min(index, CARD_DELAYS.length - 1)] }, item.iconName))) })] }) }));
|
|
42
42
|
}
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
13
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
14
|
+
import { acquireRealtimeSession } from "../../../contracts/realtime-session";
|
|
14
15
|
import { conversationPingPath, userConversationsPingPath, } from "../realtime";
|
|
15
16
|
import { normalizeError } from "../../../errors/normalize";
|
|
16
17
|
import { apiClient, ApiClientError } from "../../../http/ApiClient";
|
|
17
|
-
import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
18
|
+
import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS, } from "../../../constants/api-endpoints";
|
|
18
19
|
/**
|
|
19
20
|
* @deprecated Re-exported for backwards compat — import `conversationPingPath`
|
|
20
21
|
* from `@mohasinac/appkit` instead.
|
|
@@ -69,22 +70,45 @@ export function useConversation(conversationId) {
|
|
|
69
70
|
setIsConnected(false);
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
let unsubscribe;
|
|
74
|
+
let release;
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
// 🛑 Sign in BEFORE subscribing. Without a session `auth == null`, and the
|
|
77
|
+
// `chats/$conversationId` rule requires
|
|
78
|
+
// `auth.token.conversationIds[$conversationId]` — a claim that, until this
|
|
79
|
+
// change, NOTHING issued (the token route only ever sent `chatIds`, from
|
|
80
|
+
// the unrelated `chatRooms` collection). Two stacked faults on one channel:
|
|
81
|
+
// no sign-in, and a claim with no issuer. The recipient of a message never
|
|
82
|
+
// saw it until they manually refreshed; the sender only saw their own
|
|
83
|
+
// because `send()` calls `refetch()` directly.
|
|
84
|
+
void (async () => {
|
|
85
|
+
try {
|
|
86
|
+
release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
release?.();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
unsubscribe = getClientRealtimeProvider().subscribe(conversationPingPath(conversationId), () => {
|
|
92
|
+
setIsConnected(true);
|
|
93
|
+
void refetch();
|
|
94
|
+
}, (err) => {
|
|
95
|
+
void normalizeError(err);
|
|
96
|
+
setIsConnected(false);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (_err) {
|
|
100
|
+
void normalizeError(_err);
|
|
101
|
+
// Provider unregistered or token fetch failed — falls back to the
|
|
102
|
+
// one-shot fetch above.
|
|
78
103
|
setIsConnected(false);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
void normalizeError(_err);
|
|
84
|
-
// Realtime provider not registered — falls back to one-shot fetch above.
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return () => {
|
|
107
|
+
cancelled = true;
|
|
85
108
|
setIsConnected(false);
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
unsubscribe?.();
|
|
110
|
+
release?.();
|
|
111
|
+
};
|
|
88
112
|
}, [conversationId, refetch]);
|
|
89
113
|
const sendMessage = useCallback(async (body) => {
|
|
90
114
|
if (!conversationId)
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { useCallback, useEffect, useState } from "react";
|
|
8
8
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
9
|
+
import { acquireRealtimeSession } from "../../../contracts/realtime-session";
|
|
9
10
|
import { userConversationsPingPath } from "../realtime";
|
|
10
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
11
12
|
import { apiClient } from "../../../http/ApiClient";
|
|
12
|
-
import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
|
+
import { CONVERSATION_ENDPOINTS, ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
14
|
const LIST_ENDPOINT = CONVERSATION_ENDPOINTS.LIST;
|
|
14
15
|
async function fetchList(endpoint) {
|
|
15
16
|
const data = await apiClient.get(endpoint);
|
|
@@ -46,19 +47,40 @@ export function useConversations(userId, opts = {}) {
|
|
|
46
47
|
useEffect(() => {
|
|
47
48
|
if (!userId)
|
|
48
49
|
return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
let unsubscribe;
|
|
51
|
+
let release;
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
// 🛑 Sign in BEFORE subscribing. This hook used to call subscribe() with no
|
|
54
|
+
// authenticated session at all, so `auth == null` and the
|
|
55
|
+
// `chats/user/$userId` rule (`auth.uid == $userId`) could never pass. Every
|
|
56
|
+
// update was permission-denied and the error handler below was a literal
|
|
57
|
+
// `// ignore`, so the whole live-list channel was inert while looking wired.
|
|
58
|
+
void (async () => {
|
|
59
|
+
try {
|
|
60
|
+
release = await acquireRealtimeSession("messages", () => apiClient.post(ADMIN_ENDPOINTS.REALTIME_TOKEN, {}));
|
|
61
|
+
if (cancelled) {
|
|
62
|
+
release?.();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
unsubscribe = getClientRealtimeProvider().subscribe(userConversationsPingPath(userId), () => {
|
|
66
|
+
void refetch();
|
|
67
|
+
}, (err) => {
|
|
68
|
+
// Surfaced, not ignored. A denied subscription is a real defect and
|
|
69
|
+
// silence here is what hid it for months.
|
|
70
|
+
void normalizeError(err);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (_err) {
|
|
74
|
+
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
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
return () => {
|
|
80
|
+
cancelled = true;
|
|
81
|
+
unsubscribe?.();
|
|
82
|
+
release?.();
|
|
83
|
+
};
|
|
62
84
|
}, [userId, refetch]);
|
|
63
85
|
const totalUnread = conversations.reduce((sum, c) => sum + (c[unreadField] ?? 0), 0);
|
|
64
86
|
return { conversations, isLoading, error, totalUnread, refetch };
|
|
@@ -30,6 +30,29 @@ export declare class ConversationsRepository {
|
|
|
30
30
|
findById(id: string): Promise<ConversationDocument | null>;
|
|
31
31
|
listByBuyer(buyerId: string): Promise<ConversationDocument[]>;
|
|
32
32
|
listByStore(storeId: string): Promise<ConversationDocument[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Conversation ids this user may subscribe to, for the RTDB `conversationIds`
|
|
35
|
+
* custom-token claim.
|
|
36
|
+
*
|
|
37
|
+
* 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
|
|
38
|
+
* rule requires `auth.token.conversationIds[$conversationId] == true`, but
|
|
39
|
+
* `/api/realtime/token` only ever issued `chatIds`, built by
|
|
40
|
+
* `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
|
|
41
|
+
* a different collection with different document ids. So the buyer↔seller
|
|
42
|
+
* live-message channel was denied for every user, and both subscribing hooks
|
|
43
|
+
* swallowed the permission error (one into `setIsConnected(false)`, one into
|
|
44
|
+
* a literal `// ignore`). Messages only ever appeared for the sender, via an
|
|
45
|
+
* unrelated `refetch()` after send.
|
|
46
|
+
*
|
|
47
|
+
* `.select()` with no fields fetches ids only — the document bodies are not
|
|
48
|
+
* needed and this runs on every token issue.
|
|
49
|
+
*
|
|
50
|
+
* @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
|
|
51
|
+
* total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
|
|
52
|
+
* user in hundreds of conversations would make `createCustomToken` throw
|
|
53
|
+
* and take the whole route down (see the route's own comment).
|
|
54
|
+
*/
|
|
55
|
+
getConversationIdsForUser(userId: string, storeIds?: readonly string[], limit?: number): Promise<string[]>;
|
|
33
56
|
/**
|
|
34
57
|
* Append a message to a conversation in a transaction. Bumps the counterparty
|
|
35
58
|
* unread counter; updates lastMessage / lastMessageAt / updatedAt.
|
|
@@ -133,6 +133,59 @@ export class ConversationsRepository {
|
|
|
133
133
|
.get();
|
|
134
134
|
return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Conversation ids this user may subscribe to, for the RTDB `conversationIds`
|
|
138
|
+
* custom-token claim.
|
|
139
|
+
*
|
|
140
|
+
* 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
|
|
141
|
+
* rule requires `auth.token.conversationIds[$conversationId] == true`, but
|
|
142
|
+
* `/api/realtime/token` only ever issued `chatIds`, built by
|
|
143
|
+
* `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
|
|
144
|
+
* a different collection with different document ids. So the buyer↔seller
|
|
145
|
+
* live-message channel was denied for every user, and both subscribing hooks
|
|
146
|
+
* swallowed the permission error (one into `setIsConnected(false)`, one into
|
|
147
|
+
* a literal `// ignore`). Messages only ever appeared for the sender, via an
|
|
148
|
+
* unrelated `refetch()` after send.
|
|
149
|
+
*
|
|
150
|
+
* `.select()` with no fields fetches ids only — the document bodies are not
|
|
151
|
+
* needed and this runs on every token issue.
|
|
152
|
+
*
|
|
153
|
+
* @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
|
|
154
|
+
* total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
|
|
155
|
+
* user in hundreds of conversations would make `createCustomToken` throw
|
|
156
|
+
* and take the whole route down (see the route's own comment).
|
|
157
|
+
*/
|
|
158
|
+
async getConversationIdsForUser(userId, storeIds = [], limit = 25) {
|
|
159
|
+
const queries = [
|
|
160
|
+
this.collection()
|
|
161
|
+
.where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
|
|
162
|
+
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
163
|
+
.limit(limit)
|
|
164
|
+
.select()
|
|
165
|
+
.get(),
|
|
166
|
+
// Firestore `in` caps at 30 values; a seller with more stores than that
|
|
167
|
+
// is not a shape this app produces.
|
|
168
|
+
...(storeIds.length > 0
|
|
169
|
+
? [
|
|
170
|
+
this.collection()
|
|
171
|
+
.where(CONVERSATION_FIELDS.STORE_ID, "in", storeIds.slice(0, 30))
|
|
172
|
+
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
173
|
+
.limit(limit)
|
|
174
|
+
.select()
|
|
175
|
+
.get(),
|
|
176
|
+
]
|
|
177
|
+
: []),
|
|
178
|
+
];
|
|
179
|
+
const snaps = await Promise.all(queries);
|
|
180
|
+
const ids = new Set();
|
|
181
|
+
for (const snap of snaps) {
|
|
182
|
+
for (const doc of snap.docs)
|
|
183
|
+
ids.add(doc.id);
|
|
184
|
+
}
|
|
185
|
+
// Most-recent-first is already the sort order; truncating here keeps the
|
|
186
|
+
// claim inside the byte budget when a user is both buyer and seller.
|
|
187
|
+
return [...ids].slice(0, limit);
|
|
188
|
+
}
|
|
136
189
|
/**
|
|
137
190
|
* Append a message to a conversation in a transaction. Bumps the counterparty
|
|
138
191
|
* unread counter; updates lastMessage / lastMessageAt / updatedAt.
|
|
@@ -27,7 +27,18 @@ export interface OrderWriteContext {
|
|
|
27
27
|
declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
28
28
|
constructor();
|
|
29
29
|
private decryptOrder;
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Declared, not hand-rolled.
|
|
32
|
+
*
|
|
33
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
34
|
+
* `encryptOrderData` did — encrypt the listed fields, then add the mapped blind
|
|
35
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
36
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
37
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
38
|
+
* encryption lived in a private method only three call sites remembered to
|
|
39
|
+
* call.
|
|
40
|
+
*/
|
|
41
|
+
protected piiFields: readonly ["userName", "userEmail", "sellerEmail"];
|
|
31
42
|
protected mapDoc<D = OrderDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
32
43
|
create(input: OrderCreateInput): Promise<OrderDocument>;
|
|
33
44
|
findByUser(userId: string): Promise<OrderDocument[]>;
|
|
@@ -3,7 +3,7 @@ import { planSearchTxt, refineSearchTxt, emptySearchResult, } from "../../../uti
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { DatabaseError, NotFoundError } from "../../../errors";
|
|
5
5
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
6
|
-
import { decryptPiiFields,
|
|
6
|
+
import { decryptPiiFields, ORDER_PII_FIELDS, } from "../../../security";
|
|
7
7
|
import { serverTimestamp, arrayUnion } from "../../../contracts/field-ops";
|
|
8
8
|
import { createOrderId, ORDER_COLLECTION, ORDER_TRACKED_FIELDS, OrderStatusValues, } from "../schemas";
|
|
9
9
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
@@ -24,15 +24,24 @@ const ACTIVE_ALLOWANCE_STATUSES = [
|
|
|
24
24
|
class OrderRepository extends BaseRepository {
|
|
25
25
|
constructor() {
|
|
26
26
|
super(ORDER_COLLECTION);
|
|
27
|
+
/**
|
|
28
|
+
* Declared, not hand-rolled.
|
|
29
|
+
*
|
|
30
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
31
|
+
* `encryptOrderData` did — encrypt the listed fields, then add the mapped blind
|
|
32
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
33
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
34
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
35
|
+
* encryption lived in a private method only three call sites remembered to
|
|
36
|
+
* call.
|
|
37
|
+
*/
|
|
38
|
+
this.piiFields = ORDER_PII_FIELDS;
|
|
27
39
|
}
|
|
28
40
|
decryptOrder(doc) {
|
|
29
41
|
return decryptPiiFields(doc, [
|
|
30
42
|
...ORDER_PII_FIELDS,
|
|
31
43
|
]);
|
|
32
44
|
}
|
|
33
|
-
encryptOrderData(data) {
|
|
34
|
-
return encryptPiiFields(data, [...ORDER_PII_FIELDS]);
|
|
35
|
-
}
|
|
36
45
|
mapDoc(snap) {
|
|
37
46
|
const raw = super.mapDoc(snap);
|
|
38
47
|
return this.decryptOrder(raw);
|
|
@@ -46,7 +55,7 @@ class OrderRepository extends BaseRepository {
|
|
|
46
55
|
createdAt: new Date(),
|
|
47
56
|
updatedAt: new Date(),
|
|
48
57
|
};
|
|
49
|
-
const encrypted = this.
|
|
58
|
+
const encrypted = this.applyWriteHooks(orderData);
|
|
50
59
|
await this.db
|
|
51
60
|
.collection(this.collection)
|
|
52
61
|
.doc(id)
|
|
@@ -20,17 +20,27 @@ export declare class PayoutRepository extends BaseRepository<PayoutDocument> {
|
|
|
20
20
|
/** Decrypt PII fields on a payout document after Firestore read */
|
|
21
21
|
private decryptPayout;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Declared, not hand-rolled.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
26
|
+
* `encryptPayoutData` did — encrypt the listed fields, then add the mapped blind
|
|
27
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
28
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
29
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
30
|
+
* encryption lived in a private method only three call sites remembered to
|
|
31
|
+
* call.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
protected piiFields: readonly ["sellerEmail", "upiId"];
|
|
34
|
+
protected piiIndexMap: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* The base hook, plus the nested objects it cannot see.
|
|
37
|
+
*
|
|
38
|
+
* `piiFields` is a flat list of top-level names, so a nested blob has to be
|
|
39
|
+
* encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
|
|
40
|
+
* encryption path with one extra step on top, rather than two parallel paths
|
|
41
|
+
* that have to agree — which is what the hand-rolled override was.
|
|
42
|
+
*/
|
|
43
|
+
protected applyWriteHooks<D extends object>(data: D): D;
|
|
34
44
|
/** Override mapDoc to auto-decrypt PII on every Firestore read */
|
|
35
45
|
protected mapDoc<D = PayoutDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
36
46
|
/**
|
|
@@ -7,10 +7,23 @@
|
|
|
7
7
|
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
8
8
|
import { PAYOUT_COLLECTION, PAYOUT_FIELDS, PAYOUT_TRACKED_FIELDS, createPayoutId } from "../schemas";
|
|
9
9
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
10
|
-
import {
|
|
10
|
+
import { decryptPiiFields, PAYOUT_PII_FIELDS, PAYOUT_PII_INDEX_MAP, encryptPayoutBankAccount, decryptPayoutBankAccount, } from "../../../security";
|
|
11
11
|
export class PayoutRepository extends BaseRepository {
|
|
12
12
|
constructor() {
|
|
13
13
|
super(PAYOUT_COLLECTION);
|
|
14
|
+
/**
|
|
15
|
+
* Declared, not hand-rolled.
|
|
16
|
+
*
|
|
17
|
+
* `BaseRepository.applyWriteHooks` already does exactly what the private
|
|
18
|
+
* `encryptPayoutData` did — encrypt the listed fields, then add the mapped blind
|
|
19
|
+
* indices derived from the PLAINTEXT source. Declaring the fields means all
|
|
20
|
+
* nine mutating methods get it, including the `createWithId` / `*InTx` /
|
|
21
|
+
* `*InBatch` family that inherited unhooked base implementations while the
|
|
22
|
+
* encryption lived in a private method only three call sites remembered to
|
|
23
|
+
* call.
|
|
24
|
+
*/
|
|
25
|
+
this.piiFields = PAYOUT_PII_FIELDS;
|
|
26
|
+
this.piiIndexMap = PAYOUT_PII_INDEX_MAP;
|
|
14
27
|
}
|
|
15
28
|
/** Decrypt PII fields on a payout document after Firestore read */
|
|
16
29
|
decryptPayout(doc) {
|
|
@@ -21,26 +34,19 @@ export class PayoutRepository extends BaseRepository {
|
|
|
21
34
|
return decrypted;
|
|
22
35
|
}
|
|
23
36
|
/**
|
|
24
|
-
*
|
|
37
|
+
* The base hook, plus the nested objects it cannot see.
|
|
25
38
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `piiIndicesFor` returns the index fields alone, so the ciphertext cannot be
|
|
31
|
-
* overwritten. The identical bug was fixed in UserRepository and never
|
|
32
|
-
* propagated here.
|
|
39
|
+
* `piiFields` is a flat list of top-level names, so a nested blob has to be
|
|
40
|
+
* encrypted separately. Calling `super.applyWriteHooks` FIRST keeps a single
|
|
41
|
+
* encryption path with one extra step on top, rather than two parallel paths
|
|
42
|
+
* that have to agree — which is what the hand-rolled override was.
|
|
33
43
|
*/
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
39
|
-
const access = encrypted;
|
|
40
|
-
if (access.bankAccount) {
|
|
44
|
+
applyWriteHooks(data) {
|
|
45
|
+
const out = super.applyWriteHooks(data);
|
|
46
|
+
const access = out;
|
|
47
|
+
if (access.bankAccount)
|
|
41
48
|
access.bankAccount = encryptPayoutBankAccount(access.bankAccount);
|
|
42
|
-
|
|
43
|
-
return encrypted;
|
|
49
|
+
return out;
|
|
44
50
|
}
|
|
45
51
|
/** Override mapDoc to auto-decrypt PII on every Firestore read */
|
|
46
52
|
mapDoc(snap) {
|
|
@@ -61,7 +67,7 @@ export class PayoutRepository extends BaseRepository {
|
|
|
61
67
|
updatedAt: now,
|
|
62
68
|
};
|
|
63
69
|
// Encrypt PII before persisting
|
|
64
|
-
const encrypted = this.
|
|
70
|
+
const encrypted = this.applyWriteHooks(data);
|
|
65
71
|
await this.db
|
|
66
72
|
.collection(this.collection)
|
|
67
73
|
.doc(id)
|
|
@@ -70,7 +76,7 @@ export class PayoutRepository extends BaseRepository {
|
|
|
70
76
|
}
|
|
71
77
|
/** Override base update to preserve encryption/indexing for changed PII fields. */
|
|
72
78
|
async update(payoutId, data) {
|
|
73
|
-
const encrypted = this.
|
|
79
|
+
const encrypted = this.applyWriteHooks(data);
|
|
74
80
|
return super.update(payoutId, encrypted);
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback } from "react";
|
|
|
4
4
|
import { Columns, Heart, ShoppingCart } from "lucide-react";
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { useProducts } from "../../products/hooks/useProducts";
|
|
7
|
-
import { BulkActionBar, Div, FilterDrawer, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
|
|
7
|
+
import { BulkActionBar, Div, FilterDrawer, Grid, ListingToolbar, LoginRequiredModal, Pagination, Row, Stack, Text, useToast, StickyToolbar } from "../../../ui";
|
|
8
8
|
import { usePendingTable } from "../../../react/hooks/usePendingTable";
|
|
9
9
|
import { useAuthGate } from "../../../react/hooks/useAuthGate";
|
|
10
10
|
import { ACTION_ID, ACTION_META, COMPARE_MAX_ITEMS } from "../../products/constants/action-defs";
|
|
@@ -133,7 +133,6 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
133
133
|
showToast("Added to cart", "success");
|
|
134
134
|
}, [localCart, showToast]);
|
|
135
135
|
const selection = useBulkSelection({ items: preOrders, keyExtractor: (p) => p.id });
|
|
136
|
-
const gridClass = "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-[var(--appkit-space-4)]";
|
|
137
136
|
const handleBulkAddToCart = useCallback(() => {
|
|
138
137
|
const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
|
|
139
138
|
selected.forEach((p) => {
|
|
@@ -227,7 +226,7 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
|
|
|
227
226
|
setCompareIds(ids);
|
|
228
227
|
},
|
|
229
228
|
},
|
|
230
|
-
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(
|
|
229
|
+
] }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Grid, { cols: "cards", gap: "md", children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-3 w-1/2", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-8", surface: "subtle", rounded: "default" })] })] }, i))) })) : preOrders.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "start", children: "No pre-orders found." })) : view === "list" ? (_jsx(Stack, { className: "divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)]", border: "subtle", rounded: "xl", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "list", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) : (_jsx(Grid, { cols: "cards", gap: "md", children: preOrders.map((product) => (_jsx(MarketplacePreorderCard, { product: product, variant: "grid", hrefBuilder: (p) => String(ROUTES.PUBLIC.PRE_ORDER_DETAIL(p.id)), onAddToCart: handleAddToCart, wishlistActions: wishlistActions, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(CompareOverlay, { isOpen: compareIds.length > 0, productIds: compareIds, productType: "preorder", onClose: () => {
|
|
231
230
|
setCompareIds([]);
|
|
232
231
|
selection.clearSelection();
|
|
233
232
|
}, onRemove: (id) => setCompareIds((ids) => ids.filter((i) => i !== id)) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(PreOrderFilters, { table: pendingTable, currencyPrefix: "\u20B9", categoryOptions: categoryOptions, brandOptions: brandOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|