@mohasinac/appkit 4.64.0 → 4.65.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 (163) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/checkout/actions.js +18 -1
  3. package/dist/_internal/server/features/search/index-build/sources.js +27 -12
  4. package/dist/_internal/server/features/seo/sitemap.js +78 -12
  5. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  6. package/dist/_internal/server/features/site-settings/adapters.js +16 -1
  7. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  8. package/dist/_internal/server/features/stores/adapters.js +14 -0
  9. package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
  10. package/dist/_internal/server/features/stores/tabs.js +20 -21
  11. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
  12. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
  13. package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
  14. package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
  15. package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
  16. package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
  17. package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
  18. package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
  19. package/dist/client.d.ts +5 -2
  20. package/dist/client.js +8 -1
  21. package/dist/constants/field-names.d.ts +1 -0
  22. package/dist/constants/field-names.js +14 -0
  23. package/dist/features/account/api/[userId]/addresses/route.js +1 -5
  24. package/dist/features/addresses/repository/addresses.repository.js +1 -1
  25. package/dist/features/admin/api/bids/route.js +1 -5
  26. package/dist/features/admin/api/coupons/route.js +1 -5
  27. package/dist/features/admin/api/products/route.js +1 -5
  28. package/dist/features/admin/api/reviews/route.js +1 -5
  29. package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
  31. package/dist/features/admin/components/AdminGuideHubView.js +1 -1
  32. package/dist/features/admin/components/AdminProductEditorView.js +4 -1
  33. package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
  34. package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
  35. package/dist/features/admin/components/OwnerQuickActions.js +1 -1
  36. package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
  37. package/dist/features/admin/schemas/firestore.d.ts +52 -0
  38. package/dist/features/admin/schemas/firestore.js +20 -0
  39. package/dist/features/auctions/api/route.js +1 -8
  40. package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
  41. package/dist/features/auth/hooks/useAuth.js +1 -1
  42. package/dist/features/before-after/api/route.js +1 -5
  43. package/dist/features/blog/api/route.js +1 -8
  44. package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
  45. package/dist/features/blog/components/BlogIndexPageView.js +6 -9
  46. package/dist/features/cart/actions/cart-actions.js +1 -1
  47. package/dist/features/cart/api/route.js +1 -5
  48. package/dist/features/cart/utils/guest-cart.js +1 -5
  49. package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
  50. package/dist/features/categories/api/route.js +5 -23
  51. package/dist/features/categories/components/BundlesListView.d.ts +2 -3
  52. package/dist/features/categories/components/BundlesListView.js +2 -5
  53. package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
  54. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
  55. package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
  56. package/dist/features/categories/constants/category-tabs.d.ts +1 -1
  57. package/dist/features/categories/constants/category-tabs.js +7 -0
  58. package/dist/features/categories/hooks/useCategories.d.ts +6 -0
  59. package/dist/features/categories/hooks/useCategories.js +73 -2
  60. package/dist/features/categories/types/index.d.ts +1 -1
  61. package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
  62. package/dist/features/collections/api/route.js +1 -5
  63. package/dist/features/consultation/api/route.js +1 -5
  64. package/dist/features/contact/email.js +2 -5
  65. package/dist/features/corporate/api/route.js +1 -5
  66. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
  67. package/dist/features/events/components/AdminEventEditorView.js +1 -1
  68. package/dist/features/events/components/EventsListPageView.d.ts +2 -3
  69. package/dist/features/events/components/EventsListPageView.js +8 -11
  70. package/dist/features/faq/constants/help-guides.d.ts +8 -5
  71. package/dist/features/faq/constants/help-guides.js +9 -5
  72. package/dist/features/filters/AsyncFacetSection.js +2 -2
  73. package/dist/features/filters/FilterFacetSection.js +5 -5
  74. package/dist/features/filters/RangeFilter.js +2 -2
  75. package/dist/features/filters/SwitchFilter.js +2 -2
  76. package/dist/features/filters/filterUtils.d.ts +0 -2
  77. package/dist/features/filters/filterUtils.js +0 -4
  78. package/dist/features/filters/index.d.ts +1 -1
  79. package/dist/features/filters/index.js +1 -1
  80. package/dist/features/forms/Checkbox.js +4 -3
  81. package/dist/features/forms/Input.js +5 -4
  82. package/dist/features/forms/Radio.js +6 -5
  83. package/dist/features/forms/Select.js +3 -2
  84. package/dist/features/forms/Slider.js +3 -2
  85. package/dist/features/forms/Textarea.js +3 -2
  86. package/dist/features/forms/Toggle.js +3 -3
  87. package/dist/features/forms/index.d.ts +1 -1
  88. package/dist/features/forms/index.js +1 -1
  89. package/dist/features/forms/utils.d.ts +0 -2
  90. package/dist/features/forms/utils.js +0 -4
  91. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
  92. package/dist/features/grouped/schemas/index.d.ts +2 -2
  93. package/dist/features/history/utils/guest-history.js +1 -5
  94. package/dist/features/homepage/components/CustomCardsSection.js +8 -2
  95. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
  96. package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
  97. package/dist/features/layout/AppLayoutShell.d.ts +23 -1
  98. package/dist/features/layout/AppLayoutShell.js +13 -12
  99. package/dist/features/layout/BottomNavItem.d.ts +9 -1
  100. package/dist/features/layout/BottomNavItem.js +18 -4
  101. package/dist/features/layout/index.d.ts +1 -1
  102. package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
  103. package/dist/features/media/MediaLightbox.js +3 -5
  104. package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
  105. package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
  106. package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
  107. package/dist/features/orders/components/RefundRequestView.js +1 -1
  108. package/dist/features/parts/schemas/firestore.d.ts +274 -0
  109. package/dist/features/parts/schemas/firestore.js +19 -0
  110. package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
  111. package/dist/features/pre-orders/api/admin/route.js +1 -5
  112. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
  113. package/dist/features/products/api/route.js +1 -3
  114. package/dist/features/products/components/CustomSectionTabContent.js +0 -5
  115. package/dist/features/products/components/GroupMemberPicker.js +2 -2
  116. package/dist/features/products/components/GroupSettingsPanel.js +1 -1
  117. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
  118. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
  119. package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
  120. package/dist/features/products/components/ProductDetailPageView.js +1 -7
  121. package/dist/features/products/repository/products.repository.js +0 -1
  122. package/dist/features/promotions/api/route.js +1 -8
  123. package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
  124. package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
  125. package/dist/features/seller/api/coupons/route.js +1 -5
  126. package/dist/features/seller/api/products/route.js +1 -5
  127. package/dist/features/seller/components/SellerProductShell.js +1 -2
  128. package/dist/features/seller/hooks/useSellerListingData.js +0 -30
  129. package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
  130. package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
  131. package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
  132. package/dist/features/stores/components/StoreGuideHubView.js +1 -1
  133. package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
  134. package/dist/features/stores/components/StoresIndexPageView.js +6 -9
  135. package/dist/features/stores/schemas/firestore.d.ts +27 -0
  136. package/dist/features/stores/schemas/firestore.js +2 -0
  137. package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
  138. package/dist/index.d.ts +4 -1
  139. package/dist/index.js +28 -1
  140. package/dist/providers/db-firebase/admin-app-lite.js +14 -2
  141. package/dist/providers/db-firebase/admin.js +8 -2
  142. package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
  143. package/dist/react/hooks/useBrowsePageFlags.js +18 -0
  144. package/dist/schemas/registry.d.ts +4 -4
  145. package/dist/security/csp.d.ts +32 -2
  146. package/dist/security/csp.js +32 -2
  147. package/dist/styles.css +1 -1
  148. package/dist/tailwind-utilities.css +1 -1
  149. package/dist/ui/components/Button.js +1 -1
  150. package/dist/ui/components/Chart.js +0 -1
  151. package/dist/ui/rich-text/RichText.js +1 -3
  152. package/dist/ui/rich-text/RichTextRenderer.js +1 -3
  153. package/dist/utils/browser-storage.d.ts +14 -0
  154. package/dist/utils/browser-storage.js +18 -0
  155. package/dist/utils/classnames.d.ts +19 -0
  156. package/dist/utils/classnames.js +21 -0
  157. package/dist/utils/search-params.d.ts +44 -0
  158. package/dist/utils/search-params.js +46 -0
  159. package/dist/utils/sieve-builder.d.ts +13 -0
  160. package/dist/utils/sieve-builder.js +22 -0
  161. package/dist/utils/string.formatter.d.ts +12 -0
  162. package/dist/utils/string.formatter.js +17 -0
  163. package/package.json +2 -2
@@ -18,6 +18,78 @@
18
18
  * Future-expansion Pattern 5. Slots with Phase 7 (CTA registry consults the
19
19
  * same flag to hide actions for disabled types).
20
20
  */
21
+ // Route-map only — a pure constants module with no firebase-admin in its
22
+ // import chain, so this stays safe in the client bundle (appkit Export Rules).
23
+ import { ROUTES } from "../../../next/routing/route-map.js";
24
+ /**
25
+ * Declared as `Record<BrowsePageKey, true>` for the same reason
26
+ * `ALL_LISTING_TYPES_MAP` is: adding a key to the union without adding it here
27
+ * is a COMPILE error rather than a section that silently can never be hidden.
28
+ * Root Cause #61 — every hand-written enumeration of a union in this codebase
29
+ * has drifted at least once.
30
+ */
31
+ const ALL_BROWSE_PAGES_MAP = {
32
+ categories: true,
33
+ brands: true,
34
+ franchise: true,
35
+ stores: true,
36
+ };
37
+ export const ALL_BROWSE_PAGES = Object.keys(ALL_BROWSE_PAGES_MAP);
38
+ /**
39
+ * `true` when the section is missing from the flag map OR explicitly enabled.
40
+ *
41
+ * Absent-means-enabled, matching its two siblings: a section has to be
42
+ * EXPLICITLY `false` to close. An install that predates this setting keeps
43
+ * every section open, so adding the field cannot take a site's navigation away
44
+ * from it.
45
+ */
46
+ export function isBrowsePageEnabled(page, settings) {
47
+ return settings?.listings?.browsePages?.[page] !== false;
48
+ }
49
+ export function enabledBrowsePages(settings) {
50
+ return ALL_BROWSE_PAGES.filter((p) => isBrowsePageEnabled(p, settings));
51
+ }
52
+ /** Narrow an untrusted string — a route param, a form value — to the union. */
53
+ export function isBrowsePageKey(value) {
54
+ return typeof value === "string" && value in ALL_BROWSE_PAGES_MAP;
55
+ }
56
+ /**
57
+ * The public path prefixes each section owns.
58
+ *
59
+ * 🛑 `/sellers` maps to `stores` — see the `BrowsePageKey` note. Two doors,
60
+ * one directory, one flag.
61
+ */
62
+ const BROWSE_PAGE_PREFIXES = [
63
+ [String(ROUTES.PUBLIC.CATEGORIES), "categories"],
64
+ [String(ROUTES.PUBLIC.BRANDS), "brands"],
65
+ [String(ROUTES.PUBLIC.FRANCHISE), "franchise"],
66
+ [String(ROUTES.PUBLIC.STORES), "stores"],
67
+ [String(ROUTES.PUBLIC.SELLERS), "stores"],
68
+ ];
69
+ /**
70
+ * Which browse section a link points into, or `null` for a link that is not in
71
+ * one. Used to drop nav, sidebar, mobile-sheet and footer entries for a closed
72
+ * section.
73
+ *
74
+ * 🛑 Matches on an EXACT path or a `/`-delimited descendant, never a bare
75
+ * `startsWith`. `/store` is the SELLER DASHBOARD and `/stores` is the public
76
+ * directory, so a prefix test would hide a signed-in seller's own dashboard
77
+ * link the moment an admin closed the public storefronts — a different
78
+ * surface, a different audience, one character apart.
79
+ *
80
+ * Query strings and hashes are tolerated because footer and nav hrefs are
81
+ * hand-authored and some carry them.
82
+ */
83
+ export function browsePageForPath(href) {
84
+ if (typeof href !== "string" || href.length === 0)
85
+ return null;
86
+ const path = href.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
87
+ for (const [prefix, key] of BROWSE_PAGE_PREFIXES) {
88
+ if (path === prefix || path.startsWith(`${prefix}/`))
89
+ return key;
90
+ }
91
+ return null;
92
+ }
21
93
  /**
22
94
  * `true` when the type is missing from the flag map OR explicitly enabled.
23
95
  * Defaulting to enabled keeps legacy data working — a flag has to be
package/dist/client.d.ts CHANGED
@@ -223,7 +223,7 @@ export { AppLayoutShell, LocaleSwitcher, useDashboardNav, BottomActionsProvider,
223
223
  * visible Buy/Checkout button. See `useCtaReveal`.
224
224
  */
225
225
  export { ctaAnchorProps, CTA_ANCHOR_ATTR } from "./features/layout/index";
226
- export type { AppLayoutShellProps, AppLayoutShellSidebarLink, AppLayoutShellSidebarSection, DashboardNavState, MainNavbarItem, LayoutClientProps } from "./features/layout/index";
226
+ export type { AppLayoutShellProps, AppLayoutShellSidebarLink, AppLayoutShellSidebarSection, SidebarItemActionRenderer, DashboardNavState, MainNavbarItem, LayoutClientProps } from "./features/layout/index";
227
227
  export { ListingLayout } from "./ui/components/ListingLayout";
228
228
  export type { ListingLayoutProps, ListingLayoutLabels } from "./ui/components/ListingLayout";
229
229
  export { Search } from "./features/search/components";
@@ -364,6 +364,8 @@ export { useAuthGate } from "./react/hooks/useAuthGate";
364
364
  export type { UseAuthGateReturn } from "./react/hooks/useAuthGate";
365
365
  export { useListingTypeFlags } from "./react/hooks/useListingTypeFlags";
366
366
  export type { ListingTypeFlags, ListingTypeFlagsShape, } from "./react/hooks/useListingTypeFlags";
367
+ export { useBrowsePageFlags } from "./react/hooks/useBrowsePageFlags";
368
+ export type { BrowsePageFlags, BrowsePageFlagsShape, } from "./react/hooks/useBrowsePageFlags";
367
369
  export { useActionDispatch } from "./react/hooks/use-action-dispatch";
368
370
  export type { DispatchAction, UseActionDispatchOptions } from "./react/hooks/use-action-dispatch";
369
371
  export { usePanelStore } from "./stores/panel-store";
@@ -437,7 +439,8 @@ export type { TrackArgs as TrackHistoryArgs } from "./features/history/hooks/use
437
439
  export { WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, } from "./constants/limits";
438
440
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/utils/listing-type";
439
441
  export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
440
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, } from "./_internal/shared/listing-types/feature-flags";
442
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags";
443
+ export type { BrowsePageKey } from "./_internal/shared/listing-types/feature-flags";
441
444
  export { ADVANCED_LISTING_TYPE_CAPABILITY, ADVANCED_LISTING_TYPES, BASIC_LISTING_TYPES, LISTING_TYPE_TIER, hasAdvancedListingTypes, isAdvancedListingType, isListingTypeAllowedFor, listingTypesAllowedFor, } from "./_internal/shared/listing-types/tiers";
442
445
  export type { ListingTypeAccessContext, ListingTypeTier, } from "./_internal/shared/listing-types/tiers";
443
446
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
package/dist/client.js CHANGED
@@ -403,6 +403,9 @@ export { useAuthGate } from "./react/hooks/useAuthGate.js";
403
403
  // [CLIENT-ONLY] W1-43 — useListingTypeFlags: read featureFlags.listingTypes
404
404
  // from siteSettings; hide nav/search/picker entries for disabled types.
405
405
  export { useListingTypeFlags } from "./react/hooks/useListingTypeFlags.js";
406
+ // [CLIENT-ONLY] useBrowsePageFlags: read listings.browsePages from
407
+ // siteSettings; drop nav/sidebar/footer links into a closed browse section.
408
+ export { useBrowsePageFlags } from "./react/hooks/useBrowsePageFlags.js";
406
409
  // Action dispatch hook + panel store
407
410
  export { useActionDispatch } from "./react/hooks/use-action-dispatch.js";
408
411
  export { usePanelStore } from "./stores/panel-store.js";
@@ -470,7 +473,11 @@ export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardLi
470
473
  // `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
471
474
  export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale.js";
472
475
  // SB-UNI-X4 2026-05-13 — per-type feature-flag helpers (client-safe).
473
- export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType, } from "./_internal/shared/listing-types/feature-flags.js";
476
+ export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, isListingType,
477
+ // Browse-section gates. Client-safe and genuinely needed here: the nav,
478
+ // sidebar, mobile sheet and footer all filter on them, and a link into a
479
+ // closed section is a 404 on every page of the site.
480
+ isBrowsePageEnabled, enabledBrowsePages, ALL_BROWSE_PAGES, isBrowsePageKey, } from "./_internal/shared/listing-types/feature-flags.js";
474
481
  // Basic/advanced listing-type split. Client-safe: the nav filter and any
475
482
  // seller-facing picker need the same rule the server enforces, and deriving it
476
483
  // twice is how the ten hand-written copies of the union happened.
@@ -603,6 +603,7 @@ export declare const CATEGORY_FIELDS: {
603
603
  readonly SUBLISTING: "sublisting";
604
604
  readonly BUNDLE: "bundle";
605
605
  readonly FRANCHISE: "franchise";
606
+ readonly CHARACTER: "character";
606
607
  };
607
608
  readonly BUNDLE_STOCK_STATUS_VALUES: {
608
609
  readonly AVAILABLE: "available";
@@ -715,6 +715,20 @@ export const CATEGORY_FIELDS = {
715
715
  * A brand is who MADE it (Takara-Tomy); a franchise is what it IS FROM.
716
716
  */
717
717
  FRANCHISE: "franchise",
718
+ /*
719
+ * WHO USED the bey in the story — Tyson Granger, Kai Hiwatari. The third
720
+ * sibling of BRAND and FRANCHISE and tier-0 for the same reason: a row the
721
+ * product points AT, never one it lives under. Who made it / what it is
722
+ * from / who used it.
723
+ *
724
+ * 🛑 Match on the SLUG, never the display name. `brand-rows.ts` trap 1
725
+ * records that brands match on display NAME, so renaming a brand silently
726
+ * orphans its whole catalogue; franchise was added later and deliberately
727
+ * took the stable key instead. Characters follow franchise — and it matters
728
+ * more here, because character names are routinely re-localised between
729
+ * the Japanese and English releases.
730
+ */
731
+ CHARACTER: "character",
718
732
  },
719
733
  BUNDLE_STOCK_STATUS_VALUES: {
720
734
  AVAILABLE: "available",
@@ -11,11 +11,7 @@
11
11
  import { NextResponse } from "next/server.js";
12
12
  import { getProviders } from "../../../../../contracts/index.js";
13
13
  import { normalizeError } from "../../../../../errors/normalize.js";
14
- function numParam(url, key, fallback) {
15
- const v = url.searchParams.get(key);
16
- const n = v !== null ? Number(v) : NaN;
17
- return Number.isFinite(n) ? n : fallback;
18
- }
14
+ import { numParam } from "../../../../../utils/search-params.js";
19
15
  export async function GET(request, { params }) {
20
16
  try {
21
17
  const { userId } = await params;
@@ -12,7 +12,7 @@ import { normalizeError } from "../../../errors/normalize.js";
12
12
  */
13
13
  import { DatabaseError } from "../../../errors/index.js";
14
14
  import { serverLogger } from "../../../monitoring/index.js";
15
- import { BaseRepository, getFirestoreCount, prepareForFirestore, } from "../../../providers/db-firebase/index.js";
15
+ import { BaseRepository, getFirestoreCount, prepareForFirestore } from "../../../providers/db-firebase/index.js";
16
16
  import { maskPii, revealPii, ADDRESS_PII_FIELDS, } from "../../../security/index.js";
17
17
  import { buildTokensIndex } from "../../../security/blind-index-tokens.js";
18
18
  import { ADDRESS_FIELDS, ADDRESSES_COLLECTION, } from "../schemas/index.js";
@@ -12,11 +12,7 @@
12
12
  import { NextResponse } from "next/server.js";
13
13
  import { getProviders } from "../../../../contracts/index.js";
14
14
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
15
- function numParam(url, key, fallback) {
16
- const v = url.searchParams.get(key);
17
- const n = v !== null ? Number(v) : NaN;
18
- return Number.isFinite(n) ? n : fallback;
19
- }
15
+ import { numParam } from "../../../../utils/search-params.js";
20
16
  export const GET = createRouteHandler({
21
17
  roles: ["admin", "moderator"],
22
18
  handler: async ({ request }) => {
@@ -12,11 +12,7 @@
12
12
  import { NextResponse } from "next/server.js";
13
13
  import { getProviders } from "../../../../contracts/index.js";
14
14
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
15
- function numParam(url, key, fallback) {
16
- const v = url.searchParams.get(key);
17
- const n = v !== null ? Number(v) : NaN;
18
- return Number.isFinite(n) ? n : fallback;
19
- }
15
+ import { numParam } from "../../../../utils/search-params.js";
20
16
  export const GET = createRouteHandler({
21
17
  roles: ["admin", "moderator"],
22
18
  handler: async ({ request }) => {
@@ -12,11 +12,7 @@
12
12
  import { NextResponse } from "next/server.js";
13
13
  import { getProviders } from "../../../../contracts/index.js";
14
14
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
15
- function numParam(url, key, fallback) {
16
- const v = url.searchParams.get(key);
17
- const n = v !== null ? Number(v) : NaN;
18
- return Number.isFinite(n) ? n : fallback;
19
- }
15
+ import { numParam } from "../../../../utils/search-params.js";
20
16
  export const GET = createRouteHandler({
21
17
  roles: ["admin", "moderator"],
22
18
  handler: async ({ request }) => {
@@ -12,11 +12,7 @@
12
12
  import { NextResponse } from "next/server.js";
13
13
  import { getProviders } from "../../../../contracts/index.js";
14
14
  import { createRouteHandler } from "../../../../next/api/routeHandler.js";
15
- function numParam(url, key, fallback) {
16
- const v = url.searchParams.get(key);
17
- const n = v !== null ? Number(v) : NaN;
18
- return Number.isFinite(n) ? n : fallback;
19
- }
15
+ import { numParam } from "../../../../utils/search-params.js";
20
16
  export const GET = createRouteHandler({
21
17
  roles: ["admin", "moderator"],
22
18
  handler: async ({ request }) => {
@@ -17,7 +17,6 @@ const __P = {
17
17
  };
18
18
  const CLS_PANEL = "rounded-xl border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] p-[var(--appkit-space-4)] space-y-3";
19
19
  const CLS_ROW_BETWEEN = "flex items-center justify-between";
20
- const CLS_SECTION_HEADING = "text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)]";
21
20
  const carouselSlideSchema = z.object({
22
21
  title: z.string().min(1, "Slide title is required").max(150),
23
22
  order: z.number().int().min(0),
@@ -4,7 +4,7 @@ import { Row } from "@mohasinac/appkit/ui";
4
4
  import { useApiMutation } from "@mohasinac/appkit/client";
5
5
  import React from "react";
6
6
  import { useQuery } from "@tanstack/react-query";
7
- import { Checkbox, ConfirmDeleteModal, Div, Label, StackedViewShell, Text, useToast, } from "../../../ui/index.js";
7
+ import { Checkbox, ConfirmDeleteModal, Div, Label, StackedViewShell, Text, useToast } from "../../../ui/index.js";
8
8
  import { apiClient } from "../../../http/index.js";
9
9
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
10
10
  import { FormErrorSummary, applyZodIssues } from "../../../ui/forms/index.js";
@@ -156,7 +156,6 @@ export function AdminFeatureEditorView({ featureId, fixedScope, fixedStoreId, em
156
156
  * the productTypes minimum). The button now stays enabled and the schema
157
157
  * reports what is actually wrong, on the field itself.
158
158
  */
159
- const isDisabled = isSubmitting;
160
159
  const sections = React.useMemo(() => buildSectionsFromSchema(productFeatureFormSchema, {
161
160
  options: {
162
161
  iconColor: [...PRODUCT_FEATURE_ICON_COLOR_OPTIONS],
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Users, Package, Store, ShoppingBag, FileText, Settings, Shield, BarChart2, AlertTriangle, BookOpen, CreditCard, } from "lucide-react";
2
+ import { Users, Package, Store, ShoppingBag, FileText, Settings, Shield, BarChart2, AlertTriangle, BookOpen, CreditCard } from "lucide-react";
3
3
  import { Alert, Div, Grid, Heading, Row, Section, Stack, Text, TextLink } from "../../../ui/index.js";
4
4
  import { ROUTES } from "../../../next/routing/route-map.js";
5
5
  const CLS_WARN_PANEL = "rounded-2xl border border-amber-200 bg-warning-surface dark:border-amber-800 p-[var(--appkit-space-6)]";
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // audit-form-mutation-hook-multi-ok: saveMutation is the single combined save
4
+ // across all tabs; deleteMutation is a separate destructive action, not a
5
+ // second per-tab save.
3
6
  import { useApiMutation } from "@mohasinac/appkit/client";
4
7
  import React from "react";
5
8
  import { z } from "zod";
6
9
  import { useQuery } from "@tanstack/react-query";
7
- import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar, } from "../../../ui/index.js";
10
+ import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar } from "../../../ui/index.js";
8
11
  import { useFormShellState, applyZodIssues, FormErrorSummary, FormShellContext } from "../../../ui/forms/index.js";
9
12
  import { SectionForm, useSectionFormNav } from "../../shell/index.js";
10
13
  import { PRODUCT_MAX_IMAGES } from "../../../_internal/shared/media/limits.js";
@@ -18,7 +18,7 @@ import { apiClient } from "../../../http/index.js";
18
18
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
19
19
  import { ROUTES } from "../../../next/routing/route-map.js";
20
20
  import { NOTIFICATION_TYPE_TABS } from "../../../constants/notification-types.js";
21
- import { ALL_LISTING_TYPES } from "../../../_internal/shared/listing-types/feature-flags.js";
21
+ import { ALL_LISTING_TYPES, ALL_BROWSE_PAGES, } from "../../../_internal/shared/listing-types/feature-flags.js";
22
22
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
23
23
  import { SITE_SETTINGS_TABS, isSiteSettingsTabId, } from "../constants/site-settings-tabs.js";
24
24
  import { ThemeManagerView, } from "../../site-settings/components/ThemeManagerView.js";
@@ -316,6 +316,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
316
316
  // Per-listing-type / per-category-type visibility flags.
317
317
  const [listingTypeFlags, setListingTypeFlags] = React.useState({});
318
318
  const [categoryTypeFlags, setCategoryTypeFlags] = React.useState({});
319
+ const [browsePageFlags, setBrowsePageFlags] = React.useState({});
319
320
  // Per-channel notification-type allowlists — empty array means "all types".
320
321
  const [notifEmailTypes, setNotifEmailTypes] = React.useState([]);
321
322
  const [notifWhatsappTypes, setNotifWhatsappTypes] = React.useState([]);
@@ -557,6 +558,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
557
558
  setCodMaxOrderValue(s.payment?.codMaxOrderValue ?? COD_MAX_ORDER_VALUE_FALLBACK);
558
559
  setListingTypeFlags(s.listings?.listingTypes ?? {});
559
560
  setCategoryTypeFlags(s.listings?.categoryTypes ?? {});
561
+ setBrowsePageFlags(s.listings?.browsePages ?? {});
560
562
  setNotifEmailTypes(s.notificationChannels?.email?.types ?? []);
561
563
  setNotifWhatsappTypes(s.notificationChannels?.whatsapp?.types ?? []);
562
564
  setBidIncrementTiers(s.auctionConfig?.bidIncrementTiers ?? DEFAULT_AUCTION_BID_INCREMENT_TIERS);
@@ -798,7 +800,11 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
798
800
  // wholesale, so omitting one here would wipe it. The load-time spread this
799
801
  // used to carry existed to preserve `adminCheckoutBypass`, which is now a
800
802
  // permission rather than a stored flag.
801
- listings: { listingTypes: listingTypeFlags, categoryTypes: categoryTypeFlags },
803
+ listings: {
804
+ listingTypes: listingTypeFlags,
805
+ categoryTypes: categoryTypeFlags,
806
+ browsePages: browsePageFlags,
807
+ },
802
808
  notificationChannels: {
803
809
  inApp: { enabled: true, readOnly: true },
804
810
  email: { enabled: notifEmailEnabled, minPriority: notifEmailMinPriority, types: notifEmailTypes },
@@ -866,6 +872,16 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
866
872
  */
867
873
  const LISTING_TYPE_KEYS = ALL_LISTING_TYPES;
868
874
  const CATEGORY_TYPE_KEYS = ["category", "sublisting", "brand", "bundle"];
875
+ /* Derived for the same reason LISTING_TYPE_KEYS is — a hand-written copy of
876
+ * a union in this file has already drifted once, in both directions. */
877
+ const BROWSE_PAGE_KEYS = ALL_BROWSE_PAGES;
878
+ /** What each section actually withdraws, in the admin's own vocabulary. */
879
+ const BROWSE_PAGE_META = {
880
+ categories: { label: "Categories", hint: "/categories and every category page" },
881
+ brands: { label: "Brands", hint: "/brands and every brand page" },
882
+ franchise: { label: "Franchises", hint: "/franchise and every franchise page" },
883
+ stores: { label: "Stores & Sellers", hint: "/stores, /sellers and every storefront" },
884
+ };
869
885
  const NOTIF_TYPE_OPTIONS = NOTIFICATION_TYPE_TABS.filter((t) => t.id !== "All").map((t) => ({ value: t.id, label: t.label }));
870
886
  /*
871
887
  * Validate the payload that is actually sent, then save.
@@ -915,7 +931,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
915
931
  // The plugin's own display label, not the raw key — the
916
932
  // literal version rendered "pre-order" and
917
933
  // "digital-code" at an admin.
918
- label: pluginFor(key).typeLabel, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Razorpay" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Razorpay Key ID", value: razorpayKeyId, onChange: setRazorpayKeyId, placeholder: "rzp_test_\u2026 / rzp_live_\u2026" }), _jsx(MaskedInput, { label: "Razorpay Key Secret", value: razorpayKeySecret, onChange: setRazorpayKeySecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Razorpay Webhook Secret", value: razorpayWebhookSecret, onChange: setRazorpayWebhookSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" }), _jsx(Input, { label: "Microsoft Clarity Project ID", value: clarityProjectId, onChange: (e) => setClarityProjectId(e.target.value), placeholder: "abcdefghij", helperText: "Heatmaps and session replay. Recording is strictly masked \u2014 no text or input values are captured. Leave blank to keep Clarity off." })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-razorpay-enabled", children: _jsx(Toggle, { label: "Razorpay (online card/UPI/netbanking) enabled \u2014 needs Key ID + Key Secret under Credentials", checked: razorpayEnabled, onChange: setRazorpayEnabled }) }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 ignored while Razorpay is on; only one online gateway runs at a time", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD maximum order value (\u20B9)",
934
+ label: pluginFor(key).typeLabel, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Browse sections" }), _jsx(Text, { size: "xs", color: "muted", children: "A closed section returns Not Found and disappears from the navbar, the sidebar, the mobile menu, the footer and the sitemap. Products, categories and stores are untouched \u2014 only the public way in is withdrawn, and re-opening restores it." }), _jsx(Grid, { cols: 2, gap: "sm", children: BROWSE_PAGE_KEYS.map((key) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Toggle, { label: BROWSE_PAGE_META[key]?.label ?? key, checked: browsePageFlags[key] ?? true, onChange: (v) => setBrowsePageFlags((prev) => ({ ...prev, [key]: v })) }), _jsx(Text, { size: "xs", color: "muted", children: BROWSE_PAGE_META[key]?.hint })] }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Razorpay" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Razorpay Key ID", value: razorpayKeyId, onChange: setRazorpayKeyId, placeholder: "rzp_test_\u2026 / rzp_live_\u2026" }), _jsx(MaskedInput, { label: "Razorpay Key Secret", value: razorpayKeySecret, onChange: setRazorpayKeySecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Razorpay Webhook Secret", value: razorpayWebhookSecret, onChange: setRazorpayWebhookSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" }), _jsx(Input, { label: "Microsoft Clarity Project ID", value: clarityProjectId, onChange: (e) => setClarityProjectId(e.target.value), placeholder: "abcdefghij", helperText: "Heatmaps and session replay. Recording is strictly masked \u2014 no text or input values are captured. Leave blank to keep Clarity off." })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-razorpay-enabled", children: _jsx(Toggle, { label: "Razorpay (online card/UPI/netbanking) enabled \u2014 needs Key ID + Key Secret under Credentials", checked: razorpayEnabled, onChange: setRazorpayEnabled }) }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 ignored while Razorpay is on; only one online gateway runs at a time", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD maximum order value (\u20B9)",
919
935
  /*
920
936
  🛑 A MAXIMUM, sitting directly above a THRESHOLD that is a
921
937
  minimum. The helper text says which way each one runs,
@@ -23,6 +23,12 @@ export interface OwnerQuickActionsProps {
23
23
  * `card` sits in a hero corner and uses a scrim so it reads on photography;
24
24
  * `detail` sits in a header row beside the title.
25
25
  */
26
+ /**
27
+ * 🛑 ACCEPTED AND NOT READ since `onMedia` took over the dark-surface
28
+ * decision — see the note at the `onMedia` branch below. 36 call sites still
29
+ * pass it, so it is declared rather than deleted; delete it WITH those call
30
+ * sites, not ahead of them.
31
+ */
26
32
  placement?: "card" | "detail";
27
33
  /**
28
34
  * 🛑 Set this whenever `placement="detail"` sits on a DARK cover hero.
@@ -43,4 +49,4 @@ export interface OwnerQuickActionsProps {
43
49
  onMedia?: boolean;
44
50
  className?: string;
45
51
  }
46
- export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement, onMedia, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
52
+ export declare function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, onMedia, className, }: OwnerQuickActionsProps): import("react").JSX.Element | null;
@@ -108,7 +108,7 @@ const RESOURCE_LEAVES = {
108
108
  USER: { bucket: "ADMIN", edit: "manage-user" },
109
109
  SECTION: { bucket: "ADMIN", edit: "edit-section" },
110
110
  };
111
- export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, placement = "card", onMedia = false, className = "", }) {
111
+ export function OwnerQuickActions({ resource, editHref, onDelete, onClone, ownerId, storeId, recordLabel, onMedia = false, className = "", }) {
112
112
  const session = useOptionalSession();
113
113
  const isOwnerOrAdmin = useIsOwner(ownerId);
114
114
  const isAdmin = useIsAdmin();
@@ -30,7 +30,6 @@ import { annotate } from "../../shell/field-ui-meta.js";
30
30
  import { mediaUrlSchema } from "../../../validation/schemas.js";
31
31
  import { StoreStatusValues } from "../../stores/schemas/firestore.js";
32
32
  import { ALL_STORE_CAPABILITIES } from "../../auth/permissions/constants.js";
33
- const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
34
33
  /*
35
34
  * `adminAddressFormSchema` is DELETED (W5 / D19).
36
35
  *
@@ -670,6 +670,39 @@ export interface SiteSettingsDocument extends BaseDocument {
670
670
  search?: {
671
671
  indexEnabled?: boolean;
672
672
  };
673
+ /**
674
+ * The WhatsApp catalogue feed — `/whatsapp/{storeSlug}/feed.csv`, read on a
675
+ * schedule by each seller's own Meta Commerce Manager.
676
+ *
677
+ * ## 🛑 A group of its own, for the same reason `search` is
678
+ *
679
+ * It is NOT in `notificationChannels.whatsapp`, which governs the platform's
680
+ * outbound order-update templates — a different channel with a different cost
681
+ * (per-template spend vs. a public CSV), owned by the platform rather than
682
+ * pointed at by each seller. Nor is it in `payment` or any other group that
683
+ * `AdminSiteSettingsView.buildFullPayload` rewrites WHOLE from form state: a
684
+ * fifth key in such a group is silently erased the next time an admin saves
685
+ * anything on that page (Root Cause #76), and a kill switch that a routine
686
+ * save turns back on fails in the direction of the thing it exists to stop.
687
+ *
688
+ * ## 🛑 Default ON — reader tests `!== false`
689
+ *
690
+ * An absent field, an unseeded document and a failed settings read must all
691
+ * mean "feeds work". Meta EMPTIES a connected catalogue the first time a
692
+ * scheduled fetch returns a file with no rows, so a `=== true` reader would
693
+ * wipe every seller's WhatsApp catalogue on the first Firestore blip — the
694
+ * same fail-open argument `allPurchasableTypes()` in the feed route is
695
+ * written around. The only state that disables it is somebody deliberately
696
+ * writing `false`.
697
+ *
698
+ * Switching it off empties EVERY store's feed on its next fetch, which is a
699
+ * platform-wide retraction and the reason it has no admin-UI toggle today —
700
+ * like `search.indexEnabled`, it is a deliberate Firestore-level switch. The
701
+ * per-store equivalent a seller owns is `StoreDocument.whatsappFeedEnabled`.
702
+ */
703
+ whatsappFeed?: {
704
+ enabled?: boolean;
705
+ };
673
706
  emailSettings: {
674
707
  fromName: string;
675
708
  fromEmail: string;
@@ -728,6 +761,25 @@ export interface SiteSettingsDocument extends BaseDocument {
728
761
  brand?: boolean;
729
762
  bundle?: boolean;
730
763
  };
764
+ /**
765
+ * Which public BROWSE SECTIONS are open — `/categories`, `/brands`,
766
+ * `/franchise`, `/stores` (+ `/sellers`).
767
+ *
768
+ * A third axis, not a rename of the two above: `listingTypes` says what the
769
+ * marketplace sells, `categoryTypes` says what a taxonomy row may be, and
770
+ * this says whether a public entry point is open. Closing `/brands` does
771
+ * not untag a single product — `BrandDetailPageView`'s query is unchanged
772
+ * and brand rows keep driving it. It withdraws the front door.
773
+ *
774
+ * ⚠️ Same absent-means-enabled contract as its siblings, and the same
775
+ * warning applies: `BrowsePageKey` and `ALL_BROWSE_PAGES_MAP`
776
+ * (`_internal/shared/listing-types/feature-flags.ts`) are the authority.
777
+ * That Record makes a missing member a compile error; this interface does
778
+ * not, and the `listingTypes` field above is the record of what happens
779
+ * when the two drift (it listed 8 types after the union reached 10).
780
+ * Add a new section THERE, not here.
781
+ */
782
+ browsePages?: Partial<Record<string, boolean>>;
731
783
  };
732
784
  /**
733
785
  * Admin-authored HTML overrides for the public policy pages. When a key holds
@@ -371,6 +371,26 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
371
371
  brand: true,
372
372
  bundle: false,
373
373
  },
374
+ /*
375
+ * Every browse section CLOSED, as of 2026-10-24.
376
+ *
377
+ * Spelled out rather than left absent even though absent means open: this
378
+ * block is the "never configured" baseline, and the whole point of the
379
+ * setting is that these four are not ready to show. Relying on the
380
+ * permissive default would open all four on a fresh install, which is the
381
+ * opposite of the decision.
382
+ *
383
+ * Re-opening one is a single toggle in Site Settings -> Listings -> Browse
384
+ * sections. Nothing else has to change: the routes, the nav entries, the
385
+ * footer links and the sitemap rows all come back on their own, because
386
+ * all four read this same flag rather than having been deleted.
387
+ */
388
+ browsePages: {
389
+ categories: false,
390
+ brands: false,
391
+ franchise: false,
392
+ stores: false,
393
+ },
374
394
  },
375
395
  shipping: {
376
396
  estimatedDays: 5,
@@ -13,14 +13,7 @@
13
13
  import { NextResponse } from "next/server.js";
14
14
  import { getProviders } from "../../../contracts/index.js";
15
15
  import { normalizeError } from "../../../errors/normalize.js";
16
- function param(url, key) {
17
- return url.searchParams.get(key);
18
- }
19
- function numParam(url, key, fallback) {
20
- const v = url.searchParams.get(key);
21
- const n = v !== null ? Number(v) : NaN;
22
- return Number.isFinite(n) ? n : fallback;
23
- }
16
+ import { numParam, param } from "../../../utils/search-params.js";
24
17
  // --- GET /api/bids ------------------------------------------------------------
25
18
  // Query params:
26
19
  // auctionId — filter bids for a specific auction product (recommended)
@@ -13,7 +13,7 @@ const CLS_LIVE_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-war
13
13
  import { ROUTES } from "../../../next/routing/route-map.js";
14
14
  import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
15
15
  import { formatCurrency } from "../../../utils/number.formatter.js";
16
- import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
16
+ import { toDescriptionHtml } from "../../../utils/string.formatter.js";
17
17
  import { safeDisplayName } from "../../../security/index.js";
18
18
  import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal.js";
19
19
  import { isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config.js";
@@ -44,12 +44,6 @@ import { SublistingCarouselSection } from "../../products/components/SublistingC
44
44
  import { isFinalSale } from "../../products/constants/final-sale.js";
45
45
  import { OwnerQuickActions } from "../../admin/components/OwnerQuickActions.js";
46
46
  import { PurchaseReviewAffordance } from "../../reviews/components/PurchaseReviewAffordance.js";
47
- function toDescriptionHtml(raw) {
48
- if (!raw)
49
- return "";
50
- const s = typeof raw === "string" ? raw : JSON.stringify(raw);
51
- return normalizeRichTextHtml(s);
52
- }
53
47
  function renderAuctionInfoPanel(props) {
54
48
  const { productId, storeId, title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, buyNowAvailable, featured, freeShipping, condition, finalSale, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref } = props;
55
49
  return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Span, { size: "xs", weight: "semibold", className: CLS_LIVE_BADGE, children: "\uD83C\uDFF7\uFE0F Live Auction" }), _jsx(AuctionLiveStatusBadge, { auctionEndDate: endDate, initialIsEnded: isEnded })] }), _jsx(Heading, { level: 1, className: "leading-snug", smSize: "2xl", color: "primary", size: "xl", weight: "bold", children: title }), _jsx(OwnerQuickActions, { resource: "PRODUCT", placement: "detail", recordLabel: title, storeId: storeId, editHref: String(ROUTES.STORE.PRODUCTS_EDIT(productId)) })] }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-0.5", color: "muted", size: "xs", children: "Current bid" }), _jsx(LiveBidPrice, { productId: productId, currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "2xl", badgeSize: "sm" }), endDate && !isEnded && (_jsxs(Text, { className: "mt-1.5", color: "muted", size: "sm", children: ["Ends in ", _jsx(Span, { weight: "medium", color: "muted", children: _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" }) })] })), endDate && _jsxs(Text, { className: "mt-0.5", color: "faint", size: "xs", children: [isEnded ? "Ended" : "Ends", " ", endDate.toLocaleString()] })] }), buyNowAvailable && buyNowPrice !== null && (_jsxs(Row, { align: "center", gap: "sm", className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)] bg-primary-50 dark:bg-primary-900/20", padding: "inlineSm", rounded: "lg", children: [_jsx(Span, { size: "xs", color: "muted", children: "Buy Now:" }), _jsx(Span, { size: "base", weight: "bold", className: "text-primary-700 dark:text-primary-300", children: _jsx(GatedPrice, { label: "Sign in to see the Buy Now price", children: formatCurrency(buyNowPrice, currency) }) }), _jsx(Span, { size: "xs", color: "faint", children: "\u2014 in \u201CPlace a bid\u201D" })] })), _jsx(ProductFeatureBadges, { featured: featured, freeShipping: freeShipping, condition: condition ?? undefined, finalSale: finalSale, labels: { featured: "Featured", fasterDelivery: "Faster Delivery", ratedSeller: "Rated Seller", condition: "Condition", conditionNew: "New", conditionUsed: "Used", conditionBroken: "For Parts", conditionRefurbished: "Refurbished", returnable: "Returnable", finalSale: "Final Sale", freeShipping: "Free Shipping", codAvailable: "Cash on Delivery", emiAvailable: "EMI Available", wishlistCount: (n) => `${n} wishlisted`, categoryProductCount: (n, cat) => `${n} in ${cat}` } }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: categoryName || category }), !category && categoryName && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName }), brand && brandSlug && _jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-surface hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:text-primary-400", children: brand }), brand && !brandSlug && _jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand })] })), productFeatures && features.length > 0 && _jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures }), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this item" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => _jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i)) })] })), descriptionHtml && _jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-[length:var(--appkit-text-sm)] leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" }), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Listed by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && _jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" })] }) }))] }));
@@ -7,7 +7,7 @@ import { getClientSessionAdapter } from "../../../contracts/client-session.js";
7
7
  import { apiClient } from "../../../http/index.js";
8
8
  import { useAuthEvent } from "./useAuthEvent.js";
9
9
  import { RealtimeEventStatus } from "../../../react/hooks/useRealtimeEvent.js";
10
- import { AUTH_ENDPOINTS, } from "../../../constants/api-endpoints.js";
10
+ import { AUTH_ENDPOINTS } from "../../../constants/api-endpoints.js";
11
11
  export function useCurrentUser(opts) {
12
12
  const query = useQuery({
13
13
  queryKey: ["auth", "me"],
@@ -11,11 +11,7 @@
11
11
  import { NextResponse } from "next/server.js";
12
12
  import { getProviders } from "../../../contracts/index.js";
13
13
  import { normalizeError } from "../../../errors/normalize.js";
14
- function numParam(url, key, fallback) {
15
- const v = url.searchParams.get(key);
16
- const n = v !== null ? Number(v) : NaN;
17
- return Number.isFinite(n) ? n : fallback;
18
- }
14
+ import { numParam } from "../../../utils/search-params.js";
19
15
  export async function GET(request) {
20
16
  try {
21
17
  const url = new URL(request.url);