@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.
Files changed (232) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  3. package/dist/_internal/server/features/categories/adapters.js +13 -0
  4. package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
  5. package/dist/_internal/server/features/checkout/actions.js +22 -5
  6. package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
  7. package/dist/_internal/server/features/events/like-actions.js +67 -0
  8. package/dist/_internal/server/features/events/likes.d.ts +65 -0
  9. package/dist/_internal/server/features/events/likes.js +146 -0
  10. package/dist/_internal/server/features/orders/adapters.js +3 -1
  11. package/dist/_internal/server/features/seo/index.d.ts +3 -0
  12. package/dist/_internal/server/features/seo/index.js +2 -0
  13. package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
  14. package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
  15. package/dist/_internal/server/features/seo/robots.js +20 -1
  16. package/dist/_internal/server/features/seo/sitemap.js +125 -27
  17. package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
  18. package/dist/_internal/server/features/seo/static-params.js +228 -0
  19. package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
  20. package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
  21. package/dist/_internal/server/features/site-settings/adapters.js +20 -2
  22. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  23. package/dist/_internal/server/features/stores/adapters.js +88 -1
  24. package/dist/_internal/server/functions/scheduled.d.ts +16 -1
  25. package/dist/_internal/server/functions/scheduled.js +23 -1
  26. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
  27. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  28. package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
  29. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
  30. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
  31. package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
  32. package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
  33. package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
  34. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
  35. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
  36. package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
  37. package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
  38. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
  39. package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
  40. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  41. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  42. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
  43. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
  44. package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
  45. package/dist/_internal/server/notifications/channel-health.js +2 -2
  46. package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
  47. package/dist/_internal/server/notifications/send-guard.js +4 -2
  48. package/dist/_internal/shared/actions/action-registry.js +89 -0
  49. package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
  50. package/dist/_internal/shared/checkout/order-math.js +1 -1
  51. package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
  52. package/dist/_internal/shared/features/checkout/config.js +2 -0
  53. package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
  54. package/dist/_internal/shared/features/messaging/config.js +4 -1
  55. package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
  56. package/dist/_internal/shared/features/seller-standing/config.js +153 -0
  57. package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
  58. package/dist/_internal/shared/features/site-settings/fees.js +2 -0
  59. package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
  60. package/dist/_internal/shared/fees/calculator.d.ts +8 -0
  61. package/dist/_internal/shared/fees/calculator.js +7 -0
  62. package/dist/cli/index.js +2 -0
  63. package/dist/client.d.ts +2 -2
  64. package/dist/client.js +1 -1
  65. package/dist/constants/api-endpoints.d.ts +39 -0
  66. package/dist/constants/api-endpoints.js +16 -0
  67. package/dist/constants/notification-types.d.ts +1 -1
  68. package/dist/constants/notification-types.js +1 -1
  69. package/dist/core/integration-keys.d.ts +5 -0
  70. package/dist/core/integration-keys.js +10 -0
  71. package/dist/features/about/components/FeesView.js +6 -0
  72. package/dist/features/about/components/HelpPageView.js +9 -1
  73. package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
  74. package/dist/features/account/components/BecomeSellerView.js +2 -2
  75. package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
  76. package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
  77. package/dist/features/account/types/index.d.ts +2 -0
  78. package/dist/features/admin/actions/notification-actions.d.ts +17 -2
  79. package/dist/features/admin/actions/notification-actions.js +154 -9
  80. package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
  81. package/dist/features/admin/components/AdminOrdersView.js +2 -0
  82. package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
  83. package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
  84. package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
  85. package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
  86. package/dist/features/admin/components/AdminStoresView.js +59 -2
  87. package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
  88. package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
  89. package/dist/features/admin/constants/site-settings-tabs.js +12 -6
  90. package/dist/features/admin/repository/site-settings.repository.js +1 -1
  91. package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
  92. package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
  93. package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
  94. package/dist/features/admin/schemas/admin-user-form.js +44 -3
  95. package/dist/features/admin/schemas/firestore.d.ts +73 -0
  96. package/dist/features/admin/schemas/firestore.js +8 -3
  97. package/dist/features/auth/checkout-value-otp.d.ts +1 -1
  98. package/dist/features/auth/checkout-value-otp.js +1 -1
  99. package/dist/features/auth/permissions/constants.d.ts +2 -2
  100. package/dist/features/auth/permissions/constants.js +2 -1
  101. package/dist/features/blog/components/BlogIndexPageView.js +2 -2
  102. package/dist/features/blog/components/BlogListView.js +2 -1
  103. package/dist/features/blog/components/BlogPostView.js +2 -1
  104. package/dist/features/blog/constants/category-badge.d.ts +33 -0
  105. package/dist/features/blog/constants/category-badge.js +40 -0
  106. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  107. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  108. package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
  109. package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
  110. package/dist/features/cart/repository/cart.repository.js +2 -1
  111. package/dist/features/cart/schemas/firestore.d.ts +2 -1
  112. package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
  113. package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
  114. package/dist/features/categories/schemas/firestore.d.ts +25 -0
  115. package/dist/features/categories/utils/load-category-options.d.ts +33 -0
  116. package/dist/features/categories/utils/load-category-options.js +50 -0
  117. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
  118. package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
  119. package/dist/features/events/components/EventCard.js +4 -12
  120. package/dist/features/events/components/EventInsights.d.ts +27 -0
  121. package/dist/features/events/components/EventInsights.js +10 -0
  122. package/dist/features/events/components/EventLikeButton.d.ts +21 -0
  123. package/dist/features/events/components/EventLikeButton.js +59 -0
  124. package/dist/features/events/components/EventLikersRail.d.ts +23 -0
  125. package/dist/features/events/components/EventLikersRail.js +54 -0
  126. package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
  127. package/dist/features/events/components/EventTypeBadge.js +7 -0
  128. package/dist/features/events/components/EventsListPageView.js +2 -2
  129. package/dist/features/events/components/SpinWheel.d.ts +41 -0
  130. package/dist/features/events/components/SpinWheel.js +128 -0
  131. package/dist/features/events/components/SpinWheelView.js +36 -15
  132. package/dist/features/events/constants/event-type-meta.d.ts +54 -0
  133. package/dist/features/events/constants/event-type-meta.js +109 -0
  134. package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
  135. package/dist/features/events/repository/event-likes.repository.js +151 -0
  136. package/dist/features/events/schemas/firestore.d.ts +111 -0
  137. package/dist/features/events/schemas/likes.d.ts +81 -0
  138. package/dist/features/events/schemas/likes.js +59 -0
  139. package/dist/features/events/types/index.d.ts +7 -0
  140. package/dist/features/events/utils/carousel-slide.d.ts +42 -0
  141. package/dist/features/events/utils/carousel-slide.js +174 -0
  142. package/dist/features/events/utils/event-charts.d.ts +65 -0
  143. package/dist/features/events/utils/event-charts.js +172 -0
  144. package/dist/features/faq/constants/help-guides.js +9 -0
  145. package/dist/features/faq/schemas/firestore.js +1 -0
  146. package/dist/features/faq/types/index.d.ts +19 -0
  147. package/dist/features/homepage/components/BrandsSection.js +25 -1
  148. package/dist/features/homepage/components/HeroCarousel.js +47 -10
  149. package/dist/features/homepage/lib/section-renderer.js +1 -1
  150. package/dist/features/homepage/types/index.d.ts +7 -0
  151. package/dist/features/orders/actions/refund-actions.js +1 -0
  152. package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
  153. package/dist/features/orders/components/OrderAddonBadges.js +4 -3
  154. package/dist/features/orders/repository/orders.repository.d.ts +8 -0
  155. package/dist/features/orders/repository/orders.repository.js +2 -0
  156. package/dist/features/orders/schemas/firestore.d.ts +4 -0
  157. package/dist/features/orders/types/index.d.ts +5 -2
  158. package/dist/features/search/action-index/settings-entries.js +9 -0
  159. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
  160. package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
  161. package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
  162. package/dist/features/store-extensions/schemas/firestore.js +10 -0
  163. package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
  164. package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
  165. package/dist/features/stores/components/SellerStandingPanel.js +25 -0
  166. package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
  167. package/dist/features/stores/components/SellerTierBadge.js +19 -0
  168. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
  169. package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
  170. package/dist/features/stores/components/StoreCreateForm.js +107 -0
  171. package/dist/features/stores/components/StoreGuideHubView.js +18 -5
  172. package/dist/features/stores/components/StoreHeader.js +2 -1
  173. package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
  174. package/dist/features/stores/repository/store.repository.d.ts +23 -1
  175. package/dist/features/stores/repository/store.repository.js +52 -1
  176. package/dist/features/stores/schemas/firestore.d.ts +76 -7
  177. package/dist/features/stores/schemas/firestore.js +21 -6
  178. package/dist/features/stores/schemas/index.d.ts +88 -2
  179. package/dist/features/stores/schemas/index.js +14 -0
  180. package/dist/features/stores/schemas/store-create.d.ts +47 -0
  181. package/dist/features/stores/schemas/store-create.js +86 -0
  182. package/dist/features/stores/types/index.d.ts +42 -0
  183. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
  184. package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
  185. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
  186. package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
  187. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
  188. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
  189. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
  190. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
  191. package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
  192. package/dist/features/whatsapp-bot/components/index.js +2 -0
  193. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
  194. package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
  195. package/dist/features/whatsapp-bot/index.d.ts +3 -0
  196. package/dist/features/whatsapp-bot/index.js +3 -0
  197. package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
  198. package/dist/features/whatsapp-bot/manifest.js +13 -0
  199. package/dist/features/whatsapp-bot/messages/en.json +7 -0
  200. package/dist/features/whatsapp-bot/server.d.ts +7 -0
  201. package/dist/features/whatsapp-bot/server.js +7 -0
  202. package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
  203. package/dist/features/whatsapp-bot/types/index.js +1 -0
  204. package/dist/index.d.ts +23 -6
  205. package/dist/index.js +38 -12
  206. package/dist/next/routing/route-map.d.ts +6 -1
  207. package/dist/next/routing/route-map.js +14 -0
  208. package/dist/providers/db-firebase/base.repository.d.ts +1 -1
  209. package/dist/providers/db-firebase/base.repository.js +1 -1
  210. package/dist/repositories/index.d.ts +1 -1
  211. package/dist/repositories/index.js +1 -1
  212. package/dist/schemas/registry.d.ts +115 -0
  213. package/dist/schemas/registry.js +2 -1
  214. package/dist/security/pii-encrypt.js +1 -1
  215. package/dist/server.d.ts +15 -2
  216. package/dist/server.js +32 -2
  217. package/dist/styles.css +36 -1
  218. package/dist/tailwind-utilities.css +1 -1
  219. package/dist/ui/components/HorizontalScroller.js +15 -2
  220. package/dist/ui/components/NavigationLoader.d.ts +14 -1
  221. package/dist/ui/components/NavigationLoader.js +79 -30
  222. package/dist/ui/components/PageHeroHeading.d.ts +40 -0
  223. package/dist/ui/components/PageHeroHeading.js +15 -0
  224. package/dist/ui/components/Typography.d.ts +3 -3
  225. package/dist/ui/components/Typography.js +59 -31
  226. package/dist/ui/components/Typography.style.css +35 -0
  227. package/dist/ui/index.d.ts +2 -0
  228. package/dist/ui/index.js +1 -0
  229. package/package.json +1 -1
  230. package/scripts/seed-cli.mjs +4 -2
  231. package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
  232. 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
- { cap: "verified_seller", unlocks: "Shows a Verified badge on your store profile and listing cards.", defaultOn: false },
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: "What it means for buyers" }), _jsx(Text, { className: "leading-relaxed", children: "The Verified badge appears on your store profile page and on every listing card. It signals to buyers that LetItRip has reviewed your identity and track record. Verified sellers typically see higher conversion rates, especially on high-value items." })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Requirements" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, children: "Government-issued photo ID submitted to LetItRip support." }), _jsx(Li, { className: LIST_DISC, children: "Store active for at least 6 months." }), _jsx(Li, { className: LIST_DISC, children: "Average rating of 4.5 or above (across all reviews)." }), _jsx(Li, { className: LIST_DISC, children: "At least 50 delivered orders with no outstanding fraud reports." }), _jsx(Li, { className: LIST_DISC, children: "No active hard ban or store suspension in the past 12 months." })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "How to apply" }), _jsx(Text, { className: "leading-relaxed", children: "Open a support ticket from your store dashboard with category \"Account\" and subject \"Verified Seller Application\". Attach your government ID scan. Our team reviews within 10 business days." })] })] }));
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 vacation mode.",
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: "Request verified badge",
70
- done: Boolean(store?.isVerified),
71
- href: String(ROUTES.USER.SUPPORT),
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: Visibility states ---------------------------------------------
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.