@mohasinac/appkit 4.26.0 → 4.28.0
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/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/bundles/data.d.ts +19 -0
- package/dist/_internal/server/features/bundles/data.js +32 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +27 -13
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
- package/dist/features/reviews/repository/reviews.repository.js +53 -13
- package/dist/features/reviews/schemas/firestore.d.ts +6 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +108 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +235 -13
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +20 -4
- package/dist/security/pii-schemas.js +23 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { useRouter } from "next/navigation";
|
|
6
|
-
import { Clock, Gavel,
|
|
6
|
+
import { Clock, Gavel, Play, ShoppingBag, Star } from "lucide-react";
|
|
7
|
+
import { WishlistHeartButton } from "../../wishlist/components/WishlistHeartButton";
|
|
7
8
|
import { useLongPress } from "../../../react/hooks/useLongPress";
|
|
8
9
|
import { MediaImage } from "../../media";
|
|
9
10
|
import { useWishlistToggle } from "../../wishlist";
|
|
@@ -17,19 +18,17 @@ const CLS_STAR_ICON = "h-5 w-5 fill-warning text-warning";
|
|
|
17
18
|
const CLS_LIVE_BADGE = "rounded-full bg-error-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-error-on-solid";
|
|
18
19
|
const CLS_ENDING_BADGE = "rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
|
|
19
20
|
const CLS_RESERVE_BADGE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-warning-solid px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-warning-on-solid";
|
|
20
|
-
const CLS_HEART_ON = "fill-error text-error";
|
|
21
|
-
const CLS_HEART_OFF = "text-zinc-400";
|
|
22
21
|
function renderAuctionCardHero(props) {
|
|
23
22
|
const { product, auctionHref, currentSrc, hovered, images, imageIndex, hasVideo, isEnded, isEndingSoon, isSelected, selectable, mergedLabels, onSelect, setHovered, handleSelect } = props;
|
|
24
23
|
return (_jsxs(BaseListingCard.Hero, { aspect: "square", variant: "grid", onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "absolute inset-0 block", children: _jsx(MediaImage, { src: currentSrc, alt: product.title, size: "card", fallback: "Auction image", className: `transition-transform duration-500 ${hovered && images.length > 1 ? "scale-105" : "scale-100"}` }) }), hasVideo && imageIndex === 0 ? (_jsx(Div, { className: "absolute right-2 top-2", children: _jsx(Span, { surface: "overlay-sm", layout: "flex-center", color: "inverse", className: "h-8 w-8", rounded: "full", children: _jsx(Play, { className: "h-4 w-4", "aria-label": mergedLabels.videoLabel }) }) })) : null, images.length > 1 ? (_jsx(Row, { gap: "xs", className: "pointer-events-none absolute bottom-2 left-1/2 -translate-x-1/2", children: images.map((_, index) => (_jsx(Span, { className: `transition-all duration-200 ${index === imageIndex ? "h-1.5 w-3 bg-[white]" : "h-1.5 w-1.5 bg-[white]/60"}`, rounded: "full" }, `${product.id}-image-${index}`))) })) : null, product.featured ? (_jsx(Div, { className: "pointer-events-none absolute left-2 top-2 z-10", children: _jsx(Star, { className: CLS_STAR_ICON }) })) : null, onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: handleSelect, label: isSelected ? mergedLabels.deselectItem : mergedLabels.selectItem, className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Stack, { className: `absolute left-2 z-10 ${product.featured ? "top-8" : "top-2"}`, gap: "xs", children: [!isEnded ? _jsx(Span, { size: "xs", weight: "bold", className: CLS_LIVE_BADGE, children: mergedLabels.liveBadge }) : null, isEndingSoon ? _jsx(Span, { size: "xs", weight: "bold", className: CLS_ENDING_BADGE, children: mergedLabels.endingSoon }) : null, isEnded ? _jsx(Span, { color: "inverse", size: "xs", weight: "bold", surface: "overlay-lg", rounded: "full", padding: "pill-xs", children: mergedLabels.ended }) : null, product.status === "sold" ? _jsx(Span, { color: "inverse", size: "xs", weight: "bold", surface: "overlay-xl", rounded: "full", padding: "pill-xs", children: mergedLabels.sold }) : null] }), _jsx(Div, { className: "pointer-events-none absolute bottom-2 right-2 z-10", children: _jsxs(Span, { size: "xs", weight: "semibold", className: CLS_RESERVE_BADGE, children: [_jsx(Gavel, { className: "h-3 w-3" }), mergedLabels.typeBadge] }) })] }));
|
|
25
24
|
}
|
|
26
25
|
function renderAuctionCardInfoList(props) {
|
|
27
26
|
const { product, auctionHref, displayBid, bidCount, isEnded, remaining, inWishlist, wishlistLoading, wishlistActions, countdownClass, mergedLabels, handleWishlist, handleNavigate } = props;
|
|
28
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: `flex-1 leading-snug transition-colors hover:text-primary`, color: "primary", truncate: 2, size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(
|
|
27
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: `flex-1 leading-snug transition-colors hover:text-primary`, color: "primary", truncate: 2, size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, isLoading: wishlistLoading, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), _jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", className: `items-[center] py-[0.125rem] text-[11px] ${countdownClass}`, padding: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-2.5 w-2.5" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), bidCount > 0 && _jsx(Caption, { className: "text-[11px]", children: mergedLabels.totalBids(bidCount) })] }), !isEnded && (_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", className: "self-start px-[0.625rem] mt-0.5", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }))] }));
|
|
29
28
|
}
|
|
30
29
|
function renderAuctionCardInfoGrid(props) {
|
|
31
30
|
const { product, auctionHref, displayBid, bidCount, hasCurrentBid, isEnded, remaining, resolvedBuyoutPrice, inWishlist, wishlistLoading, wishlistActions, countdownClass, mergedLabels, handleWishlist, handleNavigate } = props;
|
|
32
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Row, { align: "start", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "min-w-0 flex-1 leading-snug transition-colors hover:text-primary", color: "primary", size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(
|
|
31
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Row, { align: "start", gap: "sm", children: [_jsx(TextLink, { href: auctionHref ?? "#", className: "min-w-0 flex-1 leading-snug transition-colors hover:text-primary", color: "primary", size: "sm", weight: "medium", children: product.title }), wishlistActions ? (_jsx(WishlistHeartButton, { size: "inline", inWishlist: inWishlist, isLoading: wishlistLoading, addLabel: mergedLabels.addToWishlist, removeLabel: mergedLabels.removeFromWishlist, onToggle: handleWishlist })) : null] }), _jsxs(Div, { children: [_jsx(Caption, { children: isEnded && hasCurrentBid ? mergedLabels.winningBid : hasCurrentBid ? mergedLabels.currentBid : mergedLabels.startingBid }), _jsx(Text, { className: "leading-none text-primary", size: "base", weight: "bold", children: formatCurrency(displayBid, getDefaultCurrency()) }), isEnded && product.winnerDisplayName && (_jsxs(Caption, { className: "mt-0.5", color: "muted", children: [mergedLabels.wonBy, ": ", maskDisplayName(product.winnerDisplayName)] }))] }), _jsxs(Row, { wrap: true, justify: "between", gap: "sm", className: "gap-x-2 gap-y-1", children: [_jsxs(Div, { layout: "inline-flex", gap: "1", textWeight: "semibold", textSize: "xs", className: `items-[center] ${countdownClass}`, paddingY: "y-2xs", paddingX: "x-xs", rounded: "full", children: [_jsx(Clock, { className: "h-3 w-3" }), _jsx(Span, { children: formatCountdownLabel(remaining, mergedLabels.ended) })] }), _jsx(Caption, { children: mergedLabels.totalBids(bidCount) })] }), _jsx(Row, { wrap: true, gap: "xs", className: "mt-auto", children: isEnded ? (_jsx(Button, { gap: "xs", type: "button", variant: "ghost", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1 cursor-not-allowed opacity-60", disabled: true, children: _jsx(Span, { children: mergedLabels.ended }) })) : (_jsxs(_Fragment, { children: [_jsxs(Button, { gap: "xs", type: "button", variant: "warning", size: "sm", textSize: "xs", paddingX: "xs", className: "flex-1", onClick: handleNavigate, children: [_jsx(Gavel, { className: "h-3 w-3" }), _jsx(Span, { children: mergedLabels.placeBid })] }), resolvedBuyoutPrice ? (
|
|
33
32
|
/* `secondary`, not `danger` — this button only navigates to the
|
|
34
33
|
PDP, and `danger` is reserved for destructive actions (Rule #7
|
|
35
34
|
kinds). ACTIONS.AUCTION["buy-it-now"] is itself `secondary`. */
|
|
@@ -5,6 +5,31 @@ export interface PlaceBidInput {
|
|
|
5
5
|
bidAmount: number;
|
|
6
6
|
autoMaxBid?: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The `ActionResult` envelope every server action in this app returns.
|
|
10
|
+
*
|
|
11
|
+
* 🛑 Exactly ONE level. `onPlaceBid`/`onBuyNow` used to be typed
|
|
12
|
+
* `Promise<unknown>`, and the consumer's actions returned a *doubly*-wrapped
|
|
13
|
+
* `{ ok: true, data: { ok: false, error } }`. Reading the outer `ok` therefore
|
|
14
|
+
* saw `true` for every possible outcome — including every failure — so Buy Now
|
|
15
|
+
* placed a real bid and a real locked cart line and then silently
|
|
16
|
+
* `router.refresh()`ed a page that renders nothing from the cart. Typing these
|
|
17
|
+
* props is what makes a re-introduced second envelope a compile error rather
|
|
18
|
+
* than a silent no-op button.
|
|
19
|
+
*/
|
|
20
|
+
export type BidActionEnvelope<T> = {
|
|
21
|
+
ok: true;
|
|
22
|
+
data: T;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
code?: string;
|
|
27
|
+
issues?: unknown[];
|
|
28
|
+
};
|
|
29
|
+
/** The subset of `BuyNowAuctionResult` this component actually consumes. */
|
|
30
|
+
export interface BuyNowSuccess {
|
|
31
|
+
checkoutUrl?: string;
|
|
32
|
+
}
|
|
8
33
|
export interface PlaceBidFormClientProps {
|
|
9
34
|
productId: string;
|
|
10
35
|
currentBid: number;
|
|
@@ -17,8 +42,8 @@ export interface PlaceBidFormClientProps {
|
|
|
17
42
|
buyNowPrice: number | null;
|
|
18
43
|
bidCount: number;
|
|
19
44
|
tags?: string[];
|
|
20
|
-
onPlaceBid: (input: PlaceBidInput) => Promise<unknown
|
|
21
|
-
onBuyNow?: () => Promise<
|
|
45
|
+
onPlaceBid: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
|
|
46
|
+
onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
|
|
22
47
|
}
|
|
23
48
|
export declare function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, startingBid, tiers, minBidIncrementOverride, currency, isEnded, auctionEndDate, buyNowPrice, bidCount: ssrBidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): React.JSX.Element;
|
|
24
49
|
/**
|
|
@@ -48,6 +48,12 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
48
48
|
const [isBuyNowPending, startBuyNowTransition] = useTransition();
|
|
49
49
|
const [success, setSuccess] = useState(false);
|
|
50
50
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
51
|
+
/**
|
|
52
|
+
* Buy Now has no field to hang an error off — it is a standalone button, not
|
|
53
|
+
* a form input — so it needs its own slot. Without one, every domain failure
|
|
54
|
+
* (`AUCTION_ENDED`, `BUY_NOW_UNAVAILABLE`, rate-limit) was silent.
|
|
55
|
+
*/
|
|
56
|
+
const [buyNowError, setBuyNowError] = useState(null);
|
|
51
57
|
/** Last amount the form itself wrote — see `seedAmount` below. */
|
|
52
58
|
const lastSeeded = useRef(minBid);
|
|
53
59
|
/**
|
|
@@ -140,43 +146,59 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
function bidErrorMessage(result) {
|
|
143
|
-
if (
|
|
144
|
-
return null;
|
|
145
|
-
const r = result;
|
|
146
|
-
if (r.ok !== false)
|
|
149
|
+
if (result.ok)
|
|
147
150
|
return null;
|
|
148
|
-
return (
|
|
151
|
+
return ((result.code && BID_ERROR_DISPLAY[result.code]) ??
|
|
152
|
+
result.error ??
|
|
153
|
+
"Failed to place bid. Please try again.");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Show a Buy Now failure.
|
|
157
|
+
*
|
|
158
|
+
* Every failure used to be indistinguishable from success and was discarded.
|
|
159
|
+
* An auth failure never THROWS across a server-action boundary — it arrives
|
|
160
|
+
* as a code — so `isAuthError` in the catch below could never fire for it.
|
|
161
|
+
*
|
|
162
|
+
* ONLY `UNAUTHENTICATED` (401) means "sign in". `FORBIDDEN` (403) is "signed
|
|
163
|
+
* in, but not allowed" — bidding on your own auction, or an active
|
|
164
|
+
* `place_bids` soft ban. A login prompt for those tells the buyer to fix
|
|
165
|
+
* something that is not the problem.
|
|
166
|
+
*/
|
|
167
|
+
function reportBuyNowFailure(code, message) {
|
|
168
|
+
if (code === "UNAUTHENTICATED") {
|
|
169
|
+
setShowLoginModal(true);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
setBuyNowError((code && BID_ERROR_DISPLAY[code]) ??
|
|
173
|
+
message ??
|
|
174
|
+
"Buy Now failed. Please try again.");
|
|
149
175
|
}
|
|
150
176
|
function handleBuyNow() {
|
|
151
177
|
if (!onBuyNow)
|
|
152
178
|
return;
|
|
153
179
|
setSuccess(false);
|
|
180
|
+
setBuyNowError(null);
|
|
154
181
|
startBuyNowTransition(async () => {
|
|
155
182
|
try {
|
|
156
183
|
const result = await onBuyNow();
|
|
157
|
-
if (result
|
|
158
|
-
|
|
159
|
-
"ok" in result &&
|
|
160
|
-
result.ok === false) {
|
|
161
|
-
setSuccess(false);
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
reportBuyNowFailure(result.code, result.error);
|
|
162
186
|
return;
|
|
163
187
|
}
|
|
164
188
|
setSuccess(true);
|
|
165
189
|
// Buy-Now writes a locked cart line, so send the buyer straight to the
|
|
166
|
-
// auction checkout lane to pay for it.
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
router.push(checkoutUrl);
|
|
190
|
+
// auction checkout lane to pay for it. `buyNowAuction` deliberately
|
|
191
|
+
// never touches the product document, so falling through to
|
|
192
|
+
// `router.refresh()` re-renders a byte-identical page — which is
|
|
193
|
+
// exactly what "the button does nothing" looked like.
|
|
194
|
+
if (result.data.checkoutUrl)
|
|
195
|
+
router.push(result.data.checkoutUrl);
|
|
173
196
|
else
|
|
174
197
|
router.refresh();
|
|
175
198
|
}
|
|
176
199
|
catch (err) {
|
|
177
200
|
void normalizeError(err);
|
|
178
|
-
|
|
179
|
-
setShowLoginModal(true);
|
|
201
|
+
reportBuyNowFailure(isAuthError(err) ? "UNAUTHENTICATED" : undefined, err instanceof Error ? err.message : undefined);
|
|
180
202
|
}
|
|
181
203
|
});
|
|
182
204
|
}
|
|
@@ -212,7 +234,7 @@ export function PlaceBidFormClient({ productId, currentBid: ssrCurrentBid, start
|
|
|
212
234
|
return applyZodIssues(parsed.error.issues, setFieldError);
|
|
213
235
|
setSuccess(false);
|
|
214
236
|
startTransition(() => submitBid(parsed.data.bidAmount, setFieldError));
|
|
215
|
-
}, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", disabled: isBuyNowPending || isPending, isLoading: isBuyNowPending, onClick: handleBuyNow, children: `Buy Now — ${formatCurrency(buyNowPrice, currency)}` }), _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Skips the bidding. The auction stays live until you pay \u2014 complete checkout within ", AUCTION_BUYOUT_WINDOW_MINUTES, " minutes."] })] }))] })) }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", size: "xs", children: tag }, tag))) }) }))] }));
|
|
237
|
+
}, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", disabled: isBuyNowPending || isPending, isLoading: isBuyNowPending, onClick: handleBuyNow, children: `Buy Now — ${formatCurrency(buyNowPrice, currency)}` }), buyNowError && (_jsx(Text, { align: "center", size: "xs", variant: "error", role: "alert", children: buyNowError })), _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Skips the bidding. The auction stays live until you pay \u2014 complete checkout within ", AUCTION_BUYOUT_WINDOW_MINUTES, " minutes."] })] }))] })) }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", size: "xs", children: tag }, tag))) }) }))] }));
|
|
216
238
|
}
|
|
217
239
|
/**
|
|
218
240
|
* `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Button, Div, Row, Span } from "../../../ui";
|
|
3
3
|
export function SocialAuthButtons({ onGoogle, disabled, dividerLabel, googleLabel, }) {
|
|
4
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-
|
|
4
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "relative", children: [_jsx(Row, { className: "absolute inset-0", align: "center", children: _jsx(Div, { className: "w-full border-t border-neutral-200" }) }), _jsx(Row, { textSize: "sm", className: "relative", justify: "center", children: _jsx(Span, { padding: "x-xs", surface: "default", color: "muted", children: dividerLabel }) })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1", children: _jsxs(Button, { rounded: "md", type: "button", onClick: onGoogle, disabled: disabled, variant: "outline", paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", className: "inline- justify-[center] border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50", children: [_jsxs("svg", { className: "mr-2 h-5 w-5", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }), _jsx("path", { fill: "currentColor", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }), _jsx("path", { fill: "currentColor", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }), _jsx("path", { fill: "currentColor", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })] }), googleLabel] }) })] }));
|
|
5
5
|
}
|
|
@@ -74,10 +74,32 @@ export declare function useVerifyEmail(options?: {
|
|
|
74
74
|
onSuccess?: (data: JsonValue) => void;
|
|
75
75
|
onError?: (error: Error) => void;
|
|
76
76
|
}): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, VerifyEmailData, unknown>;
|
|
77
|
+
/**
|
|
78
|
+
* Re-send the verification email — Firebase-native, like the send that
|
|
79
|
+
* happens at registration.
|
|
80
|
+
*
|
|
81
|
+
* ## What this used to do, and why it could not work
|
|
82
|
+
*
|
|
83
|
+
* It POSTed `/api/auth/resend-verification`, a route **deleted** in the
|
|
84
|
+
* Firebase-native auth migration. Root Cause #54 is the reason it can never
|
|
85
|
+
* come back: the Admin SDK cannot send email at all — it only *generates*
|
|
86
|
+
* action links — so the only way to deliver a verification mail without
|
|
87
|
+
* routing it through Resend is the CLIENT SDK's own
|
|
88
|
+
* `sendEmailVerification()`, which uses Firebase's hosted template.
|
|
89
|
+
*
|
|
90
|
+
* So every call 404'd, and `useRegister` above says in as many words that
|
|
91
|
+
* "the user can re-request verification later" — a documented recovery path
|
|
92
|
+
* that did not exist. Found by `audit-client-verb-match`'s NO_ROUTE rule.
|
|
93
|
+
*
|
|
94
|
+
* `ResendVerificationData.email` is accepted and unused: the client SDK
|
|
95
|
+
* re-sends for the CURRENTLY signed-in user and takes no address, so honouring
|
|
96
|
+
* an arbitrary email here would be a lie about what happens. Kept in the
|
|
97
|
+
* signature so the two existing call shapes stay source-compatible.
|
|
98
|
+
*/
|
|
77
99
|
export declare function useResendVerification(options?: {
|
|
78
100
|
onSuccess?: (data: JsonValue) => void;
|
|
79
101
|
onError?: (error: Error) => void;
|
|
80
|
-
}): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, ResendVerificationData, unknown>;
|
|
102
|
+
}): import("@tanstack/react-query").UseMutationResult<JsonValue, Error, void | ResendVerificationData, unknown>;
|
|
81
103
|
export declare function useForgotPassword(options?: {
|
|
82
104
|
onSuccess?: (data: JsonValue) => void;
|
|
83
105
|
onError?: (error: Error) => void;
|
|
@@ -337,9 +337,35 @@ export function useVerifyEmail(options) {
|
|
|
337
337
|
onError: options?.onError,
|
|
338
338
|
});
|
|
339
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Re-send the verification email — Firebase-native, like the send that
|
|
342
|
+
* happens at registration.
|
|
343
|
+
*
|
|
344
|
+
* ## What this used to do, and why it could not work
|
|
345
|
+
*
|
|
346
|
+
* It POSTed `/api/auth/resend-verification`, a route **deleted** in the
|
|
347
|
+
* Firebase-native auth migration. Root Cause #54 is the reason it can never
|
|
348
|
+
* come back: the Admin SDK cannot send email at all — it only *generates*
|
|
349
|
+
* action links — so the only way to deliver a verification mail without
|
|
350
|
+
* routing it through Resend is the CLIENT SDK's own
|
|
351
|
+
* `sendEmailVerification()`, which uses Firebase's hosted template.
|
|
352
|
+
*
|
|
353
|
+
* So every call 404'd, and `useRegister` above says in as many words that
|
|
354
|
+
* "the user can re-request verification later" — a documented recovery path
|
|
355
|
+
* that did not exist. Found by `audit-client-verb-match`'s NO_ROUTE rule.
|
|
356
|
+
*
|
|
357
|
+
* `ResendVerificationData.email` is accepted and unused: the client SDK
|
|
358
|
+
* re-sends for the CURRENTLY signed-in user and takes no address, so honouring
|
|
359
|
+
* an arbitrary email here would be a lie about what happens. Kept in the
|
|
360
|
+
* signature so the two existing call shapes stay source-compatible.
|
|
361
|
+
*/
|
|
340
362
|
export function useResendVerification(options) {
|
|
341
363
|
return useMutation({
|
|
342
|
-
mutationFn: (
|
|
364
|
+
mutationFn: async () => {
|
|
365
|
+
const authProvider = getClientAuthProvider();
|
|
366
|
+
await authProvider.sendEmailVerification();
|
|
367
|
+
return { success: true };
|
|
368
|
+
},
|
|
343
369
|
onSuccess: options?.onSuccess,
|
|
344
370
|
onError: options?.onError,
|
|
345
371
|
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Permission naming: admin:resource:action
|
|
12
12
|
* Ban action naming: plain verb phrase — write_reviews, place_bids, etc.
|
|
13
13
|
*/
|
|
14
|
-
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
|
|
14
|
+
export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:audit-log:read" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:offers:read" | "admin:offers:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:tester-checklist:read" | "admin:tester-checklist:write" | "admin:tester-checklist:delete" | "admin:tester-feedback:read" | "admin:tester-feedback:write" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:shipments:read" | "admin:shipments:write" | "admin:catalogue:read" | "admin:catalogue:write" | "admin:grouped-listings:read" | "admin:grouped-listings:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
|
|
15
15
|
/**
|
|
16
16
|
* Granular user actions that can be soft-banned individually.
|
|
17
17
|
* A user may have multiple simultaneous soft bans of different types.
|
|
@@ -126,6 +126,8 @@ export const PERMISSION_GROUPS = {
|
|
|
126
126
|
],
|
|
127
127
|
catalog_manager: [
|
|
128
128
|
"admin:dashboard:view",
|
|
129
|
+
"admin:grouped-listings:read",
|
|
130
|
+
"admin:grouped-listings:write",
|
|
129
131
|
"admin:categories:read",
|
|
130
132
|
"admin:categories:write",
|
|
131
133
|
"admin:categories:delete",
|
|
@@ -277,6 +279,7 @@ export const ROUTE_PERMISSION_MAP = {
|
|
|
277
279
|
"/admin/audit-log": "admin:audit-log:read",
|
|
278
280
|
"/admin/shipments": "admin:shipments:read",
|
|
279
281
|
"/admin/catalogue-approvals": "admin:catalogue:read",
|
|
282
|
+
"/admin/grouped-listings": "admin:grouped-listings:read",
|
|
280
283
|
"/admin/categories": "admin:categories:read",
|
|
281
284
|
"/admin/brands": "admin:brands:read",
|
|
282
285
|
"/admin/coupons": "admin:coupons:read",
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import type { DocumentReference } from "firebase-admin/firestore";
|
|
1
|
+
import type { DocumentReference, DocumentSnapshot } from "firebase-admin/firestore";
|
|
2
2
|
import { BaseRepository, type FirebaseSieveFields, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
|
|
3
3
|
import { type SessionCreateInput, type SessionDocument, type SessionUpdateInput } from "../schemas";
|
|
4
4
|
export declare class SessionRepository extends BaseRepository<SessionDocument> {
|
|
5
5
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
|
+
/**
|
|
7
|
+
* Session PII, encrypted by BaseRepository's write hooks.
|
|
8
|
+
*
|
|
9
|
+
* This repository had no encryption at all — `createSession` calls
|
|
10
|
+
* `createWithId`, and before the hooks existed that method applied nothing —
|
|
11
|
+
* so device IPs, user agents and city were stored in cleartext.
|
|
12
|
+
*
|
|
13
|
+
* `deviceInfo.browser` / `deviceInfo.os` are deliberately NOT here: they are
|
|
14
|
+
* low-entropy strings the admin sessions view filters on (see SIEVE_FIELDS
|
|
15
|
+
* above), and encrypting them would break those queries while revealing
|
|
16
|
+
* nothing an IP does not.
|
|
17
|
+
*/
|
|
18
|
+
protected piiFields: readonly ["deviceInfo.ip", "deviceInfo.userAgent", "location.city"];
|
|
6
19
|
constructor();
|
|
20
|
+
/** Decrypt on read so callers see the same shape they wrote. */
|
|
21
|
+
protected mapDoc<D = SessionDocument>(snap: DocumentSnapshot): D;
|
|
7
22
|
list(model: SieveModel): Promise<FirebaseSieveResult<SessionDocument>>;
|
|
8
23
|
listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<SessionDocument>>;
|
|
9
24
|
createSession(userId: string, data: Omit<SessionCreateInput, "userId" | "expiresAt" | "isActive">): Promise<SessionDocument>;
|
|
@@ -3,10 +3,33 @@ import { DatabaseError } from "../../../errors";
|
|
|
3
3
|
import { serverLogger } from "../../../monitoring";
|
|
4
4
|
import { BaseRepository, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
5
5
|
import { generateSessionId } from "../token-helpers";
|
|
6
|
+
import { decryptPiiFields } from "../../../security/pii-encrypt";
|
|
6
7
|
import { SESSION_COLLECTION, SESSION_EXPIRATION_MS, SESSION_FIELDS, } from "../schemas";
|
|
7
8
|
export class SessionRepository extends BaseRepository {
|
|
8
9
|
constructor() {
|
|
9
10
|
super(SESSION_COLLECTION);
|
|
11
|
+
/**
|
|
12
|
+
* Session PII, encrypted by BaseRepository's write hooks.
|
|
13
|
+
*
|
|
14
|
+
* This repository had no encryption at all — `createSession` calls
|
|
15
|
+
* `createWithId`, and before the hooks existed that method applied nothing —
|
|
16
|
+
* so device IPs, user agents and city were stored in cleartext.
|
|
17
|
+
*
|
|
18
|
+
* `deviceInfo.browser` / `deviceInfo.os` are deliberately NOT here: they are
|
|
19
|
+
* low-entropy strings the admin sessions view filters on (see SIEVE_FIELDS
|
|
20
|
+
* above), and encrypting them would break those queries while revealing
|
|
21
|
+
* nothing an IP does not.
|
|
22
|
+
*/
|
|
23
|
+
this.piiFields = [
|
|
24
|
+
"deviceInfo.ip",
|
|
25
|
+
"deviceInfo.userAgent",
|
|
26
|
+
"location.city",
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/** Decrypt on read so callers see the same shape they wrote. */
|
|
30
|
+
mapDoc(snap) {
|
|
31
|
+
const raw = super.mapDoc(snap);
|
|
32
|
+
return decryptPiiFields(raw, [...this.piiFields]);
|
|
10
33
|
}
|
|
11
34
|
async list(model) {
|
|
12
35
|
return this.sieveQuery(model, SessionRepository.SIEVE_FIELDS);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize";
|
|
2
2
|
import { DatabaseError } from "../../../errors";
|
|
3
3
|
import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
4
|
-
import { USER_PII_FIELDS, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
|
|
4
|
+
import { USER_PII_FIELDS, USER_PII_INDEX_MAP, piiIndicesFor, decryptPayoutDetails, decryptPiiFields, decryptShippingConfig, encryptPayoutDetails, encryptPiiFields, encryptShippingConfig, piiBlindIndex, } from "../../../security";
|
|
5
5
|
import { USER_COLLECTION, USER_FIELDS, createUserId, } from "../schemas";
|
|
6
6
|
export class UserRepository extends BaseRepository {
|
|
7
7
|
constructor() {
|
|
@@ -23,7 +23,18 @@ export class UserRepository extends BaseRepository {
|
|
|
23
23
|
// addPiiIndices is intentionally NOT called here — it spreads the original
|
|
24
24
|
// plaintext `data` back into the result, which would overwrite the encrypted
|
|
25
25
|
// ciphertext with the original plaintext values, defeating the encryption.
|
|
26
|
-
|
|
26
|
+
//
|
|
27
|
+
// The mapped indices below are not optional. `encryptPiiFields` derives its
|
|
28
|
+
// sibling name mechanically as `${field}Index`, so `phoneNumber` produces
|
|
29
|
+
// `phoneNumberIndex` — but `findByPhone` queries `USER_FIELDS.PHONE_INDEX`,
|
|
30
|
+
// which is `phoneIndex`. Only the seed paths ever wrote that name, so
|
|
31
|
+
// **phone lookup could never match a user created by the app.**
|
|
32
|
+
// `piiIndicesFor` applies USER_PII_INDEX_MAP's real names and, unlike
|
|
33
|
+
// `addPiiIndices`, carries no plaintext back over the ciphertext.
|
|
34
|
+
const encrypted = {
|
|
35
|
+
...encryptPiiFields(data, [...USER_PII_FIELDS]),
|
|
36
|
+
...piiIndicesFor(data, USER_PII_INDEX_MAP),
|
|
37
|
+
};
|
|
27
38
|
const access = encrypted;
|
|
28
39
|
if (access.payoutDetails) {
|
|
29
40
|
access.payoutDetails = encryptPayoutDetails(access.payoutDetails);
|
|
@@ -25,7 +25,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
|
|
|
25
25
|
: "";
|
|
26
26
|
const isInteractive = !!(href || onClick);
|
|
27
27
|
const card = (_jsxs(Article, { role: onClick && !href ? "button" : undefined, tabIndex: onClick && !href ? 0 : undefined, onKeyDown: onClick && !href
|
|
28
|
-
? (e) => (e.key === "Enter" || e.key === "
|
|
28
|
+
? (e) => (e.key === "Enter" || e.key === "") && onClick(post)
|
|
29
29
|
: undefined, onClick: onClick && !href ? () => onClick(post) : undefined, className: `group flex flex-col h-full overflow-hidden rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] shadow-sm transition hover:shadow-md ${isInteractive ? "cursor-pointer" : ""} ${className}`, children: [_jsx(Div, { className: `relative aspect-video w-full ${__O.hidden} bg-[var(--appkit-color-surface)] dark:bg-[var(--appkit-color-surface-elevated)] flex-shrink-0`, children: coverImageUrl ? (_jsx(MediaImage, { src: coverImageUrl, alt: post.title, size: "card", className: "transition-transform duration-300 group-hover:scale-105" })) : (_jsx(Row, { surface: "muted", className: "h-full w-full", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-30", size: "4xl", "aria-hidden": "true", children: "\u270D\uFE0F" }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p5}`, children: [_jsxs(Row, { className: "mb-2", gap: "sm", wrap: true, children: [_jsx(Span, { size: "xs", weight: "medium", className: "bg-primary/10 text-primary", rounded: "full", padding: "pill-xs", transform: "capitalize", children: post.category }), post.isFeatured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_FEATURED_BADGE, children: "Featured" })), post.readTimeMinutes && (_jsxs(Span, { size: "xs", color: "muted", children: [post.readTimeMinutes, " min read"] }))] }), _jsx(Heading, { level: 3, className: `group-hover:text-primary`, color: "primary", truncate: 2, size: "base", weight: "semibold", children: post.title }), post.excerpt && (_jsx(Text, { className: `mt-2 flex-1`, color: "muted", truncate: 3, size: "sm", children: post.excerpt })), _jsxs(Row, { className: "mt-auto", padding: "t-sm", gap: "3", children: [post.authorAvatar ? (_jsx(Div, { className: "relative h-7 w-7 flex-shrink-0", rounded: "full", overflow: "hidden", children: _jsx(MediaImage, { src: post.authorAvatar, alt: post.authorName ?? "author", size: "avatar" }) })) : post.authorName ? (_jsx(Row, { textWeight: "bold", textSize: "xs", className: "h-7 w-7 flex-shrink-0 bg-primary/10 text-primary", align: "center", justify: "center", rounded: "full", children: post.authorName.charAt(0).toUpperCase() })) : null, _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] min-w-0", size: "xs", children: [post.authorName && (_jsx(Span, { weight: "medium", className: "text-neutral-700", children: safeDisplayName(post.authorName, "Author") })), date && _jsxs(Span, { className: "ml-1 text-neutral-400", children: ["\u00B7 ", date] })] })] })] })] }));
|
|
30
30
|
if (href) {
|
|
31
31
|
return (_jsx(Link, { href: href, className: "block h-full", children: card }));
|
|
@@ -33,7 +33,7 @@ export function BlogCard({ post, href, onClick, className = "" }) {
|
|
|
33
33
|
return card;
|
|
34
34
|
}
|
|
35
35
|
export function BlogCategoryTabs({ categories, active, onSelect, labels = {}, }) {
|
|
36
|
-
return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-
|
|
36
|
+
return (_jsxs(Div, { layout: "flex", gap: "2", className: `scrollbar-none ${__O.xAuto}`, padding: "b-2xs", children: [_jsx(Button, { onClick: () => onSelect(null), variant: !active ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition ${!active ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels.all ?? "All" }), categories.map((cat) => (_jsx(Button, { onClick: () => onSelect(cat), variant: active === cat ? "primary" : "ghost", size: "sm", className: `whitespace-nowrap rounded-full px-[var(--appkit-space-4)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium capitalize transition ${active === cat ? "bg-neutral-900 text-white" : "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: labels[cat] ?? cat }, cat)))] }));
|
|
37
37
|
}
|
|
38
38
|
export function BlogListView({ posts, isLoading, totalPages = 1, currentPage = 1, total = 0, onPageChange, onPostClick, emptyLabel = "No posts found", slots, }) {
|
|
39
39
|
if (isLoading) {
|
|
@@ -55,7 +55,17 @@ export interface CartPriceBreakdownProps {
|
|
|
55
55
|
unavailableNote?: React.ReactNode;
|
|
56
56
|
/** Subtotal to show when `preview` is null (guest carts). */
|
|
57
57
|
fallbackSubtotal?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Shown when the preview request FAILED, as opposed to never having run.
|
|
60
|
+
*
|
|
61
|
+
* Separate from `unavailableNote` because the two say different things and
|
|
62
|
+
* the failure case can coexist with a rendered breakdown: on a refetch error
|
|
63
|
+
* the last good `preview` is deliberately kept, so the buyer sees real
|
|
64
|
+
* figures plus a note that they may be out of date — strictly better than
|
|
65
|
+
* blanking to ₹0, which is what a swallowed error used to produce.
|
|
66
|
+
*/
|
|
67
|
+
errorNote?: React.ReactNode;
|
|
58
68
|
isLoading?: boolean;
|
|
59
69
|
className?: string;
|
|
60
70
|
}
|
|
61
|
-
export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
|
|
71
|
+
export declare function CartPriceBreakdown({ preview, itemCount, currency, unavailableNote, fallbackSubtotal, errorNote, isLoading, className, }: CartPriceBreakdownProps): React.JSX.Element;
|
|
@@ -9,10 +9,10 @@ function Line({ label, amount, tone = "muted", strong = false, }) {
|
|
|
9
9
|
function withStoreCount(label, count) {
|
|
10
10
|
return count > 1 ? `${label} (${count} stores)` : label;
|
|
11
11
|
}
|
|
12
|
-
export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, isLoading = false, className = "", }) {
|
|
12
|
+
export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unavailableNote, fallbackSubtotal = 0, errorNote, isLoading = false, className = "", }) {
|
|
13
13
|
const money = (value) => formatCurrency(value, currency);
|
|
14
14
|
if (!preview) {
|
|
15
|
-
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), unavailableNote && (_jsx(Text, { size: "xs", color: "muted", children: unavailableNote }))] }));
|
|
15
|
+
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: itemCount !== undefined ? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""})` : "Subtotal", amount: money(fallbackSubtotal) }), (errorNote ?? unavailableNote) && (_jsx(Text, { size: "xs", color: errorNote ? "error" : "muted", children: errorNote ?? unavailableNote }))] }));
|
|
16
16
|
}
|
|
17
17
|
const storeCount = preview.stores.length;
|
|
18
18
|
// Count how many stores are actually charged each per-store add-on, so the
|
|
@@ -21,5 +21,5 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
|
|
|
21
21
|
const subtotalLabel = itemCount !== undefined
|
|
22
22
|
? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
|
|
23
23
|
: "Subtotal";
|
|
24
|
-
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" }))] }));
|
|
24
|
+
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee) })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.platformFee > 0 && _jsx(Line, { label: "Platform fee", amount: money(preview.platformFee) }), (preview.gstAmount > 0 || preview.gstOnFee > 0) && (_jsx(Line, { label: "GST", amount: money(preview.gstAmount + preview.gstOnFee) })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: true }) }), errorNote ? (_jsx(Text, { size: "xs", color: "error", children: errorNote })) : (isLoading && (_jsx(Text, { size: "xs", color: "muted", children: "Updating\u2026" })))] }));
|
|
25
25
|
}
|
|
@@ -24,6 +24,15 @@ export interface StoreAddonsRates {
|
|
|
24
24
|
shipmentProtectionFeePercent?: number;
|
|
25
25
|
shipmentProtectionFeeMin?: number;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* True when the site offers at least one paid add-on.
|
|
29
|
+
*
|
|
30
|
+
* Exported because the picker renders NOTHING when the answer is false, and a
|
|
31
|
+
* parent that doesn't know that ends up drawing a bare "Add-ons" heading over
|
|
32
|
+
* empty space. Callers use this to decide between mounting the picker and
|
|
33
|
+
* showing an explanatory line instead.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasAnyStoreAddon(rates: StoreAddonsRates | null | undefined): boolean;
|
|
27
36
|
export interface StoreAddonsPickerProps {
|
|
28
37
|
storeId: string;
|
|
29
38
|
/** This store's subtotal — shipment protection is a percentage of it. */
|
|
@@ -6,12 +6,22 @@ import { IconBox } from "../../../ui/components/IconBox";
|
|
|
6
6
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
7
7
|
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
8
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* True when the site offers at least one paid add-on.
|
|
11
|
+
*
|
|
12
|
+
* Exported because the picker renders NOTHING when the answer is false, and a
|
|
13
|
+
* parent that doesn't know that ends up drawing a bare "Add-ons" heading over
|
|
14
|
+
* empty space. Callers use this to decide between mounting the picker and
|
|
15
|
+
* showing an explanatory line instead.
|
|
16
|
+
*/
|
|
17
|
+
export function hasAnyStoreAddon(rates) {
|
|
18
|
+
return (!!rates &&
|
|
19
|
+
!!(rates.whatsappNotifyFeeEnabled ||
|
|
12
20
|
rates.giftWrapFeeEnabled ||
|
|
13
|
-
rates.shipmentProtectionFeeEnabled);
|
|
14
|
-
|
|
21
|
+
rates.shipmentProtectionFeeEnabled));
|
|
22
|
+
}
|
|
23
|
+
export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rates, disabled = false, disabledReason, showGiftMessage = false, currency = "INR", className = "", }) {
|
|
24
|
+
if (!hasAnyStoreAddon(rates) || !rates)
|
|
15
25
|
return null;
|
|
16
26
|
const money = (value) => formatCurrency(value, currency);
|
|
17
27
|
const set = (patch) => onChange(storeId, { ...value, ...patch });
|
|
@@ -5,7 +5,7 @@ export { CartView } from "./CartView";
|
|
|
5
5
|
export type { CartViewProps, CartOrderGroup } from "./CartView";
|
|
6
6
|
export { CartSummary } from "./CartSummary";
|
|
7
7
|
export type { CartSummaryProps } from "./CartSummary";
|
|
8
|
-
export { StoreAddonsPicker } from "./StoreAddonsPicker";
|
|
8
|
+
export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
|
|
9
9
|
export type { StoreAddonsPickerProps, StoreAddonsValue, StoreAddonsRates, } from "./StoreAddonsPicker";
|
|
10
10
|
export { CartPriceBreakdown } from "./CartPriceBreakdown";
|
|
11
11
|
export type { CartPriceBreakdownProps, CartPriceBreakdownData, CartPriceBreakdownStore, } from "./CartPriceBreakdown";
|
|
@@ -2,7 +2,7 @@ export { CartItemRow, CartDrawer } from "./CartDrawer";
|
|
|
2
2
|
export { CartGroupLineRow } from "./CartGroupLineRow";
|
|
3
3
|
export { CartView } from "./CartView";
|
|
4
4
|
export { CartSummary } from "./CartSummary";
|
|
5
|
-
export { StoreAddonsPicker } from "./StoreAddonsPicker";
|
|
5
|
+
export { StoreAddonsPicker, hasAnyStoreAddon } from "./StoreAddonsPicker";
|
|
6
6
|
export { CartPriceBreakdown } from "./CartPriceBreakdown";
|
|
7
7
|
export { CheckoutView } from "./CheckoutView";
|
|
8
8
|
export { CheckoutAddressStep } from "./CheckoutAddressStep";
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { type GuestCartItem } from "../utils/guest-cart";
|
|
2
|
+
import { type NormalizedError } from "../../../errors/normalize";
|
|
2
3
|
export interface UseGuestCartMergeOptions {
|
|
3
4
|
userId: string | null | undefined;
|
|
4
5
|
mergeEndpoint?: string;
|
|
5
6
|
onNavigate?: (url: string) => void;
|
|
6
7
|
mergeFn?: (items: GuestCartItem[]) => Promise<void>;
|
|
7
8
|
onMerged?: (count: number) => void;
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Receives a typed NormalizedError, not the raw `unknown` — the hook is the
|
|
11
|
+
* boundary that knows how to classify the failure, so every consumer would
|
|
12
|
+
* otherwise repeat the same narrowing. Zero external consumers at the time of
|
|
13
|
+
* narrowing, so this broke nothing.
|
|
14
|
+
*/
|
|
15
|
+
onMergeFailed?: (error: NormalizedError) => void;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* useGuestCartMerge
|
|
@@ -3,6 +3,7 @@ import { useEffect, useRef } from "react";
|
|
|
3
3
|
import { apiClient } from "../../../http";
|
|
4
4
|
import { clearGuestCart, clearGuestReturnTo, getGuestCartItems, getGuestReturnTo, } from "../utils/guest-cart";
|
|
5
5
|
import { CART_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
6
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
6
7
|
/**
|
|
7
8
|
* useGuestCartMerge
|
|
8
9
|
*
|
|
@@ -42,7 +43,7 @@ export function useGuestCartMerge(options) {
|
|
|
42
43
|
}
|
|
43
44
|
})
|
|
44
45
|
.catch((err) => {
|
|
45
|
-
options.onMergeFailed?.(err);
|
|
46
|
+
options.onMergeFailed?.(normalizeError(err));
|
|
46
47
|
if (returnTo)
|
|
47
48
|
clearGuestReturnTo();
|
|
48
49
|
});
|