@mohasinac/appkit 4.10.0 → 4.11.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/layout/DashboardLayoutClient.d.ts +6 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +1 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +2 -2
- package/dist/_internal/server/features/admin/daily-digest-actions.d.ts +18 -0
- package/dist/_internal/server/features/admin/daily-digest-actions.js +37 -0
- package/dist/_internal/server/features/audit-log/actions.d.ts +25 -0
- package/dist/_internal/server/features/audit-log/actions.js +38 -0
- package/dist/_internal/server/features/brands/actions.js +0 -2
- package/dist/_internal/server/features/brands/og.d.ts +4 -1
- package/dist/_internal/server/features/brands/og.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +4 -0
- package/dist/_internal/server/features/grouped/data.js +24 -0
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +9 -0
- package/dist/_internal/server/features/orders/adapters.js +25 -0
- package/dist/_internal/server/functions/scheduled.d.ts +3 -1
- package/dist/_internal/server/functions/scheduled.js +17 -1
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +40 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +164 -0
- package/dist/_internal/server/jobs/core/hardBanCascade.js +11 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +12 -0
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +85 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +16 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.js +4 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/index.js +3 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +27 -0
- package/dist/_internal/server/notifications/channel-health.js +101 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +0 -6
- package/dist/_internal/shared/features/brands/schema.js +0 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.js +3 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/client.d.ts +21 -6
- package/dist/client.js +13 -3
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +2 -3
- package/dist/contracts/client-auth.d.ts +3 -7
- package/dist/contracts/email.d.ts +1 -0
- package/dist/features/account/components/AddressForm.js +24 -2
- package/dist/features/account/components/BecomeSellerView.d.ts +1 -2
- package/dist/features/account/components/BecomeSellerView.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserBidsView.d.ts +1 -0
- package/dist/features/account/components/UserBidsView.js +47 -0
- package/dist/features/account/components/UserOrdersView.d.ts +4 -11
- package/dist/features/account/components/UserOrdersView.js +54 -4
- package/dist/features/account/components/UserReturnsView.d.ts +4 -9
- package/dist/features/account/components/UserReturnsView.js +37 -3
- package/dist/features/account/components/index.d.ts +3 -2
- package/dist/features/account/components/index.js +1 -0
- package/dist/features/admin/actions/admin-actions.js +22 -0
- package/dist/features/admin/actions/admin-coupon-actions.js +10 -0
- package/dist/features/admin/actions/notification-actions.js +4 -3
- package/dist/features/admin/components/AdminAdsView.d.ts +2 -3
- package/dist/features/admin/components/AdminAdsView.js +86 -89
- package/dist/features/admin/components/AdminAllEventEntriesView.js +18 -3
- package/dist/features/admin/components/AdminAuditLogView.d.ts +11 -0
- package/dist/features/admin/components/AdminAuditLogView.js +95 -0
- package/dist/features/admin/components/AdminBlogEditorView.js +37 -3
- package/dist/features/admin/components/AdminBrandEditorView.js +10 -6
- package/dist/features/admin/components/AdminBundleEditorView.js +18 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +2 -2
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +2 -2
- package/dist/features/admin/components/AdminCartsView.js +34 -4
- package/dist/features/admin/components/AdminCategoryEditorView.js +5 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
- package/dist/features/admin/components/AdminHistoryView.js +11 -1
- package/dist/features/admin/components/AdminNotificationsView.js +27 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +10 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +92 -24
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.d.ts +7 -0
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +47 -0
- package/dist/features/admin/components/AdminPayoutsView.js +15 -31
- package/dist/features/admin/components/AdminProductEditorView.js +26 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +22 -2
- package/dist/features/admin/components/AdminStoresView.js +7 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -2
- package/dist/features/admin/components/AdminUserEditorView.d.ts +2 -2
- package/dist/features/admin/components/AdminUserEditorView.js +4 -2
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/AdminWishlistsView.js +14 -1
- package/dist/features/admin/components/DataListingView.d.ts +2 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.d.ts +19 -0
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +22 -0
- package/dist/features/admin/components/ViewNotificationModal.d.ts +21 -0
- package/dist/features/admin/components/ViewNotificationModal.js +15 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +4 -7
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +56 -36
- package/dist/features/admin/components/index.d.ts +8 -1
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +49 -0
- package/dist/features/admin/constants/filter-tabs.js +27 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +6 -0
- package/dist/features/admin/hooks/useAdminListing.js +3 -1
- package/dist/features/admin/hooks/useAdminListingData.d.ts +9 -1
- package/dist/features/admin/hooks/useAdminListingData.js +9 -2
- package/dist/features/admin/repository/site-settings.repository.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.d.ts +12 -0
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +8 -0
- package/dist/features/auctions/schemas/index.d.ts +5 -0
- package/dist/features/audit-log/repository/audit-log.repository.d.ts +45 -0
- package/dist/features/audit-log/repository/audit-log.repository.js +30 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +59 -0
- package/dist/features/audit-log/schemas/firestore.js +35 -0
- package/dist/features/auth/actions/index.d.ts +0 -1
- package/dist/features/auth/actions/index.js +0 -1
- package/dist/features/auth/hooks/useAuth.d.ts +8 -35
- package/dist/features/auth/hooks/useAuth.js +21 -37
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +2 -0
- package/dist/features/blog/schemas/index.d.ts +32 -0
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +31 -3
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +2 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +7 -3
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.d.ts +14 -0
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.js +22 -0
- package/dist/features/categories/components/CategoryProductsView.d.ts +1 -5
- package/dist/features/categories/components/CategoryProductsView.js +1 -1
- package/dist/features/categories/components/CategorySelectorCreate.js +16 -2
- package/dist/features/categories/schemas/firestore.d.ts +19 -2
- package/dist/features/categories/schemas/index.d.ts +24 -3
- package/dist/features/categories/schemas/index.js +3 -1
- package/dist/features/categories/types/index.d.ts +7 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +17 -3
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +101 -22
- package/dist/features/contact/components/ContactForm.js +17 -4
- package/dist/features/contact/email.d.ts +1 -0
- package/dist/features/contact/email.js +3 -0
- package/dist/features/events/components/AdminEventEditorView.js +54 -2
- package/dist/features/events/components/AdminEventEntriesView.d.ts +2 -16
- package/dist/features/events/components/AdminEventEntriesView.js +56 -89
- package/dist/features/events/components/EventFormDrawer.js +5 -1
- package/dist/features/events/components/EventRaffleEntryForm.d.ts +1 -3
- package/dist/features/events/components/EventRaffleEntryForm.js +1 -1
- package/dist/features/events/schemas/index.d.ts +22 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +4 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +18 -0
- package/dist/features/homepage/components/CharacterHotspotForm.js +202 -187
- package/dist/features/homepage/components/SectionCarousel.d.ts +1 -7
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/media/MediaImage.js +39 -3
- package/dist/features/media/types/index.d.ts +10 -0
- package/dist/features/media/types/index.js +1 -0
- package/dist/features/media/upload/MediaUploadField.d.ts +7 -1
- package/dist/features/media/upload/MediaUploadField.js +11 -7
- package/dist/features/media/upload/video-poster.d.ts +2 -0
- package/dist/features/media/upload/video-poster.js +16 -1
- package/dist/features/media/upload/video-poster.test.js +1 -1
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +12 -0
- package/dist/features/orders/components/OrderStatusTimeline.js +55 -0
- package/dist/features/orders/components/index.d.ts +2 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/constants/payment-window.d.ts +25 -0
- package/dist/features/orders/constants/payment-window.js +25 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +56 -0
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +33 -0
- package/dist/features/pre-orders/schemas/index.d.ts +5 -0
- package/dist/features/products/components/BidHistory.js +9 -1
- package/dist/features/products/components/GroupSettingsPanel.js +25 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -4
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailActions.d.ts +1 -2
- package/dist/features/products/components/ProductDetailActions.js +1 -1
- package/dist/features/products/components/ProductGrid.js +15 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/schemas/index.d.ts +5 -0
- package/dist/features/promotions/components/CouponsIndexListing.d.ts +1 -3
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/search/components/SearchView.d.ts +1 -3
- package/dist/features/search/components/SearchView.js +1 -1
- package/dist/features/seller/components/QuickProductForm.js +2 -1
- package/dist/features/seller/components/SellerBidsView.js +66 -107
- package/dist/features/seller/components/SellerGoogleReviewsView.js +17 -1
- package/dist/features/seller/components/SellerOrdersView.d.ts +15 -3
- package/dist/features/seller/components/SellerOrdersView.js +134 -163
- package/dist/features/seller/components/SellerPayoutMethodsView.js +60 -48
- package/dist/features/seller/components/SellerPayoutSettingsView.js +69 -4
- package/dist/features/seller/components/SellerPayoutsView.d.ts +12 -0
- package/dist/features/seller/components/SellerPayoutsView.js +25 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +10 -3
- package/dist/features/seller/components/SellerProductShell.js +89 -36
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +2 -7
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +4 -4
- package/dist/features/seller/components/SellerProductsView.d.ts +2 -4
- package/dist/features/seller/components/SellerProductsView.js +131 -189
- package/dist/features/seller/components/SellerReviewsView.js +95 -133
- package/dist/features/seller/components/SellerShippingConfigsView.js +47 -67
- package/dist/features/seller/components/SellerShippingView.js +41 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +51 -59
- package/dist/features/seller/components/SellerStorefrontView.d.ts +6 -1
- package/dist/features/seller/components/SellerStorefrontView.js +42 -5
- package/dist/features/seller/components/index.d.ts +2 -2
- package/dist/features/seller/components/index.js +2 -2
- package/dist/features/shell/FormShell.d.ts +24 -4
- package/dist/features/shell/FormShell.js +12 -6
- package/dist/features/shell/QuickFormDrawer.js +67 -12
- package/dist/features/shell/StepForm.d.ts +30 -1
- package/dist/features/shell/StepForm.js +47 -4
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +1 -1
- package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -2
- package/dist/features/shipments/schemas/validation.d.ts +14 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +23 -2
- package/dist/features/tester/components/TesterChecklistStepRow.js +1 -1
- package/dist/features/tester/components/TesterHubView.js +3 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +9 -1
- package/dist/features/tester/seed-data/products-tester-seed-data.js +43 -32
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +404 -15
- package/dist/index.d.ts +15 -3
- package/dist/index.js +22 -5
- package/dist/next/routing/route-map.d.ts +11 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/email-resend/provider.js +1 -0
- package/dist/providers/firebase-client/auth.d.ts +1 -0
- package/dist/providers/firebase-client/auth.js +7 -1
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +61 -25
- package/dist/security/pii-mask.js +1 -1
- package/dist/seed/categories-seed-data.js +69 -0
- package/dist/seed/products-auctions-seed-data.js +11 -0
- package/dist/seed/products-standard-seed-data.js +93 -4
- package/dist/seed/site-settings-seed-data.js +13 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +20 -2
- package/dist/server.d.ts +7 -2
- package/dist/server.js +8 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Form.d.ts +11 -2
- package/dist/ui/components/Form.js +4 -3
- package/dist/ui/components/FormField.d.ts +3 -1
- package/dist/ui/components/FormField.js +22 -4
- package/dist/ui/components/ListingLayout.d.ts +1 -2
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +68 -0
- package/dist/ui/components/RecordDetailModal.js +8 -0
- package/dist/ui/components/SlottedListingView.js +9 -1
- package/dist/ui/forms/FormErrorSummary.d.ts +25 -0
- package/dist/ui/forms/FormErrorSummary.js +37 -0
- package/dist/ui/forms/FormShell.d.ts +37 -2
- package/dist/ui/forms/FormShell.js +37 -7
- package/dist/ui/forms/FormShell.style.css +17 -0
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +5 -0
- package/dist/utils/media-url.d.ts +3 -1
- package/dist/utils/media-url.js +20 -1
- package/dist/validation/schemas.d.ts +4 -4
- package/package.json +1 -1
- package/dist/_internal/client/features/classified/ClassifiedDetailView.d.ts +0 -9
- package/dist/_internal/client/features/classified/ClassifiedDetailView.js +0 -55
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.d.ts +0 -18
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.js +0 -21
- package/dist/_internal/client/features/live/LiveItemDetailView.d.ts +0 -9
- package/dist/_internal/client/features/live/LiveItemDetailView.js +0 -25
- package/dist/_internal/server/jobs/core/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +0 -28
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +0 -61
- package/dist/_internal/server/jobs/core/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +0 -68
- package/dist/_internal/server/jobs/core/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +0 -49
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +0 -2
- package/dist/features/auth/consent-otp.d.ts +0 -57
- package/dist/features/auth/consent-otp.js +0 -127
- package/dist/features/cart/components/CheckoutOtpModal.d.ts +0 -15
- package/dist/features/cart/components/CheckoutOtpModal.js +0 -12
- package/dist/features/checkout/actions/checkout-actions.d.ts +0 -31
- package/dist/features/checkout/actions/checkout-actions.js +0 -124
- package/dist/features/classified/components/ClassifiedIndexListing.d.ts +0 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +0 -123
- package/dist/features/classified/components/ClassifiedListView.d.ts +0 -6
- package/dist/features/classified/components/ClassifiedListView.js +0 -44
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +0 -5
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +0 -114
- package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +0 -6
- package/dist/features/digital-codes/components/DigitalCodesListView.js +0 -38
- package/dist/features/live/components/LiveItemsIndexListing.d.ts +0 -5
- package/dist/features/live/components/LiveItemsIndexListing.js +0 -115
- package/dist/features/live/components/LiveItemsListView.d.ts +0 -6
- package/dist/features/live/components/LiveItemsListView.js +0 -44
- package/dist/features/seller/components/SellerTemplatesView.d.ts +0 -12
- package/dist/features/seller/components/SellerTemplatesView.js +0 -276
- package/dist/react/hooks/useVisibleItems.d.ts +0 -37
- package/dist/react/hooks/useVisibleItems.js +0 -59
- package/dist/ui/components/TabStrip.d.ts +0 -34
- package/dist/ui/components/TabStrip.js +0 -56
- package/dist/ui/components/TabStrip.style.css +0 -97
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import Image from "next/image";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { Div, Row, Span } from "../../ui";
|
|
6
6
|
import { ROUNDED_MAP, SHADOW_MAP } from "../../ui/components/surface-tokens";
|
|
7
7
|
import { resolveMediaUrl } from "../../utils/media-url";
|
|
@@ -25,6 +25,13 @@ const FALLBACK_ICONS = {
|
|
|
25
25
|
gallery: "📦",
|
|
26
26
|
avatar: "👤",
|
|
27
27
|
};
|
|
28
|
+
// A freshly-uploaded media slug can legitimately 404 for a brief window
|
|
29
|
+
// right after upload — the Firestore doc write and the Storage object write
|
|
30
|
+
// don't land atomically (see src/app/api/media/[...slug]/route.ts's own
|
|
31
|
+
// comment on this race). Retry with backoff before latching the fallback
|
|
32
|
+
// icon, instead of failing permanently on the very first request.
|
|
33
|
+
const MAX_LOAD_RETRIES = 3;
|
|
34
|
+
const RETRY_BACKOFF_MS = [600, 1200, 2400];
|
|
28
35
|
/** Resolve every URL in a comma-separated srcset string through the media proxy. */
|
|
29
36
|
function resolveSrcSet(input) {
|
|
30
37
|
return input
|
|
@@ -44,15 +51,39 @@ function resolveSrcSet(input) {
|
|
|
44
51
|
export function MediaImage({ src, alt, size = "card", priority = false, loading, objectFit = "cover", fallback, className, rounded, shadow, style, sources, onError, }) {
|
|
45
52
|
const [hasError, setHasError] = useState(false);
|
|
46
53
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
54
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
55
|
+
const retryTimeoutRef = useRef(null);
|
|
47
56
|
const icon = fallback ?? FALLBACK_ICONS[size];
|
|
48
57
|
const fitClass = objectFit === "contain" ? "object-contain" : "object-cover";
|
|
49
58
|
const resolvedSrc = resolveMediaUrl(src);
|
|
59
|
+
// Reset error/load/retry state whenever the resolved src actually changes.
|
|
60
|
+
// Without this, a component instance that hit a load failure once (e.g.
|
|
61
|
+
// lost the post-upload propagation race above) latches hasError forever,
|
|
62
|
+
// even across a later prop change to a src that would now succeed.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
setHasError(false);
|
|
65
|
+
setIsLoaded(false);
|
|
66
|
+
setRetryCount(0);
|
|
67
|
+
return () => {
|
|
68
|
+
if (retryTimeoutRef.current) {
|
|
69
|
+
clearTimeout(retryTimeoutRef.current);
|
|
70
|
+
retryTimeoutRef.current = null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
}, [resolvedSrc]);
|
|
50
75
|
const wrapperExtra = [
|
|
51
76
|
rounded ? ROUNDED_MAP[rounded] : "",
|
|
52
77
|
shadow ? SHADOW_MAP[shadow] : "",
|
|
53
78
|
className ?? "",
|
|
54
79
|
].filter(Boolean).join(" ");
|
|
55
80
|
const handleError = () => {
|
|
81
|
+
if (retryCount < MAX_LOAD_RETRIES) {
|
|
82
|
+
retryTimeoutRef.current = setTimeout(() => {
|
|
83
|
+
setRetryCount((count) => count + 1);
|
|
84
|
+
}, RETRY_BACKOFF_MS[retryCount]);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
56
87
|
setHasError(true);
|
|
57
88
|
onError?.();
|
|
58
89
|
};
|
|
@@ -62,15 +93,20 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
|
|
|
62
93
|
const isSvg = resolvedSrc.toLowerCase().endsWith(".svg") ||
|
|
63
94
|
resolvedSrc.includes("image/svg") ||
|
|
64
95
|
/[./]svg(\?|$)/i.test(resolvedSrc);
|
|
96
|
+
// Cache-bust the retried request so a browser-cached 404 response isn't
|
|
97
|
+
// silently re-served — the underlying URL is otherwise byte-identical.
|
|
98
|
+
const requestSrc = retryCount > 0
|
|
99
|
+
? `${resolvedSrc}${resolvedSrc.includes("?") ? "&" : "?"}retry=${retryCount}`
|
|
100
|
+
: resolvedSrc;
|
|
65
101
|
// Art-directed `<picture>` path — bypasses Next.js `<Image>` because the
|
|
66
102
|
// browser must pick the matching `<source>` itself. Loading + decoding hints
|
|
67
103
|
// come from the consumer's preset.
|
|
68
104
|
if (sources && sources.length > 0) {
|
|
69
|
-
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src:
|
|
105
|
+
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsxs("picture", { children: [sources.map((source, index) => (_jsx("source", { srcSet: resolveSrcSet(source.srcSet), type: source.type, media: source.media }, `${source.media ?? ""}-${source.type ?? ""}-${index}`))), _jsx("img", { src: requestSrc, alt: alt, loading: loading ?? (priority ? "eager" : "lazy"), decoding: "async",
|
|
70
106
|
// catalogued primitive for media (variant catalogue allows the inner
|
|
71
107
|
// <img> here). Object-fit + className come from typed props.
|
|
72
108
|
className: `absolute inset-0 w-full h-full ${fitClass}`, onLoad: () => setIsLoaded(true), onError: handleError })] })] }));
|
|
73
109
|
}
|
|
74
|
-
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src:
|
|
110
|
+
return (_jsxs(Div, { className: `relative w-full h-full overflow-hidden${wrapperExtra ? ` ${wrapperExtra}` : ""}`, style: style, children: [!isLoaded && (_jsx(Div, { className: "absolute inset-0 animate-pulse", surface: "subtle", "aria-hidden": "true" })), _jsx(Image, { src: requestSrc, alt: alt, fill: true, priority: priority, loading: loading ?? (priority ? "eager" : "lazy"), className: fitClass, sizes: SIZE_HINTS[size], onLoad: () => setIsLoaded(true), onError: handleError, unoptimized: isSvg })] }));
|
|
75
111
|
}
|
|
76
112
|
export default MediaImage;
|
|
@@ -12,6 +12,13 @@ export interface MediaField {
|
|
|
12
12
|
source?: MediaSource;
|
|
13
13
|
/** YouTube video ID. Only set when source === "youtube". */
|
|
14
14
|
youtubeId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Duration in seconds, for `type: "video"`. Captured automatically for
|
|
17
|
+
* direct uploads (off-DOM `<video>` element, see `video-poster.ts`) — not
|
|
18
|
+
* obtainable client-side for `source: "youtube" | "external"` without a
|
|
19
|
+
* server round-trip, so it stays undefined for those sources.
|
|
20
|
+
*/
|
|
21
|
+
duration?: number;
|
|
15
22
|
}
|
|
16
23
|
export declare const mediaFieldSchema: z.ZodObject<{
|
|
17
24
|
url: z.ZodString;
|
|
@@ -20,6 +27,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
|
|
|
20
27
|
thumbnailUrl: z.ZodOptional<z.ZodString>;
|
|
21
28
|
source: z.ZodOptional<z.ZodEnum<["upload", "youtube", "external"]>>;
|
|
22
29
|
youtubeId: z.ZodOptional<z.ZodString>;
|
|
30
|
+
duration: z.ZodOptional<z.ZodNumber>;
|
|
23
31
|
}, "strip", z.ZodTypeAny, {
|
|
24
32
|
type: "video" | "image" | "file";
|
|
25
33
|
url: string;
|
|
@@ -27,6 +35,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
|
|
|
27
35
|
alt?: string | undefined;
|
|
28
36
|
thumbnailUrl?: string | undefined;
|
|
29
37
|
youtubeId?: string | undefined;
|
|
38
|
+
duration?: number | undefined;
|
|
30
39
|
}, {
|
|
31
40
|
type: "video" | "image" | "file";
|
|
32
41
|
url: string;
|
|
@@ -34,6 +43,7 @@ export declare const mediaFieldSchema: z.ZodObject<{
|
|
|
34
43
|
alt?: string | undefined;
|
|
35
44
|
thumbnailUrl?: string | undefined;
|
|
36
45
|
youtubeId?: string | undefined;
|
|
46
|
+
duration?: number | undefined;
|
|
37
47
|
}>;
|
|
38
48
|
export declare function coerceMediaField(value: MediaFieldInput, fallbackType?: MediaFieldType): MediaField | null;
|
|
39
49
|
export declare function coerceMediaFieldArray(values: Array<MediaField | string> | null | undefined, fallbackType?: MediaFieldType): MediaField[];
|
|
@@ -7,6 +7,7 @@ export const mediaFieldSchema = z.object({
|
|
|
7
7
|
thumbnailUrl: z.string().optional(),
|
|
8
8
|
source: z.enum(["upload", "youtube", "external"]).optional(),
|
|
9
9
|
youtubeId: z.string().optional(),
|
|
10
|
+
duration: z.number().optional(),
|
|
10
11
|
});
|
|
11
12
|
export function coerceMediaField(value, fallbackType = "image") {
|
|
12
13
|
if (!value) {
|
|
@@ -28,6 +28,12 @@ export interface MediaUploadFieldProps {
|
|
|
28
28
|
enableTrim?: boolean;
|
|
29
29
|
enableThumbnail?: boolean;
|
|
30
30
|
onThumbnailChange?: (url: string) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Fired with the captured duration (seconds) for a direct video upload —
|
|
33
|
+
* mirrors `onThumbnailChange`. Never fires for the YouTube/External-URL
|
|
34
|
+
* tabs (no client-side way to read those without a server round-trip).
|
|
35
|
+
*/
|
|
36
|
+
onDurationChange?: (duration: number) => void;
|
|
31
37
|
/** Show a "YouTube URL" tab to embed a YouTube video by ID or URL. */
|
|
32
38
|
showYoutube?: boolean;
|
|
33
39
|
/** Show an "External URL" tab to link a third-party image/video URL. */
|
|
@@ -63,4 +69,4 @@ export interface MediaUploadFieldProps {
|
|
|
63
69
|
*/
|
|
64
70
|
autoCapturePoster?: boolean;
|
|
65
71
|
}
|
|
66
|
-
export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, autoCapturePoster, }: MediaUploadFieldProps): import("react").JSX.Element;
|
|
72
|
+
export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, onDurationChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, autoCapturePoster, }: MediaUploadFieldProps): import("react").JSX.Element;
|
|
@@ -88,7 +88,7 @@ export function MediaUploadField({ label, value, onChange, onChangeField, onUplo
|
|
|
88
88
|
// SB-UNI-Z5 2026-05-13 — `kind` derives accept/maxSizeMB when not set
|
|
89
89
|
// explicitly; explicit props still win. "*" + 50 stay as the floor for
|
|
90
90
|
// back-compat with callers that pass neither.
|
|
91
|
-
kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, autoCapturePoster = true, }) {
|
|
91
|
+
kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, onDurationChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, autoCapturePoster = true, }) {
|
|
92
92
|
const [isLoading, setIsLoading] = useState(false);
|
|
93
93
|
const [error, setError] = useState(null);
|
|
94
94
|
const [pendingVideoUrl, setPendingVideoUrl] = useState(null);
|
|
@@ -198,14 +198,18 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
198
198
|
: captureMode === "both"
|
|
199
199
|
? "image/*,video/*"
|
|
200
200
|
: "image/*";
|
|
201
|
-
const afterUpload = (url, fileType, autoPosterUrl) => {
|
|
201
|
+
const afterUpload = (url, fileType, autoPosterUrl, duration) => {
|
|
202
202
|
onChangeField?.({
|
|
203
203
|
url,
|
|
204
204
|
type: inferMediaTypeFromMime(fileType, url),
|
|
205
|
+
source: "upload",
|
|
205
206
|
...(autoPosterUrl ? { thumbnailUrl: autoPosterUrl } : {}),
|
|
207
|
+
...(duration ? { duration } : {}),
|
|
206
208
|
});
|
|
207
209
|
if (autoPosterUrl)
|
|
208
210
|
onThumbnailChange?.(autoPosterUrl);
|
|
211
|
+
if (duration)
|
|
212
|
+
onDurationChange?.(duration);
|
|
209
213
|
const isVideoFile = fileType.startsWith("video/");
|
|
210
214
|
if (isVideoFile && enableTrim) {
|
|
211
215
|
setPendingVideoUrl(url);
|
|
@@ -283,7 +287,7 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
283
287
|
const posterFile = posterBlobAsFile(poster, file.name);
|
|
284
288
|
const posterUrl = await onUpload(posterFile);
|
|
285
289
|
stageUrl(posterUrl);
|
|
286
|
-
return posterUrl;
|
|
290
|
+
return { posterUrl, duration: poster.duration };
|
|
287
291
|
}
|
|
288
292
|
catch (err) {
|
|
289
293
|
// The upload-poster step is best-effort. Log via normalizeError so any
|
|
@@ -309,8 +313,8 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
309
313
|
else {
|
|
310
314
|
const result = await uploadSingleFile(files[0]);
|
|
311
315
|
if (result) {
|
|
312
|
-
const
|
|
313
|
-
afterUpload(result.url, result.type, posterUrl);
|
|
316
|
+
const poster = await maybeCaptureVideoPoster(files[0]);
|
|
317
|
+
afterUpload(result.url, result.type, poster?.posterUrl, poster?.duration);
|
|
314
318
|
}
|
|
315
319
|
}
|
|
316
320
|
}
|
|
@@ -353,8 +357,8 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
353
357
|
try {
|
|
354
358
|
const url = await onUpload(file);
|
|
355
359
|
stageUrl(url);
|
|
356
|
-
const
|
|
357
|
-
afterUpload(url, blob.type || mimeType, posterUrl);
|
|
360
|
+
const poster = await maybeCaptureVideoPoster(file);
|
|
361
|
+
afterUpload(url, blob.type || mimeType, poster?.posterUrl, poster?.duration);
|
|
358
362
|
}
|
|
359
363
|
catch (err) {
|
|
360
364
|
void normalizeError(err);
|
|
@@ -26,6 +26,8 @@ export interface ExtractedPoster {
|
|
|
26
26
|
/** Frame dimensions in pixels (also the JPEG's pixel dimensions). */
|
|
27
27
|
width: number;
|
|
28
28
|
height: number;
|
|
29
|
+
/** Video duration in seconds, read from the same off-DOM element. */
|
|
30
|
+
duration: number;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Build a deterministic filename for the captured poster. We mirror the
|
|
@@ -25,6 +25,20 @@ const TARGET_FRAME_TIME_SECONDS = 0.5;
|
|
|
25
25
|
const JPEG_QUALITY = 0.85;
|
|
26
26
|
const LOAD_TIMEOUT_MS = 10000;
|
|
27
27
|
const POSTER_MIME = "image/jpeg";
|
|
28
|
+
/** Bounded wait for `video.duration` to become a finite, positive number. */
|
|
29
|
+
async function readDuration(video) {
|
|
30
|
+
if (Number.isFinite(video.duration) && video.duration > 0)
|
|
31
|
+
return video.duration;
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const timer = window.setTimeout(() => resolve(0), 2000);
|
|
34
|
+
video.addEventListener("durationchange", () => {
|
|
35
|
+
if (Number.isFinite(video.duration) && video.duration > 0) {
|
|
36
|
+
window.clearTimeout(timer);
|
|
37
|
+
resolve(video.duration);
|
|
38
|
+
}
|
|
39
|
+
}, { once: true });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
28
42
|
/**
|
|
29
43
|
* Build a deterministic filename for the captured poster. We mirror the
|
|
30
44
|
* source video's basename so downstream code can correlate the two assets
|
|
@@ -86,6 +100,7 @@ export async function extractVideoPosterFrame(file) {
|
|
|
86
100
|
const height = video.videoHeight;
|
|
87
101
|
if (!width || !height)
|
|
88
102
|
return null;
|
|
103
|
+
const duration = await readDuration(video);
|
|
89
104
|
const canvas = document.createElement("canvas");
|
|
90
105
|
canvas.width = width;
|
|
91
106
|
canvas.height = height;
|
|
@@ -96,7 +111,7 @@ export async function extractVideoPosterFrame(file) {
|
|
|
96
111
|
const blob = await new Promise((resolve) => canvas.toBlob(resolve, POSTER_MIME, JPEG_QUALITY));
|
|
97
112
|
if (!blob)
|
|
98
113
|
return null;
|
|
99
|
-
return { blob, width, height };
|
|
114
|
+
return { blob, width, height, duration };
|
|
100
115
|
}
|
|
101
116
|
catch (_err) {
|
|
102
117
|
void normalizeError(_err);
|
|
@@ -19,7 +19,7 @@ describe("posterBlobAsFile", () => {
|
|
|
19
19
|
const blob = new Blob([new Uint8Array([0xff, 0xd8, 0xff])], {
|
|
20
20
|
type: "image/jpeg",
|
|
21
21
|
});
|
|
22
|
-
const file = posterBlobAsFile({ blob, width: 320, height: 180 }, "promo-reel.mp4");
|
|
22
|
+
const file = posterBlobAsFile({ blob, width: 320, height: 180, duration: 12.5 }, "promo-reel.mp4");
|
|
23
23
|
expect(file).toBeInstanceOf(File);
|
|
24
24
|
expect(file.name).toBe("promo-reel.poster.jpg");
|
|
25
25
|
expect(file.type).toBe("image/jpeg");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OrderStatusTimelineProps {
|
|
2
|
+
orderStatus: string;
|
|
3
|
+
orderDate?: string;
|
|
4
|
+
shippingDate?: string;
|
|
5
|
+
deliveryDate?: string;
|
|
6
|
+
cancellationDate?: string;
|
|
7
|
+
trackingNumber?: string;
|
|
8
|
+
shippingCarrier?: string;
|
|
9
|
+
trackingUrl?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function OrderStatusTimeline({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate, trackingNumber, shippingCarrier, trackingUrl, className, }: OrderStatusTimelineProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* OrderStatusTimeline — a genuine (not fabricated/estimated) status timeline
|
|
5
|
+
* built from the real per-status timestamps already written by the
|
|
6
|
+
* order-status-update path (`orderDate`/`shippingDate`/`deliveryDate`/
|
|
7
|
+
* `cancellationDate`). No live courier-API integration — `trackingUrl` is
|
|
8
|
+
* just the passthrough link a seller/admin entered, rendered as an external
|
|
9
|
+
* link that opens in a new tab. Statuses with no dedicated date field
|
|
10
|
+
* (confirmed/processing/return_requested/returned/refunded) render as a step
|
|
11
|
+
* without a timestamp rather than showing a made-up date.
|
|
12
|
+
*/
|
|
13
|
+
import { Anchor, Badge, Div, Heading, Row, Stack, Text } from "../../../ui";
|
|
14
|
+
const STAGE_SEQUENCE = ["pending", "confirmed", "processing", "shipped", "delivered"];
|
|
15
|
+
const STAGE_LABELS = {
|
|
16
|
+
pending: "Order placed",
|
|
17
|
+
confirmed: "Confirmed",
|
|
18
|
+
processing: "Processing",
|
|
19
|
+
shipped: "Shipped",
|
|
20
|
+
delivered: "Delivered",
|
|
21
|
+
cancelled: "Cancelled",
|
|
22
|
+
return_requested: "Return requested",
|
|
23
|
+
returned: "Returned",
|
|
24
|
+
refunded: "Refunded",
|
|
25
|
+
};
|
|
26
|
+
function buildSteps(props) {
|
|
27
|
+
const status = (props.orderStatus ?? "").toLowerCase();
|
|
28
|
+
const currentIndex = STAGE_SEQUENCE.indexOf(status);
|
|
29
|
+
const steps = [{ key: "pending", label: STAGE_LABELS.pending, date: props.orderDate }];
|
|
30
|
+
if ((status === "confirmed" || status === "processing") && !props.shippingDate) {
|
|
31
|
+
steps.push({ key: status, label: STAGE_LABELS[status] });
|
|
32
|
+
}
|
|
33
|
+
if (props.shippingDate || currentIndex >= 3) {
|
|
34
|
+
steps.push({ key: "shipped", label: STAGE_LABELS.shipped, date: props.shippingDate });
|
|
35
|
+
}
|
|
36
|
+
if (props.deliveryDate || status === "delivered") {
|
|
37
|
+
steps.push({ key: "delivered", label: STAGE_LABELS.delivered, date: props.deliveryDate });
|
|
38
|
+
}
|
|
39
|
+
if (status === "cancelled" || props.cancellationDate) {
|
|
40
|
+
steps.push({ key: "cancelled", label: STAGE_LABELS.cancelled, date: props.cancellationDate });
|
|
41
|
+
}
|
|
42
|
+
else if (status === "return_requested" || status === "returned" || status === "refunded") {
|
|
43
|
+
steps.push({ key: status, label: STAGE_LABELS[status] ?? status });
|
|
44
|
+
}
|
|
45
|
+
return steps;
|
|
46
|
+
}
|
|
47
|
+
export function OrderStatusTimeline({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate, trackingNumber, shippingCarrier, trackingUrl, className = "", }) {
|
|
48
|
+
const steps = buildSteps({ orderStatus, orderDate, shippingDate, deliveryDate, cancellationDate });
|
|
49
|
+
const isTerminalNegative = steps[steps.length - 1]?.key === "cancelled";
|
|
50
|
+
return (_jsxs(Div, { className: className, rounded: "xl", padding: "md", surface: "muted", border: "default", children: [_jsx(Heading, { level: 3, size: "sm", weight: "semibold", color: "muted", className: "mb-3", children: "Tracking" }), _jsx(Stack, { gap: "none", children: steps.map((step, i) => {
|
|
51
|
+
const isLast = i === steps.length - 1;
|
|
52
|
+
const isNegative = step.key === "cancelled";
|
|
53
|
+
return (_jsxs(Row, { gap: "sm", padding: isLast ? "none" : "b-md", children: [_jsxs(Stack, { gap: "none", align: "center", className: "shrink-0", children: [_jsx(Div, { className: `h-2.5 w-2.5 ${isNegative ? "" : "bg-[var(--appkit-color-primary)]"}`, rounded: "full", surface: isNegative ? "danger-surface" : "none" }), !isLast && _jsx(Div, { className: "w-px flex-1 min-h-[24px]", surface: "subtle" })] }), _jsxs(Stack, { gap: "none", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "medium", className: isNegative ? "text-error" : undefined, children: step.label }), _jsx(Text, { size: "xs", color: "muted", children: step.date ? new Date(step.date).toLocaleString() : "—" })] })] }, step.key));
|
|
54
|
+
}) }), (trackingNumber || shippingCarrier || trackingUrl) && !isTerminalNegative && (_jsx(Div, { className: "mt-3 border-t border-[var(--appkit-color-border)]", padding: "t-sm", children: _jsxs(Stack, { gap: "xs", children: [shippingCarrier && (_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "sm", color: "muted", children: "Carrier" }), _jsx(Text, { size: "sm", weight: "medium", children: shippingCarrier })] })), trackingNumber && (_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "sm", color: "muted", children: "Tracking number" }), _jsx(Text, { size: "sm", weight: "medium", children: trackingNumber })] })), trackingUrl && (_jsx(Row, { justify: "end", children: _jsx(Anchor, { href: trackingUrl, tone: "brand", weight: "semibold", size: "sm", children: "Track with carrier \u2192" }) }))] }) })), !trackingNumber && !trackingUrl && !isTerminalNegative && (_jsx(Badge, { variant: "default", className: "mt-3", children: "No tracking details yet" }))] }));
|
|
55
|
+
}
|
|
@@ -6,6 +6,8 @@ export { OrderSiblingPayments } from "./OrderSiblingPayments";
|
|
|
6
6
|
export type { OrderSiblingPaymentsProps } from "./OrderSiblingPayments";
|
|
7
7
|
export { OrderPaymentSummary } from "./OrderPaymentSummary";
|
|
8
8
|
export type { OrderPaymentSummaryProps } from "./OrderPaymentSummary";
|
|
9
|
+
export { OrderStatusTimeline } from "./OrderStatusTimeline";
|
|
10
|
+
export type { OrderStatusTimelineProps } from "./OrderStatusTimeline";
|
|
9
11
|
export { RefundHistoryTable } from "./RefundHistoryTable";
|
|
10
12
|
export type { RefundHistoryTableProps } from "./RefundHistoryTable";
|
|
11
13
|
export { RefundRequestView } from "./RefundRequestView";
|
|
@@ -3,5 +3,6 @@ export { MarketplaceOrderCard, } from "./MarketplaceOrderCard";
|
|
|
3
3
|
export { OrderFilters, ORDER_FILTER_KEYS, ORDER_ADMIN_SORT_OPTIONS, ORDER_SELLER_SORT_OPTIONS, ORDER_USER_SORT_OPTIONS, getOrderFilterKeys, getOrderSortOptions, } from "./OrderFilters";
|
|
4
4
|
export { OrderSiblingPayments } from "./OrderSiblingPayments";
|
|
5
5
|
export { OrderPaymentSummary } from "./OrderPaymentSummary";
|
|
6
|
+
export { OrderStatusTimeline } from "./OrderStatusTimeline";
|
|
6
7
|
export { RefundHistoryTable } from "./RefundHistoryTable";
|
|
7
8
|
export { RefundRequestView } from "./RefundRequestView";
|
|
@@ -8,3 +8,28 @@ export declare const PAYMENT_WINDOW_MS: number;
|
|
|
8
8
|
/** Cancellation reasons this feature writes to `OrderDocument.cancellationReason` (a free-form string field). */
|
|
9
9
|
export declare const PAYMENT_WINDOW_EXPIRED_REASON = "payment_window_expired";
|
|
10
10
|
export declare const PAYMENT_FRAUD_REJECTED_REASON = "payment_fraud_rejected";
|
|
11
|
+
/**
|
|
12
|
+
* The `paymentMethod` values that settle out-of-band and therefore require a
|
|
13
|
+
* buyer-uploaded proof + a human review pass. Single source of truth for
|
|
14
|
+
* `attachPaymentProofAction`'s guard, the admin payment-review queue, and the
|
|
15
|
+
* buyer/seller "needs payment proof" UI — previously each site inlined its own
|
|
16
|
+
* `pm !== "cash" && pm !== "upi_manual" && ...` chain and they could drift.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MANUAL_PAYMENT_METHODS: readonly string[];
|
|
19
|
+
export declare function isManualPaymentMethod(method: string | undefined | null): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The two states an unresolved manual payment can be in. Derived at query
|
|
22
|
+
* time from `paymentProofUrl` + `paymentReviewOutcome` rather than stored as
|
|
23
|
+
* its own field — a denormalised mirror would drift the moment any write path
|
|
24
|
+
* forgot it (CLAUDE.md Root Cause Pattern #42).
|
|
25
|
+
*/
|
|
26
|
+
export type PaymentReviewQueueMode = "awaiting_proof" | "awaiting_verification";
|
|
27
|
+
export declare const PAYMENT_REVIEW_QUEUE_MODES: readonly PaymentReviewQueueMode[];
|
|
28
|
+
export declare function isPaymentReviewQueueMode(value: string): value is PaymentReviewQueueMode;
|
|
29
|
+
/**
|
|
30
|
+
* Hard bound on the payment-review queue's Firestore scan (Rule #6 — the
|
|
31
|
+
* Vercel 10s sync ceiling). Safe because an order only sits in this queue for
|
|
32
|
+
* 15 minutes (awaiting proof) or 2 hours (awaiting verification) before a
|
|
33
|
+
* scheduled sweep resolves it, so the live set is inherently small.
|
|
34
|
+
*/
|
|
35
|
+
export declare const PAYMENT_REVIEW_QUEUE_SCAN_LIMIT = 500;
|
|
@@ -8,3 +8,28 @@ export const PAYMENT_WINDOW_MS = PAYMENT_WINDOW_MINUTES * 60 * 1000;
|
|
|
8
8
|
/** Cancellation reasons this feature writes to `OrderDocument.cancellationReason` (a free-form string field). */
|
|
9
9
|
export const PAYMENT_WINDOW_EXPIRED_REASON = "payment_window_expired";
|
|
10
10
|
export const PAYMENT_FRAUD_REJECTED_REASON = "payment_fraud_rejected";
|
|
11
|
+
/**
|
|
12
|
+
* The `paymentMethod` values that settle out-of-band and therefore require a
|
|
13
|
+
* buyer-uploaded proof + a human review pass. Single source of truth for
|
|
14
|
+
* `attachPaymentProofAction`'s guard, the admin payment-review queue, and the
|
|
15
|
+
* buyer/seller "needs payment proof" UI — previously each site inlined its own
|
|
16
|
+
* `pm !== "cash" && pm !== "upi_manual" && ...` chain and they could drift.
|
|
17
|
+
*/
|
|
18
|
+
export const MANUAL_PAYMENT_METHODS = ["cash", "upi_manual", "emi"];
|
|
19
|
+
export function isManualPaymentMethod(method) {
|
|
20
|
+
return !!method && MANUAL_PAYMENT_METHODS.includes(method);
|
|
21
|
+
}
|
|
22
|
+
export const PAYMENT_REVIEW_QUEUE_MODES = [
|
|
23
|
+
"awaiting_proof",
|
|
24
|
+
"awaiting_verification",
|
|
25
|
+
];
|
|
26
|
+
export function isPaymentReviewQueueMode(value) {
|
|
27
|
+
return PAYMENT_REVIEW_QUEUE_MODES.includes(value);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hard bound on the payment-review queue's Firestore scan (Rule #6 — the
|
|
31
|
+
* Vercel 10s sync ceiling). Safe because an order only sits in this queue for
|
|
32
|
+
* 15 minutes (awaiting proof) or 2 hours (awaiting verification) before a
|
|
33
|
+
* scheduled sweep resolves it, so the live set is inherently small.
|
|
34
|
+
*/
|
|
35
|
+
export const PAYMENT_REVIEW_QUEUE_SCAN_LIMIT = 500;
|
|
@@ -3,6 +3,7 @@ import type { FirebaseSieveResult, SieveModel } from "../../../providers/db-fire
|
|
|
3
3
|
import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
|
|
4
4
|
import { type OrderCreateInput, type OrderDocument, type OrderRefundEvent } from "../schemas";
|
|
5
5
|
import type { OrderStatus, PaymentStatus } from "../types";
|
|
6
|
+
import { type PaymentReviewQueueMode } from "../constants/payment-window";
|
|
6
7
|
declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
7
8
|
constructor();
|
|
8
9
|
private decryptOrder;
|
|
@@ -217,6 +218,37 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
217
218
|
ref: DocumentReference;
|
|
218
219
|
data: OrderDocument;
|
|
219
220
|
}>>;
|
|
221
|
+
/**
|
|
222
|
+
* Admin/seller payment-review queue — the manual-payment orders that are
|
|
223
|
+
* waiting on somebody. Two modes:
|
|
224
|
+
*
|
|
225
|
+
* - `awaiting_proof` — buyer hasn't uploaded a screenshot yet
|
|
226
|
+
* (the 15-minute `paymentDeadline` window).
|
|
227
|
+
* - `awaiting_verification` — proof submitted, no admin decision recorded
|
|
228
|
+
* yet (the 2-hour auto-approve window).
|
|
229
|
+
*
|
|
230
|
+
* Both refinements — proof presence and manual `paymentMethod` — are done
|
|
231
|
+
* in-memory rather than as Firestore clauses, deliberately:
|
|
232
|
+
*
|
|
233
|
+
* - `paymentProofUrl != null` would silently exclude every doc where the
|
|
234
|
+
* field was never set at all, which is exactly the `awaiting_proof` set
|
|
235
|
+
* (same reasoning as `getExpiredPaymentDeadlines` /
|
|
236
|
+
* `getUnreviewedProofPastDeadline` above).
|
|
237
|
+
* - keeping `paymentMethod` out of the query means this reuses the
|
|
238
|
+
* existing `(status, paymentStatus, createdAt)` composite index instead
|
|
239
|
+
* of needing a new four-field one.
|
|
240
|
+
*
|
|
241
|
+
* The scan is hard-bounded at {@link PAYMENT_REVIEW_QUEUE_SCAN_LIMIT} for
|
|
242
|
+
* the Vercel 10s ceiling. That bound is generous for this queue by
|
|
243
|
+
* construction: an order only sits in it for 15 minutes (awaiting proof) or
|
|
244
|
+
* 2 hours (awaiting verification) before a scheduled sweep resolves it, so
|
|
245
|
+
* the live set is inherently small. `total` therefore counts matches within
|
|
246
|
+
* the scan window, not an unbounded collection-wide count.
|
|
247
|
+
*/
|
|
248
|
+
listPaymentReviewQueue(mode: PaymentReviewQueueMode, opts?: {
|
|
249
|
+
page?: number;
|
|
250
|
+
pageSize?: number;
|
|
251
|
+
}): Promise<FirebaseSieveResult<OrderDocument>>;
|
|
220
252
|
/**
|
|
221
253
|
* Cloud Functions: find delivered orders eligible for the weekly payout sweep.
|
|
222
254
|
*
|
|
@@ -4,6 +4,7 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
|
|
|
4
4
|
import { decryptPiiFields, encryptPiiFields, ORDER_PII_FIELDS, } from "../../../security";
|
|
5
5
|
import { serverTimestamp } from "../../../contracts/field-ops";
|
|
6
6
|
import { createOrderId, ORDER_COLLECTION, OrderStatusValues, } from "../schemas";
|
|
7
|
+
import { MANUAL_PAYMENT_METHODS, PAYMENT_REVIEW_QUEUE_SCAN_LIMIT, } from "../constants/payment-window";
|
|
7
8
|
import { ORDER_FIELDS } from "../../../constants/field-names";
|
|
8
9
|
/**
|
|
9
10
|
* Statuses that count toward a user's per-product / per-bundle purchase
|
|
@@ -340,6 +341,61 @@ class OrderRepository extends BaseRepository {
|
|
|
340
341
|
}))
|
|
341
342
|
.filter((entry) => !!entry.data.paymentProofUrl && !entry.data.paymentReviewOutcome);
|
|
342
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Admin/seller payment-review queue — the manual-payment orders that are
|
|
346
|
+
* waiting on somebody. Two modes:
|
|
347
|
+
*
|
|
348
|
+
* - `awaiting_proof` — buyer hasn't uploaded a screenshot yet
|
|
349
|
+
* (the 15-minute `paymentDeadline` window).
|
|
350
|
+
* - `awaiting_verification` — proof submitted, no admin decision recorded
|
|
351
|
+
* yet (the 2-hour auto-approve window).
|
|
352
|
+
*
|
|
353
|
+
* Both refinements — proof presence and manual `paymentMethod` — are done
|
|
354
|
+
* in-memory rather than as Firestore clauses, deliberately:
|
|
355
|
+
*
|
|
356
|
+
* - `paymentProofUrl != null` would silently exclude every doc where the
|
|
357
|
+
* field was never set at all, which is exactly the `awaiting_proof` set
|
|
358
|
+
* (same reasoning as `getExpiredPaymentDeadlines` /
|
|
359
|
+
* `getUnreviewedProofPastDeadline` above).
|
|
360
|
+
* - keeping `paymentMethod` out of the query means this reuses the
|
|
361
|
+
* existing `(status, paymentStatus, createdAt)` composite index instead
|
|
362
|
+
* of needing a new four-field one.
|
|
363
|
+
*
|
|
364
|
+
* The scan is hard-bounded at {@link PAYMENT_REVIEW_QUEUE_SCAN_LIMIT} for
|
|
365
|
+
* the Vercel 10s ceiling. That bound is generous for this queue by
|
|
366
|
+
* construction: an order only sits in it for 15 minutes (awaiting proof) or
|
|
367
|
+
* 2 hours (awaiting verification) before a scheduled sweep resolves it, so
|
|
368
|
+
* the live set is inherently small. `total` therefore counts matches within
|
|
369
|
+
* the scan window, not an unbounded collection-wide count.
|
|
370
|
+
*/
|
|
371
|
+
async listPaymentReviewQueue(mode, opts = {}) {
|
|
372
|
+
const page = Math.max(1, opts.page ?? 1);
|
|
373
|
+
const pageSize = Math.min(50, Math.max(1, opts.pageSize ?? 25));
|
|
374
|
+
const snap = await this.db
|
|
375
|
+
.collection(this.collection)
|
|
376
|
+
.where(ORDER_FIELDS.STATUS, "==", OrderStatusValues.PENDING)
|
|
377
|
+
.where(ORDER_FIELDS.PAYMENT_STATUS, "==", "pending")
|
|
378
|
+
.orderBy(ORDER_FIELDS.CREATED_AT, "desc")
|
|
379
|
+
.limit(PAYMENT_REVIEW_QUEUE_SCAN_LIMIT)
|
|
380
|
+
.get();
|
|
381
|
+
const matches = snap.docs
|
|
382
|
+
.map((d) => this.decryptOrder({ id: d.id, ...d.data() }))
|
|
383
|
+
.filter((order) => MANUAL_PAYMENT_METHODS.includes(order.paymentMethod ?? ""))
|
|
384
|
+
.filter((order) => mode === "awaiting_proof"
|
|
385
|
+
? !order.paymentProofUrl
|
|
386
|
+
: !!order.paymentProofUrl && !order.paymentReviewOutcome);
|
|
387
|
+
const total = matches.length;
|
|
388
|
+
const totalPages = total === 0 ? 0 : Math.max(1, Math.ceil(total / pageSize));
|
|
389
|
+
const start = (page - 1) * pageSize;
|
|
390
|
+
return {
|
|
391
|
+
items: matches.slice(start, start + pageSize),
|
|
392
|
+
total,
|
|
393
|
+
page,
|
|
394
|
+
pageSize,
|
|
395
|
+
totalPages,
|
|
396
|
+
hasMore: page < totalPages,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
343
399
|
/**
|
|
344
400
|
* Cloud Functions: find delivered orders eligible for the weekly payout sweep.
|
|
345
401
|
*
|