@mohasinac/appkit 4.27.0 → 4.29.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/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/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- 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/AdminContentGuideView.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/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -1
- package/dist/features/blog/schemas/firestore.js +1 -0
- 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/events/repository/events.repository.js +1 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -1
- package/dist/features/events/schemas/firestore.js +1 -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/messages/repository/conversations.repository.js +15 -5
- 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 +38 -16
- 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 +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +54 -13
- package/dist/features/reviews/schemas/firestore.d.ts +9 -1
- package/dist/features/reviews/schemas/firestore.js +1 -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/stores/repository/store.repository.js +1 -0
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- 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/repository/tester-checklist-item.repository.d.ts +0 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +6 -15
- package/dist/features/tester/schemas/firestore.d.ts +5 -4
- package/dist/features/tester/schemas/firestore.js +3 -3
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- 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 +32 -4
- package/dist/security/pii-schemas.js +38 -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/_helpers/search-txt-wrappers.d.ts +20 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +73 -0
- package/dist/seed/blog-posts-seed-data.d.ts +4 -0
- package/dist/seed/blog-posts-seed-data.js +7 -1
- package/dist/seed/events-seed-data.d.ts +5 -1
- package/dist/seed/events-seed-data.js +7 -1
- 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/reviews-seed-data.js +2 -1
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seed/stores-seed-data.d.ts +4 -0
- package/dist/seed/stores-seed-data.js +7 -1
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- 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
|
@@ -12,6 +12,7 @@ import { bidRepository } from "../repository/bid.repository";
|
|
|
12
12
|
import { productRepository } from "../../products/repository/products.repository";
|
|
13
13
|
import { isAuctionListing } from "../../products/utils/listing-type";
|
|
14
14
|
import { userRepository } from "../../auth/repository/user.repository";
|
|
15
|
+
import { isSoftBanned } from "../../auth/server/checkSoftBan";
|
|
15
16
|
import { unitOfWork } from "../../../core/unit-of-work";
|
|
16
17
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
17
18
|
import { siteSettingsRepository } from "../../../repositories";
|
|
@@ -28,6 +29,29 @@ import { cartRepository } from "../../cart/repository/cart.repository";
|
|
|
28
29
|
import { AUCTION_BUYOUT_WINDOW_MINUTES, AUCTION_BUYOUT_WINDOW_MS, AUCTION_CHECKOUT_URL, } from "../../../_internal/shared/checkout/lanes";
|
|
29
30
|
import { resolveDate } from "../../../utils";
|
|
30
31
|
// --- Domain Functions ----------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Load the bidder's profile and refuse if they carry an active `place_bids`
|
|
34
|
+
* soft ban. Returns the profile so the caller pays for exactly one read.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 This belongs in the DOMAIN function, not in a route.
|
|
37
|
+
*
|
|
38
|
+
* Admins can issue a `place_bids` soft ban and `BID_ERROR_CODES.USER_BANNED`
|
|
39
|
+
* has always existed with a display-map entry and client copy — but nothing
|
|
40
|
+
* ever threw it. The only `isSoftBanned(user, "place_bids")` call in the
|
|
41
|
+
* codebase sat in `POST /api/auctions/[id]/buy-now`, a route with zero callers,
|
|
42
|
+
* while the live path (the `placeBidAction` / `buyNowAction` server actions)
|
|
43
|
+
* checked nothing. A banned user could bid and buy out freely. Enforcing it
|
|
44
|
+
* here means every caller — action, route, or job — is covered by
|
|
45
|
+
* construction.
|
|
46
|
+
*/
|
|
47
|
+
async function assertCanBid(userId) {
|
|
48
|
+
const profile = await userRepository.findById(userId);
|
|
49
|
+
if (profile && isSoftBanned(profile, "place_bids")) {
|
|
50
|
+
const ban = profile.softBans?.find((b) => b.action === "place_bids");
|
|
51
|
+
throw new AuthorizationError(`Your account is restricted from bidding. Reason: ${ban?.reason ?? "Policy violation"}.`, { code: BID_ERROR_CODES.USER_BANNED });
|
|
52
|
+
}
|
|
53
|
+
return profile ?? null;
|
|
54
|
+
}
|
|
31
55
|
export async function placeBid(userId, userEmail, input) {
|
|
32
56
|
const { productId, bidAmount, autoMaxBid } = input;
|
|
33
57
|
const product = await productRepository.findById(productId);
|
|
@@ -50,6 +74,9 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
50
74
|
throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
77
|
+
// Before any write. The profile is reused below for the bid's denormalised
|
|
78
|
+
// userName/userEmail, so the ban check costs no extra read.
|
|
79
|
+
const profile = await assertCanBid(userId);
|
|
53
80
|
// An auction with no bids yet prices off its starting bid, and that starting
|
|
54
81
|
// bid is itself acceptable — see ResolveMinBidOptions.hasBids.
|
|
55
82
|
const hasBids = (product.currentBid ?? 0) > 0 || (product.bidCount ?? 0) > 0;
|
|
@@ -122,7 +149,6 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
122
149
|
visibleBid = newCap;
|
|
123
150
|
bumpedPreviousVisible = Math.min(prevCap, target);
|
|
124
151
|
}
|
|
125
|
-
const profile = await userRepository.findById(userId);
|
|
126
152
|
const bid = await bidRepository.create({
|
|
127
153
|
productId,
|
|
128
154
|
productTitle: product.title,
|
|
@@ -229,12 +255,24 @@ export async function placeBid(userId, userEmail, input) {
|
|
|
229
255
|
return { bid };
|
|
230
256
|
}
|
|
231
257
|
export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
232
|
-
const { productId } = input;
|
|
233
|
-
|
|
258
|
+
const { productId: productRef } = input;
|
|
259
|
+
// Auction URLs are slug-based, and this action is reachable from a route
|
|
260
|
+
// param, so accept either. Deliberately id-FIRST rather than
|
|
261
|
+
// `findByIdOrSlug`, which queries by slug first and would therefore cost two
|
|
262
|
+
// reads on the common id path (Rule #6) — here the fallback query only runs
|
|
263
|
+
// when the id lookup actually misses.
|
|
264
|
+
//
|
|
265
|
+
// This was masked because every seeded auction has `slug === id`. Any auction
|
|
266
|
+
// whose slug differs threw NotFoundError, which the caller then swallowed.
|
|
267
|
+
const product = (await productRepository.findById(productRef)) ??
|
|
268
|
+
(await productRepository.findBySlug(productRef));
|
|
234
269
|
if (!product)
|
|
235
270
|
throw new NotFoundError(ERROR_MESSAGES.BID.AUCTION_NOT_FOUND);
|
|
236
271
|
if (!isAuctionListing(product))
|
|
237
272
|
throw new ValidationError(ERROR_MESSAGES.BID.NOT_AN_AUCTION);
|
|
273
|
+
// 🛑 Every downstream write (the bid, the locked cart line, the notification)
|
|
274
|
+
// must key on the canonical document id, never on the ref the caller passed.
|
|
275
|
+
const productId = String(product.id);
|
|
238
276
|
if (product.isSold) {
|
|
239
277
|
throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
|
|
240
278
|
}
|
|
@@ -250,6 +288,10 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
|
250
288
|
throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
|
|
251
289
|
}
|
|
252
290
|
}
|
|
291
|
+
// Same guard as `placeBid` — a buyout IS a bid (it writes a real
|
|
292
|
+
// `BidDocument` with `isBuyout: true`), so a `place_bids` ban must block it.
|
|
293
|
+
// The profile is reused for the bid's denormalised name/email below.
|
|
294
|
+
const profile = await assertCanBid(userId);
|
|
253
295
|
const buyNowPrice = typeof product.buyNowPrice === "number" ? product.buyNowPrice : null;
|
|
254
296
|
if (buyNowPrice === null || buyNowPrice <= 0) {
|
|
255
297
|
throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_NO_PRICE, { code: BID_ERROR_CODES.BUY_NOW_NO_PRICE });
|
|
@@ -286,7 +328,6 @@ export async function buyNowAuction(userId, userName, userEmail, input) {
|
|
|
286
328
|
// The product's own currency, not the site default — the cart line, the bid
|
|
287
329
|
// and the listing all have to agree, and settlement uses the bid's currency.
|
|
288
330
|
const currency = product.currency || getDefaultCurrency();
|
|
289
|
-
const profile = await userRepository.findById(userId);
|
|
290
331
|
// A real bid at the buy-now price — this is what "the bid placed is the
|
|
291
332
|
// buyout price by the buyer" means. It starts `active`/`isWinning: false`
|
|
292
333
|
// like any other bid (bidRepository.create forces both) and is promoted to
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
currency: string;
|
|
1
|
+
import { type PlaceBidFormClientProps } from "./PlaceBidFormClient";
|
|
2
|
+
export interface AuctionBottomActionsProps extends PlaceBidFormClientProps {
|
|
4
3
|
bidCount: number;
|
|
5
|
-
isEnded: boolean;
|
|
6
|
-
auctionEndDate: Date | null;
|
|
7
4
|
}
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Mobile/after-scroll CTA bar for an auction.
|
|
7
|
+
*
|
|
8
|
+
* 🛑 It owns its own modal rather than pointing at one.
|
|
9
|
+
*
|
|
10
|
+
* Two defects lived here. (a) The single Place Bid action ran
|
|
11
|
+
* `getElementById("auction-bid-form")?.scrollIntoView(...)` — but that id is
|
|
12
|
+
* set inside `renderBidForm`, which `AuctionDetailView` wraps in
|
|
13
|
+
* `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
|
|
14
|
+
* action did nothing on the only viewport the bar exists for. A DOM id is not
|
|
15
|
+
* a contract; the element it names can be hidden, moved or removed without any
|
|
16
|
+
* signal here. (b) There was no Buy Now action at all, even though
|
|
17
|
+
* `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
|
|
18
|
+
* for a long time — a declared CTA nothing rendered (Root Cause #8's shape).
|
|
19
|
+
*
|
|
20
|
+
* Both actions now open the real form, which is the only place the Buy Now
|
|
21
|
+
* button can report an error or route to `/checkout?lane=auction`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function AuctionBottomActions(props: AuctionBottomActionsProps): import("react").JSX.Element;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
2
4
|
import { useBottomActions } from "../../layout/hooks/useBottomActions";
|
|
3
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
4
6
|
import { ACTION_ID, ACTION_META } from "../../products/constants/action-defs";
|
|
5
7
|
import { useCountdown } from "../../../react";
|
|
8
|
+
import { Modal } from "../../../ui";
|
|
9
|
+
import { isBuyNowAvailable } from "../../../_internal/shared/features/auctions/config";
|
|
10
|
+
import { PlaceBidFormClient, } from "./PlaceBidFormClient";
|
|
6
11
|
function formatCountdownLabel(remaining) {
|
|
7
12
|
if (!remaining)
|
|
8
13
|
return undefined;
|
|
@@ -13,25 +18,51 @@ function formatCountdownLabel(remaining) {
|
|
|
13
18
|
return `Ends in ${hours}h ${minutes}m ${seconds}s`;
|
|
14
19
|
return `Ends in ${minutes}m ${seconds}s`;
|
|
15
20
|
}
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Mobile/after-scroll CTA bar for an auction.
|
|
23
|
+
*
|
|
24
|
+
* 🛑 It owns its own modal rather than pointing at one.
|
|
25
|
+
*
|
|
26
|
+
* Two defects lived here. (a) The single Place Bid action ran
|
|
27
|
+
* `getElementById("auction-bid-form")?.scrollIntoView(...)` — but that id is
|
|
28
|
+
* set inside `renderBidForm`, which `AuctionDetailView` wraps in
|
|
29
|
+
* `hidden lg:block`. On mobile the target is `display:none`, so the bar's only
|
|
30
|
+
* action did nothing on the only viewport the bar exists for. A DOM id is not
|
|
31
|
+
* a contract; the element it names can be hidden, moved or removed without any
|
|
32
|
+
* signal here. (b) There was no Buy Now action at all, even though
|
|
33
|
+
* `MOBILE_PRIMARY_ACTIONS.auction` has declared `[PLACE_BID, BUY_NOW_AUCTION]`
|
|
34
|
+
* for a long time — a declared CTA nothing rendered (Root Cause #8's shape).
|
|
35
|
+
*
|
|
36
|
+
* Both actions now open the real form, which is the only place the Buy Now
|
|
37
|
+
* button can report an error or route to `/checkout?lane=auction`.
|
|
38
|
+
*/
|
|
39
|
+
export function AuctionBottomActions(props) {
|
|
40
|
+
const { currentBid, currency, bidCount, isEnded, auctionEndDate, buyNowPrice } = props;
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
18
42
|
const remaining = useCountdown(auctionEndDate ?? undefined);
|
|
43
|
+
const placeBidMeta = ACTION_META[ACTION_ID.PLACE_BID];
|
|
44
|
+
const buyNowMeta = ACTION_META[ACTION_ID.BUY_NOW_AUCTION];
|
|
45
|
+
// Same single predicate the form and the card use. `!!props.onBuyNow` matters
|
|
46
|
+
// because the preview/demo mount passes no action.
|
|
47
|
+
const buyNowAvailable = isBuyNowAvailable({ buyNowPrice, currentBid, isEnded }) && !!props.onBuyNow;
|
|
19
48
|
useBottomActions(isEnded
|
|
20
49
|
? {}
|
|
21
50
|
: {
|
|
22
51
|
actions: [
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
{ ...placeBidMeta, onClick: () => setOpen(true) },
|
|
53
|
+
...(buyNowAvailable && buyNowPrice !== null
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
...buyNowMeta,
|
|
57
|
+
label: `Buy Now — ${formatCurrency(buyNowPrice, currency)}`,
|
|
58
|
+
onClick: () => setOpen(true),
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: []),
|
|
31
62
|
],
|
|
32
63
|
secondaryLabel: formatCountdownLabel(remaining),
|
|
33
64
|
infoLabel: `${formatCurrency(currentBid, currency)} · ${bidCount} bid${bidCount !== 1 ? "s" : ""}`,
|
|
34
65
|
desktop: "after-scroll",
|
|
35
66
|
});
|
|
36
|
-
return
|
|
67
|
+
return (_jsx(Modal, { isOpen: open, onClose: () => setOpen(false), size: "md", title: "Place your bid", children: _jsx(PlaceBidFormClient, { ...props }) }));
|
|
37
68
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProductFeatureDocument } from "../../products/schemas/product-features";
|
|
2
|
-
import type { PlaceBidInput } from "./PlaceBidFormClient";
|
|
2
|
+
import type { PlaceBidInput, BidActionEnvelope, BuyNowSuccess } from "./PlaceBidFormClient";
|
|
3
3
|
export interface AuctionDetailPageViewProps {
|
|
4
4
|
id: string;
|
|
5
5
|
/**
|
|
@@ -9,8 +9,13 @@ export interface AuctionDetailPageViewProps {
|
|
|
9
9
|
* When absent, falls back to fetching by slug/id (backward compat).
|
|
10
10
|
*/
|
|
11
11
|
initialAuction?: import("../../products/schemas/firestore").ProductDocument | null;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Typed against the single `ActionResult` envelope — see the note on
|
|
14
|
+
* `BidActionEnvelope`. `Promise<unknown>` here is what let a doubly-wrapped
|
|
15
|
+
* result flow through to a client that read the wrong `ok`.
|
|
16
|
+
*/
|
|
17
|
+
onPlaceBid?: (input: PlaceBidInput) => Promise<BidActionEnvelope<unknown>>;
|
|
18
|
+
onBuyNow?: () => Promise<BidActionEnvelope<BuyNowSuccess>>;
|
|
14
19
|
/** SSR-loaded productFeatures (platform + store-scope). See ProductDetailPageView for semantics. */
|
|
15
20
|
productFeatures?: ProductFeatureDocument[];
|
|
16
21
|
}
|
|
@@ -46,7 +46,7 @@ function toDescriptionHtml(raw) {
|
|
|
46
46
|
}
|
|
47
47
|
function renderAuctionInfoPanel(props) {
|
|
48
48
|
const { productId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
|
|
49
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-
|
|
49
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), isEnded ? (_jsx(Span, { color: "error", surface: "danger-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Ended" })) : (_jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "medium", className: "inline-block", padding: "pill-sm", rounded: "full", children: "Active" }))] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: formatCurrency(buyNowPrice, currency) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Store reviews for the auction's seller.
|
|
@@ -204,5 +204,5 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
204
204
|
slug: typeof r.slug === "string" ? r.slug : undefined,
|
|
205
205
|
}));
|
|
206
206
|
return (_jsxs(Stack, { gap: "xl", children: [related.length > 0 && (_jsx(RelatedProducts, { labels: { title: "Similar Auctions" }, renderGrid: () => (_jsx(MarketplaceAuctionGrid, { auctions: related, gridClassName: "grid grid-cols-2 gap-[var(--appkit-space-4)] sm:grid-cols-3 lg:grid-cols-4" })) })), _jsx(GroupedListingsCarousel, { groups: groups }), _jsx(RelatedItemsSection, { relatedItems: relatedItems, relatedByBrand: relatedByBrand, relatedByTags: relatedByTags, relatedByStore: relatedByStore, categoryLabel: categoryName || category || undefined, brandLabel: brand || undefined, storeLabel: storeName || undefined })] }));
|
|
207
|
-
} }), renderAuctionStoreReviews(storeId), _jsx(AuctionBottomActions, { currentBid: currentBid,
|
|
207
|
+
} }), renderAuctionStoreReviews(storeId), onPlaceBid && (_jsx(AuctionBottomActions, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow }))] })] }));
|
|
208
208
|
}
|
|
@@ -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",
|