@mohasinac/appkit 4.63.4 → 4.64.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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +13 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +22 -5
- package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
- package/dist/_internal/server/features/events/like-actions.js +67 -0
- package/dist/_internal/server/features/events/likes.d.ts +65 -0
- package/dist/_internal/server/features/events/likes.js +146 -0
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/seo/index.d.ts +3 -0
- package/dist/_internal/server/features/seo/index.js +2 -0
- package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
- package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
- package/dist/_internal/server/features/seo/robots.js +20 -1
- package/dist/_internal/server/features/seo/sitemap.js +125 -27
- package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
- package/dist/_internal/server/features/seo/static-params.js +228 -0
- package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
- package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +88 -1
- package/dist/_internal/server/functions/scheduled.d.ts +16 -1
- package/dist/_internal/server/functions/scheduled.js +23 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
- package/dist/_internal/server/notifications/channel-health.js +2 -2
- package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
- package/dist/_internal/server/notifications/send-guard.js +4 -2
- package/dist/_internal/shared/actions/action-registry.js +89 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
- package/dist/_internal/shared/features/checkout/config.js +2 -0
- package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
- package/dist/_internal/shared/features/messaging/config.js +4 -1
- package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
- package/dist/_internal/shared/features/seller-standing/config.js +153 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
- package/dist/_internal/shared/features/site-settings/fees.js +2 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
- package/dist/_internal/shared/fees/calculator.d.ts +8 -0
- package/dist/_internal/shared/fees/calculator.js +7 -0
- package/dist/cli/index.js +2 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +39 -0
- package/dist/constants/api-endpoints.js +16 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/constants/notification-types.js +1 -1
- package/dist/core/integration-keys.d.ts +5 -0
- package/dist/core/integration-keys.js +10 -0
- package/dist/features/about/components/FeesView.js +6 -0
- package/dist/features/about/components/HelpPageView.js +9 -1
- package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
- package/dist/features/account/components/BecomeSellerView.js +2 -2
- package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
- package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
- package/dist/features/account/types/index.d.ts +2 -0
- package/dist/features/admin/actions/notification-actions.d.ts +17 -2
- package/dist/features/admin/actions/notification-actions.js +154 -9
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
- package/dist/features/admin/components/AdminOrdersView.js +2 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
- package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
- package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
- package/dist/features/admin/components/AdminStoresView.js +59 -2
- package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
- package/dist/features/admin/constants/site-settings-tabs.js +12 -6
- package/dist/features/admin/repository/site-settings.repository.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
- package/dist/features/admin/schemas/admin-user-form.js +44 -3
- package/dist/features/admin/schemas/firestore.d.ts +73 -0
- package/dist/features/admin/schemas/firestore.js +8 -3
- package/dist/features/auth/checkout-value-otp.d.ts +1 -1
- package/dist/features/auth/checkout-value-otp.js +1 -1
- package/dist/features/auth/permissions/constants.d.ts +2 -2
- package/dist/features/auth/permissions/constants.js +2 -1
- package/dist/features/blog/components/BlogIndexPageView.js +2 -2
- package/dist/features/blog/components/BlogListView.js +2 -1
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/blog/constants/category-badge.d.ts +33 -0
- package/dist/features/blog/constants/category-badge.js +40 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
- package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
- package/dist/features/cart/repository/cart.repository.js +2 -1
- package/dist/features/cart/schemas/firestore.d.ts +2 -1
- package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
- package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
- package/dist/features/categories/schemas/firestore.d.ts +25 -0
- package/dist/features/categories/utils/load-category-options.d.ts +33 -0
- package/dist/features/categories/utils/load-category-options.js +50 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
- package/dist/features/events/components/EventCard.js +4 -12
- package/dist/features/events/components/EventInsights.d.ts +27 -0
- package/dist/features/events/components/EventInsights.js +10 -0
- package/dist/features/events/components/EventLikeButton.d.ts +21 -0
- package/dist/features/events/components/EventLikeButton.js +59 -0
- package/dist/features/events/components/EventLikersRail.d.ts +23 -0
- package/dist/features/events/components/EventLikersRail.js +54 -0
- package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
- package/dist/features/events/components/EventTypeBadge.js +7 -0
- package/dist/features/events/components/EventsListPageView.js +2 -2
- package/dist/features/events/components/SpinWheel.d.ts +41 -0
- package/dist/features/events/components/SpinWheel.js +128 -0
- package/dist/features/events/components/SpinWheelView.js +36 -15
- package/dist/features/events/constants/event-type-meta.d.ts +54 -0
- package/dist/features/events/constants/event-type-meta.js +109 -0
- package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
- package/dist/features/events/repository/event-likes.repository.js +151 -0
- package/dist/features/events/schemas/firestore.d.ts +111 -0
- package/dist/features/events/schemas/likes.d.ts +81 -0
- package/dist/features/events/schemas/likes.js +59 -0
- package/dist/features/events/types/index.d.ts +7 -0
- package/dist/features/events/utils/carousel-slide.d.ts +42 -0
- package/dist/features/events/utils/carousel-slide.js +174 -0
- package/dist/features/events/utils/event-charts.d.ts +65 -0
- package/dist/features/events/utils/event-charts.js +172 -0
- package/dist/features/faq/constants/help-guides.js +9 -0
- package/dist/features/faq/schemas/firestore.js +1 -0
- package/dist/features/faq/types/index.d.ts +19 -0
- package/dist/features/homepage/components/BrandsSection.js +25 -1
- package/dist/features/homepage/components/HeroCarousel.js +47 -10
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/homepage/types/index.d.ts +7 -0
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
- package/dist/features/orders/components/OrderAddonBadges.js +4 -3
- package/dist/features/orders/repository/orders.repository.d.ts +8 -0
- package/dist/features/orders/repository/orders.repository.js +2 -0
- package/dist/features/orders/schemas/firestore.d.ts +4 -0
- package/dist/features/orders/types/index.d.ts +5 -2
- package/dist/features/search/action-index/settings-entries.js +9 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
- package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
- package/dist/features/store-extensions/schemas/firestore.js +10 -0
- package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
- package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
- package/dist/features/stores/components/SellerStandingPanel.js +25 -0
- package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
- package/dist/features/stores/components/SellerTierBadge.js +19 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
- package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
- package/dist/features/stores/components/StoreCreateForm.js +107 -0
- package/dist/features/stores/components/StoreGuideHubView.js +18 -5
- package/dist/features/stores/components/StoreHeader.js +2 -1
- package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
- package/dist/features/stores/repository/store.repository.d.ts +23 -1
- package/dist/features/stores/repository/store.repository.js +52 -1
- package/dist/features/stores/schemas/firestore.d.ts +76 -7
- package/dist/features/stores/schemas/firestore.js +21 -6
- package/dist/features/stores/schemas/index.d.ts +88 -2
- package/dist/features/stores/schemas/index.js +14 -0
- package/dist/features/stores/schemas/store-create.d.ts +47 -0
- package/dist/features/stores/schemas/store-create.js +86 -0
- package/dist/features/stores/types/index.d.ts +42 -0
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
- package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/components/index.js +2 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
- package/dist/features/whatsapp-bot/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/index.js +3 -0
- package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
- package/dist/features/whatsapp-bot/manifest.js +13 -0
- package/dist/features/whatsapp-bot/messages/en.json +7 -0
- package/dist/features/whatsapp-bot/server.d.ts +7 -0
- package/dist/features/whatsapp-bot/server.js +7 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
- package/dist/features/whatsapp-bot/types/index.js +1 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +38 -12
- package/dist/next/routing/route-map.d.ts +6 -1
- package/dist/next/routing/route-map.js +14 -0
- package/dist/providers/db-firebase/base.repository.d.ts +1 -1
- package/dist/providers/db-firebase/base.repository.js +1 -1
- package/dist/repositories/index.d.ts +1 -1
- package/dist/repositories/index.js +1 -1
- package/dist/schemas/registry.d.ts +115 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +15 -2
- package/dist/server.js +32 -2
- package/dist/styles.css +36 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/HorizontalScroller.js +15 -2
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/dist/ui/components/PageHeroHeading.d.ts +40 -0
- package/dist/ui/components/PageHeroHeading.js +15 -0
- package/dist/ui/components/Typography.d.ts +3 -3
- package/dist/ui/components/Typography.js +59 -31
- package/dist/ui/components/Typography.style.css +35 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -2
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- package/dist/features/orders/utils/order-announcement-message.js +0 -16
|
@@ -182,6 +182,16 @@ export const DEFAULT_ITEM_REQUEST_DATA = {
|
|
|
182
182
|
replies: [],
|
|
183
183
|
replyCount: 0,
|
|
184
184
|
};
|
|
185
|
+
export const STORE_WHATSAPP_CONFIG_COLLECTION = "storeWhatsAppConfig";
|
|
186
|
+
export const STORE_WHATSAPP_CONFIG_INDEXED_FIELDS = [
|
|
187
|
+
"storeId",
|
|
188
|
+
"isConnected",
|
|
189
|
+
"isPaid",
|
|
190
|
+
];
|
|
191
|
+
export const DEFAULT_STORE_WHATSAPP_CONFIG_DATA = {
|
|
192
|
+
isConnected: false,
|
|
193
|
+
isPaid: false,
|
|
194
|
+
};
|
|
185
195
|
export const STORE_GOOGLE_CONFIG_COLLECTION = "storeGoogleConfig";
|
|
186
196
|
export const STORE_GOOGLE_CONFIG_INDEXED_FIELDS = [
|
|
187
197
|
"storeId",
|
|
@@ -6,6 +6,7 @@ import { BaseListingCard, Div, Heading, RichText, Row, Scrim, Span, Stack } from
|
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
7
7
|
import { normalizeRichTextHtml } from "../../../utils/index.js";
|
|
8
8
|
import { useLongPress } from "../../../react/hooks/useLongPress.js";
|
|
9
|
+
import { SellerTierBadge } from "./SellerTierBadge.js";
|
|
9
10
|
const __O = {
|
|
10
11
|
hidden: "overflow-hidden",
|
|
11
12
|
};
|
|
@@ -25,7 +26,7 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
|
|
|
25
26
|
].join(" "), onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
|
|
26
27
|
e.preventDefault();
|
|
27
28
|
onSelect(store.id, !isSelected);
|
|
28
|
-
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
|
|
29
|
+
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), _jsx(SellerTierBadge, { isVerified: store.isVerified, sellerTier: store.sellerTier, size: "xs" }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
|
|
29
30
|
if (!renderOwnerActions)
|
|
30
31
|
return card;
|
|
31
32
|
/*
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Trusted Seller criteria, rendered once for three audiences.
|
|
3
|
+
*
|
|
4
|
+
* - the seller's dashboard panel (their real figures against each target)
|
|
5
|
+
* - the public `/seller-guide` (targets only — no seller to measure)
|
|
6
|
+
* - `/help/seller-badges` and the admin trust guide (same, as a reference)
|
|
7
|
+
*
|
|
8
|
+
* 🛑 ONE component for all of them, driven by `sellerStandingRows()`. The
|
|
9
|
+
* whole point of this feature's config module is that the numbers a seller is
|
|
10
|
+
* chasing and the numbers a guide page promises cannot be different numbers —
|
|
11
|
+
* and the surest way to reintroduce that is a second table (Root Cause
|
|
12
|
+
* #73/#75). `metrics` absent is what distinguishes the guide from the panel,
|
|
13
|
+
* not a separate component.
|
|
14
|
+
*/
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { type SellerStandingThresholds, type SellerTier } from "../../../_internal/shared/features/seller-standing/config";
|
|
17
|
+
import type { SellerStandingSnapshot } from "../types";
|
|
18
|
+
export interface SellerStandingPanelProps {
|
|
19
|
+
/** Absent on a store that has never been measured, and on the guide pages. */
|
|
20
|
+
standing?: SellerStandingSnapshot;
|
|
21
|
+
tier?: SellerTier;
|
|
22
|
+
/** Admin-tuned thresholds; defaults apply when omitted. */
|
|
23
|
+
thresholds?: SellerStandingThresholds;
|
|
24
|
+
/**
|
|
25
|
+
* Show "N of 5 met" and the progress bar. Off for the public guide, where
|
|
26
|
+
* there is no seller to be N-of-5 about.
|
|
27
|
+
*/
|
|
28
|
+
showProgress?: boolean;
|
|
29
|
+
heading?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function SellerStandingPanel({ standing, tier, thresholds, showProgress, heading, }: SellerStandingPanelProps): React.JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Div, Heading, Row, Stack, Text } from "../../../ui/index.js";
|
|
3
|
+
import { Icon } from "../../../ui/components/Icon.js";
|
|
4
|
+
import { Progress } from "../../../ui/components/Progress.js";
|
|
5
|
+
import { SELLER_TIER, formatSellerStandingValue, sellerStandingRows, } from "../../../_internal/shared/features/seller-standing/config.js";
|
|
6
|
+
function formatDate(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return null;
|
|
9
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
10
|
+
if (Number.isNaN(d.getTime()))
|
|
11
|
+
return null;
|
|
12
|
+
return d.toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
|
|
13
|
+
}
|
|
14
|
+
export function SellerStandingPanel({ standing, tier = SELLER_TIER.SELLER, thresholds, showProgress = false, heading = "Trusted Seller", }) {
|
|
15
|
+
const rows = sellerStandingRows(standing ?? null, thresholds);
|
|
16
|
+
const metCount = rows.filter((r) => r.met).length;
|
|
17
|
+
const isTrusted = tier === SELLER_TIER.TRUSTED;
|
|
18
|
+
const computedAt = formatDate(standing?.computedAt);
|
|
19
|
+
const nextComputeAt = formatDate(standing?.nextComputeAt);
|
|
20
|
+
return (_jsxs(Stack, { gap: "md", surface: "card", padding: "lg", rounded: "xl", children: [_jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", children: heading }), showProgress ? (_jsxs(Text, { size: "xs", color: "muted", children: [metCount, " of ", rows.length, " met"] })) : null] }), _jsx(Text, { size: "sm", color: "muted", children: isTrusted
|
|
21
|
+
? "Your store meets every criterion. Your badge is live, and your payouts start when you ship rather than on delivery."
|
|
22
|
+
: `Meet all ${rows.length} criteria in the same ${standing?.windowDays ?? thresholds?.windowDays ?? 90}-day window to earn the badge — and to be paid when you ship, rather than waiting for delivery.` })] }), showProgress ? _jsx(Progress, { value: metCount, max: rows.length, size: "sm", variant: "gradient" }) : null, _jsx(Stack, { gap: "sm", children: rows.map((row) => (_jsxs(Row, { gap: "sm", align: "start", children: [_jsx(Icon, { name: row.met ? "approve" : "confirm", size: "sm", tone: row.met ? "success" : "muted", "aria-hidden": true }), _jsxs(Stack, { gap: "xs", className: "min-w-0 flex-1", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", children: row.label }), _jsxs(Text, { size: "sm", color: row.met ? "inherit" : "muted", numeric: true, children: [row.actual === null
|
|
23
|
+
? "—"
|
|
24
|
+
: formatSellerStandingValue(row.actual, row.kind), " / ", formatSellerStandingValue(row.target, row.kind)] })] }), _jsx(Text, { size: "xs", color: "muted", children: row.why })] })] }, row.criterion))) }), _jsx(Div, { children: standing ? (_jsxs(Text, { size: "xs", color: "faint", children: ["Figures as of ", computedAt ?? "the last check", nextComputeAt ? ` · next update ${nextComputeAt}` : "", ". Updated once a month, on the last day."] })) : (_jsx(Text, { size: "xs", color: "faint", children: "Your figures are calculated on the last day of each month \u2014 nothing has been measured for your store yet." })) })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seller-trust badges: Verified and Trusted Seller.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Until 2026-10-24 NEITHER rendered anywhere a buyer could see.
|
|
5
|
+
* `isVerified` was in `PRIVATE_STORE_FIELDS`, so it was stripped from every
|
|
6
|
+
* public store payload — while `/sellers` filtered on it to offer a "verified
|
|
7
|
+
* sellers" view, `StoreCapabilitiesGuideView` told sellers in as many words
|
|
8
|
+
* that "the Verified badge appears on your store profile page and on every
|
|
9
|
+
* listing card", and the onboarding checklist offered "Request verified badge"
|
|
10
|
+
* as a step. Three surfaces describing a badge that could not exist.
|
|
11
|
+
*
|
|
12
|
+
* ## One component, every surface
|
|
13
|
+
*
|
|
14
|
+
* Store card, store header, the product page's "Sold by" block and the seller
|
|
15
|
+
* directory all render this. A per-surface copy is how the two badges would
|
|
16
|
+
* come to disagree about what "trusted" means — and one of them means we pay
|
|
17
|
+
* the seller before delivery, so that is not a drift worth risking.
|
|
18
|
+
*
|
|
19
|
+
* 🛑 NOT to be confused with `SellerTrustBadge` in `features/scams/`, which is
|
|
20
|
+
* the scam-registry lookup (is this store on the reported-scammer registry).
|
|
21
|
+
* Different question, different source, and both can show at once: a store can
|
|
22
|
+
* be verified by us and still have a report against it.
|
|
23
|
+
*
|
|
24
|
+
* ## Why no Tailwind colour utilities here
|
|
25
|
+
*
|
|
26
|
+
* `<Badge>` carries its own `appkit-badge--*` CSS, so this authors no colour
|
|
27
|
+
* classes at all — which sidesteps Root Cause #67 entirely rather than having
|
|
28
|
+
* to pick the right `-surface`/`-solid` pairing and get the ink to match.
|
|
29
|
+
*/
|
|
30
|
+
import React from "react";
|
|
31
|
+
import type { SellerTier } from "../../../_internal/shared/features/seller-standing/config";
|
|
32
|
+
export interface SellerTierBadgeProps {
|
|
33
|
+
/**
|
|
34
|
+
* 🛑 Takes the two FLAGS, not a store object.
|
|
35
|
+
*
|
|
36
|
+
* Every call site holds a different shape — `StoreListItem`, `StoreDetail`,
|
|
37
|
+
* a product page's denormalised seller block, an admin row — and a prop
|
|
38
|
+
* typed to one of them would force the others through a cast. A cast at a
|
|
39
|
+
* boundary is where Root Cause #98 lives.
|
|
40
|
+
*/
|
|
41
|
+
isVerified?: boolean;
|
|
42
|
+
sellerTier?: SellerTier;
|
|
43
|
+
size?: "xs" | "sm" | "md";
|
|
44
|
+
/**
|
|
45
|
+
* Where the badges explain themselves. Omit for no link — a card already
|
|
46
|
+
* links to the store, and nesting an anchor inside it is invalid HTML.
|
|
47
|
+
*/
|
|
48
|
+
href?: string;
|
|
49
|
+
/** Rendered as a link element supplied by the consumer (locale-aware). */
|
|
50
|
+
linkAs?: React.ElementType;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Renders nothing when a store has neither badge — and that is deliberate:
|
|
54
|
+
* an "unverified" chip would mark every ordinary seller as suspect, which is
|
|
55
|
+
* a claim we are not making about them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function SellerTierBadge({ isVerified, sellerTier, size, href, linkAs, }: SellerTierBadgeProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Row } from "../../../ui/index.js";
|
|
3
|
+
import { Icon } from "../../../ui/components/Icon.js";
|
|
4
|
+
import { SELLER_TIER } from "../../../_internal/shared/features/seller-standing/config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Renders nothing when a store has neither badge — and that is deliberate:
|
|
7
|
+
* an "unverified" chip would mark every ordinary seller as suspect, which is
|
|
8
|
+
* a claim we are not making about them.
|
|
9
|
+
*/
|
|
10
|
+
export function SellerTierBadge({ isVerified = false, sellerTier = SELLER_TIER.SELLER, size = "sm", href, linkAs, }) {
|
|
11
|
+
const isTrusted = sellerTier === SELLER_TIER.TRUSTED;
|
|
12
|
+
if (!isVerified && !isTrusted)
|
|
13
|
+
return null;
|
|
14
|
+
const badges = (_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [isVerified ? (_jsx(Badge, { variant: "info", size: size, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Icon, { name: "verify", size: size === "md" ? "md" : "sm", "aria-hidden": true }), "Verified"] }) })) : null, isTrusted ? (_jsx(Badge, { variant: "success", size: size, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Icon, { name: "review", size: size === "md" ? "md" : "sm", "aria-hidden": true }), "Trusted Seller"] }) })) : null] }));
|
|
15
|
+
if (!href)
|
|
16
|
+
return badges;
|
|
17
|
+
const Link = (linkAs ?? "a");
|
|
18
|
+
return (_jsx(Link, { href: href, "aria-label": "What do these seller badges mean?", children: badges }));
|
|
19
|
+
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ArrowLeft } from "lucide-react";
|
|
3
3
|
import { Alert, Code, Div, Heading, Li, Row, Section, Span, Stack, Table, Tbody, Td, Text, TextLink, Th, Thead, Tr, Ul } from "../../../ui/index.js";
|
|
4
4
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
5
|
+
import { SellerStandingPanel } from "./SellerStandingPanel.js";
|
|
5
6
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
6
7
|
const LIST_DISC = "list-disc leading-relaxed";
|
|
7
8
|
const SECTION_BODY = "space-y-4 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]";
|
|
@@ -30,7 +31,24 @@ const CAPABILITY_GROUPS = [
|
|
|
30
31
|
{
|
|
31
32
|
group: "Trust & Visibility",
|
|
32
33
|
items: [
|
|
33
|
-
|
|
34
|
+
/*
|
|
35
|
+
* 🛑 CORRECTED 2026-10-24. This read "Shows a Verified badge on your
|
|
36
|
+
* store profile and listing cards", and that was false in two ways.
|
|
37
|
+
*
|
|
38
|
+
* (1) The badge is driven by `StoreDocument.isVerified`, not by this
|
|
39
|
+
* capability — `verified_seller` is read by NOTHING: no gate, no
|
|
40
|
+
* render, anywhere. Granting it has never done a thing.
|
|
41
|
+
* (2) No badge rendered at all, because `isVerified` sat in
|
|
42
|
+
* `PRIVATE_STORE_FIELDS` and was stripped from every public store
|
|
43
|
+
* payload. A seller could read this sentence, be granted the
|
|
44
|
+
* capability, and correctly conclude the platform was broken.
|
|
45
|
+
*
|
|
46
|
+
* The badge now renders (`SellerTierBadge`) off `isVerified`. The
|
|
47
|
+
* capability stays declared because existing store documents carry it
|
|
48
|
+
* and removing a union member would fail their next write — but it is
|
|
49
|
+
* described as what it is.
|
|
50
|
+
*/
|
|
51
|
+
{ cap: "verified_seller", unlocks: "Legacy flag — kept so existing stores stay valid. The Verified badge is granted by an admin on the store itself, not by this capability.", defaultOn: false },
|
|
34
52
|
{ cap: "featured_placement", unlocks: "Your store is eligible to appear in featured store sections on the homepage.", defaultOn: false },
|
|
35
53
|
{ cap: "promotional_banner", unlocks: "Your store banner can be used in carousel and promotional homepage sections.", defaultOn: false },
|
|
36
54
|
{ cap: "priority_support", unlocks: "Your support tickets go to a dedicated queue with faster response SLA.", defaultOn: false },
|
|
@@ -45,6 +63,7 @@ const CAPABILITY_GROUPS = [
|
|
|
45
63
|
{ cap: "lower_commission_rate", unlocks: "Negotiated reduced platform fee. Requires a signed agreement and minimum volume.", defaultOn: false },
|
|
46
64
|
{ cap: "early_access_features", unlocks: "Access to beta features before they are publicly available.", defaultOn: false },
|
|
47
65
|
{ cap: "advanced_analytics", unlocks: "Cohort retention, basket analysis, and top-buyer breakdown (beyond summary stats).", defaultOn: false },
|
|
66
|
+
{ cap: "whatsapp_catalog_sync", unlocks: "Sync your store products to your WhatsApp Business Catalog automatically.", defaultOn: false },
|
|
48
67
|
],
|
|
49
68
|
},
|
|
50
69
|
];
|
|
@@ -88,8 +107,20 @@ function LockedFeatureGuide() {
|
|
|
88
107
|
return (_jsx(Stack, { gap: "md", children: LOCKED_FEATURES.map(({ cap, missing, howToUnlock, contractRequired }) => (_jsxs(Stack, { className: "border border-[var(--appkit-color-border)]", gap: "sm", rounded: "lg", padding: "md", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Text, { className: "font-mono text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: cap }), contractRequired && (_jsx(Span, { className: "inline-block bg-[var(--appkit-color-warning)]/10 text-[var(--appkit-color-warning)]", size: "xs", weight: "medium", rounded: "full", padding: "pill-xs", children: "Requires agreement" }))] }), _jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-1", size: "xs", weight: "semibold", children: "What you are missing:" }), _jsx(Ul, { spacing: "2xs", indent: "md", children: missing.map((item) => (_jsx(Li, { textSize: "sm", color: "muted", className: LIST_DISC, children: item }, item))) })] }), _jsxs(Div, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-1", size: "xs", weight: "semibold", children: "How to unlock:" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: howToUnlock })] })] }, cap))) }));
|
|
89
108
|
}
|
|
90
109
|
// -- Section 4: Verified seller badge -----------------------------------------
|
|
110
|
+
/*
|
|
111
|
+
* 🛑 REWRITTEN 2026-10-24. This section used to state a fourth, invented set
|
|
112
|
+
* of criteria — "store active for at least 6 months", "average rating of 4.5
|
|
113
|
+
* or above", "at least 50 delivered orders" — none of which any code has ever
|
|
114
|
+
* read, alongside `AdminStoresGuideView`'s own different numbers (">=20
|
|
115
|
+
* delivered orders, >=4.0 rating, 3+ months"). Two guides, two sets of
|
|
116
|
+
* promises, zero enforcement, and a badge that could not render at all.
|
|
117
|
+
*
|
|
118
|
+
* The two badges are now described as what they are, and the earned one
|
|
119
|
+
* renders its criteria from `SELLER_STANDING_DEFAULTS` so this page cannot
|
|
120
|
+
* drift from the job that grants it.
|
|
121
|
+
*/
|
|
91
122
|
function VerifiedSellerSection() {
|
|
92
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "
|
|
123
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Two different badges" }), _jsxs(Text, { className: "leading-relaxed", children: [_jsx(Span, { weight: "bold", children: "Verified" }), " means LetItRip has checked who you are. It is granted by an admin after an identity review, it is not earned by volume, and it does not expire on its own.", " ", _jsx(Span, { weight: "bold", children: "Trusted Seller" }), " is earned from your trading record and is recalculated every month \u2014 you can gain it and you can lose it. A store can hold either, both, or neither."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Getting Verified" }), _jsx(Text, { className: "leading-relaxed", children: "Open a support ticket from your store dashboard with category \"Account\" and subject \"Verified Seller Application\", and attach a government-issued photo ID. An admin reviews it and sets the badge on your store." })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Earning Trusted Seller" }), _jsx(Text, { className: "leading-relaxed", children: "Every criterion below is measured over the same rolling window, and all of them have to hold at the same time. Meeting them also changes when you are paid: your payout starts when you ship rather than when the parcel is delivered." })] }), _jsx(SellerStandingPanel, { heading: "The criteria" })] }));
|
|
93
124
|
}
|
|
94
125
|
// -- Section 5: Multiple stores -----------------------------------------------
|
|
95
126
|
function MultipleStoresSection() {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The store-creation form — step 2 of becoming a seller.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Why this exists. `createStoreAction` has been in the codebase with **no
|
|
5
|
+
* caller and no API route**, there was no `/store/create` page, and
|
|
6
|
+
* `/user/become-seller` collected ZERO fields — it was marketing copy and one
|
|
7
|
+
* button that flipped `role` to `seller` and set `storeStatus: "pending"`. So a
|
|
8
|
+
* real applicant became a seller and then could not open a storefront by any
|
|
9
|
+
* route in the product. Only `role: "tester"` got a store, via the auto-approve
|
|
10
|
+
* branch in `becomeSeller`. This form is the missing step.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 `<SectionForm>`, not a bare `<Form>`. `audit-form-sectionised` is
|
|
13
|
+
* strict-zero on multi-field forms that skip the section engine, and the engine
|
|
14
|
+
* is what supplies the pinned mobile action bar, the per-section error badges
|
|
15
|
+
* and the schema-driven field rendering every other form in the app has. The
|
|
16
|
+
* sections come from `storeCreateSchema`'s own annotations, so the fields, their
|
|
17
|
+
* labels and their validation have exactly one definition.
|
|
18
|
+
*
|
|
19
|
+
* 🛑 `<SectionForm>` does NOT mount a `FormShellContext` — it consumes whichever
|
|
20
|
+
* one the owning view provides and tolerates none, which means a view rendering
|
|
21
|
+
* only `<SectionForm>` gets section badges and no readable message anywhere.
|
|
22
|
+
* This component mounts the context AND renders `<FormErrorSummary>` itself.
|
|
23
|
+
*
|
|
24
|
+
* The submit handler is injected rather than imported: the server action lives
|
|
25
|
+
* in the consumer (`src/actions/seller.actions.ts`), and appkit must not reach
|
|
26
|
+
* into it. Same slot shape as `StoreCreateProductShell`'s `onSave`/`onPublish`.
|
|
27
|
+
*/
|
|
28
|
+
import React from "react";
|
|
29
|
+
import { type StoreCreateInput } from "../schemas/store-create";
|
|
30
|
+
export interface StoreCreateFormResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
/** Stable error CODE, never a server message — see the submit handler. */
|
|
33
|
+
code?: string;
|
|
34
|
+
/** Which field the failure belongs to, when the server can say. */
|
|
35
|
+
field?: keyof StoreCreateInput;
|
|
36
|
+
}
|
|
37
|
+
export interface StoreCreateFormProps {
|
|
38
|
+
/**
|
|
39
|
+
* Performs the create. Resolves `{ ok: true }` on success, or
|
|
40
|
+
* `{ ok: false, code }` with a mapped error code.
|
|
41
|
+
*/
|
|
42
|
+
onSubmit: (input: StoreCreateInput) => Promise<StoreCreateFormResult>;
|
|
43
|
+
/** Called after a successful create, for the consumer's redirect/toast. */
|
|
44
|
+
onCreated?: () => void;
|
|
45
|
+
}
|
|
46
|
+
export declare function StoreCreateForm({ onSubmit, onCreated }: StoreCreateFormProps): React.JSX.Element;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The store-creation form — step 2 of becoming a seller.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 Why this exists. `createStoreAction` has been in the codebase with **no
|
|
7
|
+
* caller and no API route**, there was no `/store/create` page, and
|
|
8
|
+
* `/user/become-seller` collected ZERO fields — it was marketing copy and one
|
|
9
|
+
* button that flipped `role` to `seller` and set `storeStatus: "pending"`. So a
|
|
10
|
+
* real applicant became a seller and then could not open a storefront by any
|
|
11
|
+
* route in the product. Only `role: "tester"` got a store, via the auto-approve
|
|
12
|
+
* branch in `becomeSeller`. This form is the missing step.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 `<SectionForm>`, not a bare `<Form>`. `audit-form-sectionised` is
|
|
15
|
+
* strict-zero on multi-field forms that skip the section engine, and the engine
|
|
16
|
+
* is what supplies the pinned mobile action bar, the per-section error badges
|
|
17
|
+
* and the schema-driven field rendering every other form in the app has. The
|
|
18
|
+
* sections come from `storeCreateSchema`'s own annotations, so the fields, their
|
|
19
|
+
* labels and their validation have exactly one definition.
|
|
20
|
+
*
|
|
21
|
+
* 🛑 `<SectionForm>` does NOT mount a `FormShellContext` — it consumes whichever
|
|
22
|
+
* one the owning view provides and tolerates none, which means a view rendering
|
|
23
|
+
* only `<SectionForm>` gets section badges and no readable message anywhere.
|
|
24
|
+
* This component mounts the context AND renders `<FormErrorSummary>` itself.
|
|
25
|
+
*
|
|
26
|
+
* The submit handler is injected rather than imported: the server action lives
|
|
27
|
+
* in the consumer (`src/actions/seller.actions.ts`), and appkit must not reach
|
|
28
|
+
* into it. Same slot shape as `StoreCreateProductShell`'s `onSave`/`onPublish`.
|
|
29
|
+
*/
|
|
30
|
+
import React from "react";
|
|
31
|
+
import { Div, Stack, Text } from "../../../ui/index.js";
|
|
32
|
+
import { FormShellContext, FormErrorSummary, useFormShellState } from "../../../ui/forms/index.js";
|
|
33
|
+
import { SectionForm, buildSectionsFromSchema, useSectionFormNav } from "../../shell/index.js";
|
|
34
|
+
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
35
|
+
import { loadCategoryOptions } from "../../categories/utils/load-category-options.js";
|
|
36
|
+
import { storeCreateSchema } from "../schemas/store-create.js";
|
|
37
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
38
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
39
|
+
import { useTranslations } from "next-intl";
|
|
40
|
+
export function StoreCreateForm({ onSubmit, onCreated }) {
|
|
41
|
+
const t = useTranslations();
|
|
42
|
+
const [values, setValues] = React.useState({
|
|
43
|
+
storeName: "",
|
|
44
|
+
storeDescription: "",
|
|
45
|
+
storeCategory: "",
|
|
46
|
+
});
|
|
47
|
+
const [isPending, setIsPending] = React.useState(false);
|
|
48
|
+
const { shellCtx, setFieldError, clearErrors, markSubmitAttempted } = useFormShellState(storeCreateSchema);
|
|
49
|
+
const handleChange = React.useCallback((partial) => {
|
|
50
|
+
setValues((prev) => Object.assign({}, prev, partial));
|
|
51
|
+
}, []);
|
|
52
|
+
/*
|
|
53
|
+
* 🛑 `PaginatedSelect`, not the inferred text input: there are over a
|
|
54
|
+
* thousand categories, so a flat list is unscannable and the >5-options rule
|
|
55
|
+
* applies. `loadCategoryOptions` passes `listingOnly=true`, without which
|
|
56
|
+
* this offers brands and bundles as categories.
|
|
57
|
+
*
|
|
58
|
+
* 🛑 `value={… || null}` — it renders the placeholder only on `value == null`,
|
|
59
|
+
* so an empty string paints a BLANK trigger with no prompt and no hint that
|
|
60
|
+
* it is a control.
|
|
61
|
+
*/
|
|
62
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(storeCreateSchema, {
|
|
63
|
+
renderers: {
|
|
64
|
+
storeCategory: ({ values: v, onChange }) => (_jsx(PaginatedSelect, { name: "storeCategory", value: v.storeCategory || null, onChange: (val) => onChange({ storeCategory: val ?? "" }), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026 (optional)", searchPlaceholder: "Type a category name\u2026", noResultsText: "No categories found", ariaLabel: "Main category" })),
|
|
65
|
+
},
|
|
66
|
+
}), []);
|
|
67
|
+
// Declared AFTER `sections`, because it takes them — it derives the ordered
|
|
68
|
+
// open/closed set from the sections that are currently visible.
|
|
69
|
+
const nav = useSectionFormNav(sections, values);
|
|
70
|
+
async function submit() {
|
|
71
|
+
clearErrors();
|
|
72
|
+
markSubmitAttempted();
|
|
73
|
+
const parsed = storeCreateSchema.safeParse(values);
|
|
74
|
+
if (!parsed.success) {
|
|
75
|
+
for (const issue of parsed.error.issues) {
|
|
76
|
+
setFieldError(String(issue.path[0]), issue.message);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setIsPending(true);
|
|
81
|
+
try {
|
|
82
|
+
const result = await onSubmit(parsed.data);
|
|
83
|
+
if (result.ok) {
|
|
84
|
+
onCreated?.();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
/*
|
|
88
|
+
* 🛑 `toUserMessage(code, t)`, never the server's own message. A
|
|
89
|
+
* raw-message fallback is how a Node MODULE_NOT_FOUND with a full
|
|
90
|
+
* require stack got rendered inside a buyer-facing modal (Root Cause
|
|
91
|
+
* #86). `toUserMessage` terminates at a generic string and never
|
|
92
|
+
* returns server text.
|
|
93
|
+
*/
|
|
94
|
+
setFieldError(result.field ?? "storeName", toUserMessage(result.code ?? "UNKNOWN", t));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// Every `NormalizedError` variant carries a non-optional `code`, so
|
|
98
|
+
// there is nothing to default here.
|
|
99
|
+
const e = normalizeError(err);
|
|
100
|
+
setFieldError("storeName", toUserMessage(e.code, t));
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
setIsPending(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Div, { children: [_jsxs(Stack, { gap: "xs", className: "mb-4", children: [_jsx(Text, { size: "lg", weight: "semibold", children: "Open your storefront" }), _jsx(Text, { size: "sm", color: "muted", children: "You can change all of this later. Your store stays private until we review it." })] }), _jsx(SectionForm, { sections: sections, values: values, onChange: handleChange, onSubmit: submit, schema: storeCreateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isPending, submitLabel: "Create my store" }), _jsx(FormErrorSummary, {})] }) }));
|
|
107
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, } from "lucide-react";
|
|
3
3
|
import { Div, Grid, Heading, Progress, Row, Section, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
4
|
+
import { SellerStandingPanel } from "./SellerStandingPanel.js";
|
|
4
5
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
5
6
|
const CLS_CHECK_ICON = "w-5 h-5 flex-shrink-0 text-emerald-500";
|
|
6
7
|
const CLS_SUCCESS_BOX = "rounded-xl border border-emerald-200 bg-emerald-50 dark:bg-emerald-950/30 dark:border-emerald-800 px-[var(--appkit-space-5)] py-[var(--appkit-space-4)]";
|
|
@@ -27,7 +28,7 @@ const GUIDE_CARDS = [
|
|
|
27
28
|
{
|
|
28
29
|
Icon: Settings,
|
|
29
30
|
title: "Settings",
|
|
30
|
-
description: "Store profile, shipping config, return policy, and
|
|
31
|
+
description: "Store profile, shipping config, return policy, vacation mode, and WhatsApp integration.",
|
|
31
32
|
href: String(ROUTES.STORE.GUIDE_SETTINGS),
|
|
32
33
|
},
|
|
33
34
|
{
|
|
@@ -65,10 +66,22 @@ function buildChecklist(store) {
|
|
|
65
66
|
done: (store?.shippingConfig?.providers?.length ?? 0) > 0,
|
|
66
67
|
href: String(ROUTES.STORE.SHIPPING_CONFIGS),
|
|
67
68
|
},
|
|
69
|
+
/*
|
|
70
|
+
* 🛑 Was "Request verified badge" → a SUPPORT TICKET, and the cast beside
|
|
71
|
+
* it (`store as StoreDocument & { isVerified?: boolean }`) was the tell
|
|
72
|
+
* that `isVerified` was not even on the type. Two things were wrong: the
|
|
73
|
+
* badge was not something to request (an admin grants it after checking
|
|
74
|
+
* identity, and no ticket category asked for it), and the step it pointed
|
|
75
|
+
* at could not complete itself.
|
|
76
|
+
*
|
|
77
|
+
* The earned tier is a real step with a real destination — the
|
|
78
|
+
* capabilities guide, where the criteria and the seller's own figures are
|
|
79
|
+
* both rendered from `SELLER_STANDING_DEFAULTS`.
|
|
80
|
+
*/
|
|
68
81
|
{
|
|
69
|
-
label: "
|
|
70
|
-
done:
|
|
71
|
-
href: String(ROUTES.
|
|
82
|
+
label: "Earn Trusted Seller status",
|
|
83
|
+
done: store?.sellerTier === "trusted",
|
|
84
|
+
href: String(ROUTES.STORE.GUIDE_CAPABILITIES),
|
|
72
85
|
},
|
|
73
86
|
];
|
|
74
87
|
}
|
|
@@ -77,7 +90,7 @@ export function StoreGuideHubView({ store }) {
|
|
|
77
90
|
const storeName = store?.storeName ?? "Your Store";
|
|
78
91
|
const steps = buildChecklist(store);
|
|
79
92
|
const completedCount = steps.filter((s) => s.done).length;
|
|
80
|
-
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }), _jsx(Text, { color: "inverse-muted", size: "base", children: "Everything you need to know about selling on LetItRip." })] }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-3", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: GUIDE_CARDS.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Getting started" }), _jsxs(Div, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsxs(Div, { className: "px-[1.25rem] pt-[1.25rem] border-b border-[var(--appkit-color-border)]", padding: "b-md", children: [_jsxs(Row, { className: "mb-2", align: "center", justify: "between", children: [_jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: [completedCount, " of ", steps.length, " complete"] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [Math.round((completedCount / steps.length) * 100), "%"] })] }), _jsx(Progress, { value: completedCount, max: steps.length, size: "sm", variant: "gradient" })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", children: steps.map(({ label, done, href }) => (_jsxs(TextLink, { variant: "bare", href: done ? "#" : href, "aria-disabled": done, layout: "flex", align: "center", gap: "sm", paddingX: "lg", paddingY: "sm-tall", size: "sm", className: `transition-colors ${done
|
|
93
|
+
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }), _jsx(Text, { color: "inverse-muted", size: "base", children: "Everything you need to know about selling on LetItRip." })] }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-3", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: GUIDE_CARDS.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) }), _jsx(SellerStandingPanel, { standing: store?.standing, tier: store?.sellerTier, showProgress: true, heading: "Your Trusted Seller standing" })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Getting started" }), _jsxs(Div, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsxs(Div, { className: "px-[1.25rem] pt-[1.25rem] border-b border-[var(--appkit-color-border)]", padding: "b-md", children: [_jsxs(Row, { className: "mb-2", align: "center", justify: "between", children: [_jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: [completedCount, " of ", steps.length, " complete"] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [Math.round((completedCount / steps.length) * 100), "%"] })] }), _jsx(Progress, { value: completedCount, max: steps.length, size: "sm", variant: "gradient" })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", children: steps.map(({ label, done, href }) => (_jsxs(TextLink, { variant: "bare", href: done ? "#" : href, "aria-disabled": done, layout: "flex", align: "center", gap: "sm", paddingX: "lg", paddingY: "sm-tall", size: "sm", className: `transition-colors ${done
|
|
81
94
|
? "cursor-default"
|
|
82
95
|
: "hover:bg-[var(--appkit-color-border)]/30"}`, children: [done ? (_jsx(CheckCircle2, { className: CLS_CHECK_ICON })) : (_jsx(Circle, { className: "w-5 h-5 flex-shrink-0 text-[var(--appkit-color-text-muted)]" })), _jsx(Text, { className: `leading-snug ${done
|
|
83
96
|
? "line-through text-[var(--appkit-color-text-muted)]"
|
|
@@ -4,6 +4,7 @@ import { MediaImage } from "../../media/MediaImage.js";
|
|
|
4
4
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
5
5
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
6
6
|
import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge.js";
|
|
7
|
+
import { SellerTierBadge } from "./SellerTierBadge.js";
|
|
7
8
|
import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
|
|
8
9
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
9
10
|
const __O = {
|
|
@@ -14,7 +15,7 @@ const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-war
|
|
|
14
15
|
const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
|
|
15
16
|
const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
|
|
16
17
|
export function StoreHeader({ store, labels = {}, onFollow, className = "", trust, }) {
|
|
17
|
-
return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsx(OwnerQuickActions, { resource: "STORE", placement: "detail", editHref: String(ROUTES.ADMIN.STORE_DETAIL(store.storeSlug)), recordLabel: store.storeName }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
|
|
18
|
+
return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), _jsx(SellerTierBadge, { isVerified: store.isVerified, sellerTier: store.sellerTier, href: String(ROUTES.PUBLIC.HELP_SELLER_BADGES) }), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsx(OwnerQuickActions, { resource: "STORE", placement: "detail", editHref: String(ROUTES.ADMIN.STORE_DETAIL(store.storeSlug)), recordLabel: store.storeName }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
|
|
18
19
|
labels.vacationMode ??
|
|
19
20
|
"Store is on vacation mode" }))] })] }));
|
|
20
21
|
}
|
|
@@ -26,7 +26,11 @@ function ReturnPolicySection() {
|
|
|
26
26
|
function VacationModeSection() {
|
|
27
27
|
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "What vacation mode does" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, children: "All your published listings are hidden from search results and browse pages." }), _jsx(Li, { className: LIST_DISC, children: "Your store profile page shows a \"On Vacation\" banner." }), _jsx(Li, { className: LIST_DISC, children: "Buyers cannot add your items to cart or checkout while on vacation." }), _jsx(Li, { className: LIST_DISC, children: "Existing orders that were placed before vacation mode must still be fulfilled." })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "How to enable" }), _jsx(Text, { className: "leading-relaxed", children: "Go to your Store Dashboard \u2192 Settings \u2192 Storefront tab. Toggle \"Vacation mode\" on. Your listings re-appear instantly when you toggle it off." })] }), _jsx(Alert, { variant: "warning", title: "Ongoing orders are not paused", children: "Enabling vacation mode does not pause or cancel any existing orders. You are still responsible for fulfilling orders placed before vacation mode was enabled." })] }));
|
|
28
28
|
}
|
|
29
|
-
// -- Section 5:
|
|
29
|
+
// -- Section 5: WhatsApp integration ------------------------------------------
|
|
30
|
+
function WhatsAppSection() {
|
|
31
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Text, { className: "leading-relaxed", children: ["The WhatsApp integration allows your customers to contact you via WhatsApp from your store profile and lets you sync your product catalog to your WhatsApp Business Catalog (requires the", _jsx(Code, { className: "mx-1", size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_catalog_sync" }), " capability)."] }), _jsxs(Text, { className: "leading-relaxed", children: ["Full settings and the catalog sync toggle are at", " ", _jsx(TextLink, { variant: "bare", href: String(ROUTES.STORE.WHATSAPP), className: "underline text-[var(--appkit-color-primary)] hover:opacity-80", children: "Settings \u2192 WhatsApp" }), ". See the WhatsApp settings page for step-by-step setup instructions."] })] }));
|
|
32
|
+
}
|
|
33
|
+
// -- Section 6: Visibility states ---------------------------------------------
|
|
30
34
|
function VisibilityStatesSection() {
|
|
31
35
|
const STATES = [
|
|
32
36
|
{
|
|
@@ -49,5 +53,5 @@ function VisibilityStatesSection() {
|
|
|
49
53
|
return (_jsx(Stack, { gap: "3", children: STATES.map(({ state, note }) => (_jsxs(Row, { textSize: "sm", gap: "3", children: [_jsx(Text, { className: "shrink-0 w-32 text-[var(--appkit-color-text)]", weight: "semibold", children: state }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", children: note })] }, state))) }));
|
|
50
54
|
}
|
|
51
55
|
export function StoreSettingsGuideView(_props) {
|
|
52
|
-
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "Settings Guide" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Store profile, shipping, returns, vacation mode, and visibility." })] }), _jsx(GuideSection, { title: "1. Store profile", children: _jsx(StoreProfileSection, {}) }), _jsx(GuideSection, { title: "2. Shipping configuration", children: _jsx(ShippingSection, {}) }), _jsx(GuideSection, { title: "3. Return policy", children: _jsx(ReturnPolicySection, {}) }), _jsx(GuideSection, { title: "4. Vacation mode", children: _jsx(VacationModeSection, {}) }), _jsx(GuideSection, { title: "5. Visibility states", children: _jsx(VisibilityStatesSection, {}) })] }));
|
|
56
|
+
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "Settings Guide" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Store profile, shipping, returns, vacation mode, WhatsApp, and visibility." })] }), _jsx(GuideSection, { title: "1. Store profile", children: _jsx(StoreProfileSection, {}) }), _jsx(GuideSection, { title: "2. Shipping configuration", children: _jsx(ShippingSection, {}) }), _jsx(GuideSection, { title: "3. Return policy", children: _jsx(ReturnPolicySection, {}) }), _jsx(GuideSection, { title: "4. Vacation mode", children: _jsx(VacationModeSection, {}) }), _jsx(GuideSection, { title: "5. WhatsApp integration", children: _jsx(WhatsAppSection, {}) }), _jsx(GuideSection, { title: "6. Visibility states", children: _jsx(VisibilityStatesSection, {}) })] }));
|
|
53
57
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Data access layer for the `stores` Firestore collection.
|
|
5
5
|
* One store per seller; identified by storeSlug (used as document ID).
|
|
6
6
|
*/
|
|
7
|
-
import { BaseRepository, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
7
|
+
import { BaseRepository, type SieveModel, type FirebaseSieveResult, type DocumentSnapshot } from "../../../providers/db-firebase";
|
|
8
8
|
import { StoreDocument } from "../schemas";
|
|
9
9
|
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
10
10
|
import type { JsonValue } from "../../../schemas/types";
|
|
@@ -17,6 +17,28 @@ export interface StoreWriteContext {
|
|
|
17
17
|
}
|
|
18
18
|
export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
19
19
|
constructor();
|
|
20
|
+
protected mapDoc<D = StoreDocument>(snap: DocumentSnapshot): D;
|
|
21
|
+
/**
|
|
22
|
+
* LIST reads never carry the WhatsApp token — not even encrypted.
|
|
23
|
+
*
|
|
24
|
+
* `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
|
|
25
|
+
* PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
|
|
26
|
+
* `mapDoc` (it previously mapped documents itself and skipped every
|
|
27
|
+
* override), that decrypt would have put a plaintext Meta OAuth bearer token
|
|
28
|
+
* on a public listing — the exact shape of Root Cause #70.
|
|
29
|
+
*
|
|
30
|
+
* So the list path deliberately does NOT call `decryptSecrets`, and drops
|
|
31
|
+
* `accessToken` outright: a list has no legitimate use for it, and a value
|
|
32
|
+
* that is never in the payload cannot be leaked by a projection someone
|
|
33
|
+
* forgets to write.
|
|
34
|
+
*
|
|
35
|
+
* Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
|
|
36
|
+
* WhatsApp settings and catalog-sync routes keep working exactly as before.
|
|
37
|
+
* Removing the decrypt from `mapDoc` itself is a separate, riskier change
|
|
38
|
+
* that CLAUDE.md explicitly defers; this closes the public-exposure half
|
|
39
|
+
* without touching the encryption round-trip at all.
|
|
40
|
+
*/
|
|
41
|
+
update(id: string, data: Partial<StoreDocument>): Promise<StoreDocument>;
|
|
20
42
|
/**
|
|
21
43
|
* Create a new store.
|
|
22
44
|
* The document ID is set to storeSlug for easy URL-based lookups.
|