@mohasinac/appkit 4.64.1 → 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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.js +18 -1
- package/dist/_internal/server/features/search/index-build/sources.js +27 -12
- package/dist/_internal/server/features/seo/sitemap.js +78 -12
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +16 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
- package/dist/_internal/server/features/stores/tabs.js +20 -21
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
- package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
- package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
- package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +8 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +14 -0
- package/dist/features/account/api/[userId]/addresses/route.js +1 -5
- package/dist/features/addresses/repository/addresses.repository.js +1 -1
- package/dist/features/admin/api/bids/route.js +1 -5
- package/dist/features/admin/api/coupons/route.js +1 -5
- package/dist/features/admin/api/products/route.js +1 -5
- package/dist/features/admin/api/reviews/route.js +1 -5
- package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +4 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
- package/dist/features/admin/components/OwnerQuickActions.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +52 -0
- package/dist/features/admin/schemas/firestore.js +20 -0
- package/dist/features/auctions/api/route.js +1 -8
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
- package/dist/features/auth/hooks/useAuth.js +1 -1
- package/dist/features/before-after/api/route.js +1 -5
- package/dist/features/blog/api/route.js +1 -8
- package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
- package/dist/features/blog/components/BlogIndexPageView.js +6 -9
- package/dist/features/cart/actions/cart-actions.js +1 -1
- package/dist/features/cart/api/route.js +1 -5
- package/dist/features/cart/utils/guest-cart.js +1 -5
- package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
- package/dist/features/categories/api/route.js +5 -23
- package/dist/features/categories/components/BundlesListView.d.ts +2 -3
- package/dist/features/categories/components/BundlesListView.js +2 -5
- package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
- package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
- package/dist/features/categories/constants/category-tabs.d.ts +1 -1
- package/dist/features/categories/constants/category-tabs.js +7 -0
- package/dist/features/categories/hooks/useCategories.d.ts +6 -0
- package/dist/features/categories/hooks/useCategories.js +73 -2
- package/dist/features/categories/types/index.d.ts +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
- package/dist/features/collections/api/route.js +1 -5
- package/dist/features/consultation/api/route.js +1 -5
- package/dist/features/contact/email.js +2 -5
- package/dist/features/corporate/api/route.js +1 -5
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
- package/dist/features/events/components/AdminEventEditorView.js +1 -1
- package/dist/features/events/components/EventsListPageView.d.ts +2 -3
- package/dist/features/events/components/EventsListPageView.js +8 -11
- package/dist/features/faq/constants/help-guides.d.ts +8 -5
- package/dist/features/faq/constants/help-guides.js +9 -5
- package/dist/features/filters/AsyncFacetSection.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +2 -2
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/filters/filterUtils.d.ts +0 -2
- package/dist/features/filters/filterUtils.js +0 -4
- package/dist/features/filters/index.d.ts +1 -1
- package/dist/features/filters/index.js +1 -1
- package/dist/features/forms/Checkbox.js +4 -3
- package/dist/features/forms/Input.js +5 -4
- package/dist/features/forms/Radio.js +6 -5
- package/dist/features/forms/Select.js +3 -2
- package/dist/features/forms/Slider.js +3 -2
- package/dist/features/forms/Textarea.js +3 -2
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/index.d.ts +1 -1
- package/dist/features/forms/index.js +1 -1
- package/dist/features/forms/utils.d.ts +0 -2
- package/dist/features/forms/utils.js +0 -4
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/history/utils/guest-history.js +1 -5
- package/dist/features/homepage/components/CustomCardsSection.js +8 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +23 -1
- package/dist/features/layout/AppLayoutShell.js +13 -12
- package/dist/features/layout/BottomNavItem.d.ts +9 -1
- package/dist/features/layout/BottomNavItem.js +18 -4
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
- package/dist/features/media/MediaLightbox.js +3 -5
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
- package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
- package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
- package/dist/features/orders/components/RefundRequestView.js +1 -1
- package/dist/features/parts/schemas/firestore.d.ts +274 -0
- package/dist/features/parts/schemas/firestore.js +19 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
- package/dist/features/pre-orders/api/admin/route.js +1 -5
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/CustomSectionTabContent.js +0 -5
- package/dist/features/products/components/GroupMemberPicker.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -7
- package/dist/features/products/repository/products.repository.js +0 -1
- package/dist/features/promotions/api/route.js +1 -8
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
- package/dist/features/seller/api/coupons/route.js +1 -5
- package/dist/features/seller/api/products/route.js +1 -5
- package/dist/features/seller/components/SellerProductShell.js +1 -2
- package/dist/features/seller/hooks/useSellerListingData.js +0 -30
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
- package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
- package/dist/features/stores/components/StoresIndexPageView.js +6 -9
- package/dist/features/stores/schemas/firestore.d.ts +27 -0
- package/dist/features/stores/schemas/firestore.js +2 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/providers/db-firebase/admin-app-lite.js +14 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
- package/dist/react/hooks/useBrowsePageFlags.js +18 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/security/csp.d.ts +32 -2
- package/dist/security/csp.js +32 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.js +1 -1
- package/dist/ui/components/Chart.js +0 -1
- package/dist/ui/rich-text/RichText.js +1 -3
- package/dist/ui/rich-text/RichTextRenderer.js +1 -3
- package/dist/utils/browser-storage.d.ts +14 -0
- package/dist/utils/browser-storage.js +18 -0
- package/dist/utils/classnames.d.ts +19 -0
- package/dist/utils/classnames.js +21 -0
- package/dist/utils/search-params.d.ts +44 -0
- package/dist/utils/search-params.js +46 -0
- package/dist/utils/sieve-builder.d.ts +13 -0
- package/dist/utils/sieve-builder.js +22 -0
- package/dist/utils/string.formatter.d.ts +12 -0
- package/dist/utils/string.formatter.js +17 -0
- package/package.json +2 -2
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.65.0",
|
|
4
|
+
"head": "3061d7d6d7b6dc89f57b2fa015b3e2f8243099a1",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": false,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-25T08:53:27.001Z"
|
|
8
8
|
}
|
|
@@ -1145,7 +1145,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1145
1145
|
*/
|
|
1146
1146
|
const pointsOptedIn = useAllPoints === true;
|
|
1147
1147
|
const pointsAccepted = pointsOptedIn && acceptsPoints;
|
|
1148
|
-
const { pointsApplied
|
|
1148
|
+
const { pointsApplied } = resolvePointsTender({
|
|
1149
1149
|
orderTotal,
|
|
1150
1150
|
balance: pointsAccepted ? pointsBudget.remaining : 0,
|
|
1151
1151
|
accepted: pointsAccepted,
|
|
@@ -3496,6 +3496,23 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
3496
3496
|
phonepeOrderId: gatewayOrder.gatewayOrderId,
|
|
3497
3497
|
transactionId: gatewayOrder.transactionId,
|
|
3498
3498
|
shippingAddress,
|
|
3499
|
+
/*
|
|
3500
|
+
* 🛑 Was MISSING until 2026-10-24, and nothing reported it. The snapshot was
|
|
3501
|
+
* computed ~490 lines above with a comment explaining why it must be revealed
|
|
3502
|
+
* first — and then never passed, so every order settled through the gateway
|
|
3503
|
+
* path lost its immutable delivery copy while the COD/UPI path (which calls
|
|
3504
|
+
* `createOrderForGroup` and does pass it) kept one.
|
|
3505
|
+
*
|
|
3506
|
+
* Two consequences, both silent: the invoice address was not frozen at
|
|
3507
|
+
* purchase time (Part 14's whole point), and `placeOfSupply` fell through to
|
|
3508
|
+
* `buyerState` — so the CGST/SGST-vs-IGST split was decided by the buyer's
|
|
3509
|
+
* profile state rather than the address they actually shipped to.
|
|
3510
|
+
*
|
|
3511
|
+
* Root Cause #39's shape exactly: two create paths, one applies a transform
|
|
3512
|
+
* the other does not. Found as an unused-variable warning, which is the only
|
|
3513
|
+
* signal a dropped value ever gives.
|
|
3514
|
+
*/
|
|
3515
|
+
deliverySnapshot,
|
|
3499
3516
|
// For the CGST/SGST vs IGST split. The TOTAL does not depend on it
|
|
3500
3517
|
// (see createPhonePeGroupOrder), which is what let the pre-payment
|
|
3501
3518
|
// amount be computed before this address was known.
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* @tag sideEffects:none
|
|
40
40
|
*/
|
|
41
41
|
import { pluginFor } from "../../../../shared/listing-types/_registry.js";
|
|
42
|
+
import { ROUTES } from "../../../../../next/routing/route-map.js";
|
|
42
43
|
// Defined here, not in `_registry` — imported from the DEFINING module
|
|
43
44
|
// (Root Cause #18).
|
|
44
45
|
import { ALL_LISTING_TYPES } from "../../../../shared/listing-types/feature-flags.js";
|
|
@@ -82,23 +83,37 @@ export const INDEX_TYPES = [
|
|
|
82
83
|
weight: t === "standard" ? 1.0 : 0.98,
|
|
83
84
|
};
|
|
84
85
|
}),
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
/*
|
|
87
|
+
* 🛑 Built from ROUTES, exactly as the listing-type entries above are built
|
|
88
|
+
* from `pluginFor`. These ten were hand-written path literals until
|
|
89
|
+
* 2026-10-24 — sitting directly beside a block whose own comment says "One
|
|
90
|
+
* source of truth, no second mapping" and cites Root Cause #48 for the
|
|
91
|
+
* version of this bug that shipped. A renamed segment would have moved every
|
|
92
|
+
* real page and left the search index pointing at the old one, with each
|
|
93
|
+
* result rendering correctly and navigating to a 404.
|
|
94
|
+
*
|
|
95
|
+
* `{s}` is the placeholder the template engine substitutes, so the builder
|
|
96
|
+
* functions are handed it directly — the same trick `detailRoute("{s}")` uses
|
|
97
|
+
* above rather than re-deriving the shape.
|
|
98
|
+
*/
|
|
99
|
+
{ key: "category", hrefTemplate: ROUTES.PUBLIC.CATEGORY_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.CATEGORIES), weight: 0.95 },
|
|
100
|
+
{ key: "brand", hrefTemplate: ROUTES.PUBLIC.BRAND_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.BRANDS), weight: 0.9 },
|
|
101
|
+
{ key: "part", hrefTemplate: ROUTES.PUBLIC.PART_CLASSIFICATION("{s}"), moreHref: String(ROUTES.PUBLIC.PARTS), weight: 0.9 },
|
|
102
|
+
/* A model IS a category row, which is why it shares the category route. */
|
|
103
|
+
{ key: "model", hrefTemplate: ROUTES.PUBLIC.CATEGORY_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.CATEGORIES), weight: 0.85 },
|
|
104
|
+
{ key: "store", hrefTemplate: ROUTES.PUBLIC.STORE_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.STORES), weight: 0.8 },
|
|
105
|
+
{ key: "blog", hrefTemplate: ROUTES.BLOG.ARTICLE("{s}"), moreHref: String(ROUTES.BLOG.LIST), weight: 0.7 },
|
|
106
|
+
{ key: "event", hrefTemplate: ROUTES.PUBLIC.EVENT_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.EVENTS), weight: 0.7 },
|
|
107
|
+
{ key: "faq", hrefTemplate: ROUTES.PUBLIC.FAQ_CATEGORY("{s}"), moreHref: String(ROUTES.PUBLIC.FAQS), weight: 0.6 },
|
|
108
|
+
{ key: "review", hrefTemplate: ROUTES.PUBLIC.REVIEW_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.REVIEWS), weight: 0.5 },
|
|
109
|
+
{ key: "group", hrefTemplate: ROUTES.PUBLIC.GROUP_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.PRODUCTS), weight: 0.5 },
|
|
110
|
+
{ key: "scammer", hrefTemplate: ROUTES.PUBLIC.SCAM_DETAIL("{s}"), moreHref: String(ROUTES.PUBLIC.SCAMS), weight: 0.5 },
|
|
96
111
|
/* `/promotions` has no per-coupon page, so `{s}` is absent on purpose and
|
|
97
112
|
* `hrefFor`'s replace is a no-op. Eleven rows all landing on one page would
|
|
98
113
|
* be noise in the federated GROUP (which is why `search-sources.ts` omits
|
|
99
114
|
* coupons), but in the index it is the difference between a code being
|
|
100
115
|
* findable and not. */
|
|
101
|
-
{ key: "coupon", hrefTemplate:
|
|
116
|
+
{ key: "coupon", hrefTemplate: String(ROUTES.PUBLIC.PROMOTIONS), moreHref: String(ROUTES.PUBLIC.PROMOTIONS), weight: 0.4 },
|
|
102
117
|
];
|
|
103
118
|
const facet = (pairs) => {
|
|
104
119
|
const parts = pairs
|
|
@@ -14,6 +14,9 @@ import { PART_GLOSSARY_COLLECTION, ALL_PART_SLOTS } from "../../../../features/p
|
|
|
14
14
|
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
15
15
|
import { PRODUCT_FIELDS } from "../../../../constants/field-names.js";
|
|
16
16
|
import { PRODUCT_DETAIL_LISTING_TYPES } from "./product-detail-types.js";
|
|
17
|
+
import { safeRead } from "../../../../errors/safe-read.js";
|
|
18
|
+
import { siteSettingsRepository } from "../../../../repositories/index.js";
|
|
19
|
+
import { ALL_BROWSE_PAGES, browsePageForPath, isBrowsePageEnabled, } from "../../../shared/listing-types/feature-flags.js";
|
|
17
20
|
// Product field strings — matches consumer field-names.ts
|
|
18
21
|
const PRODUCT_STATUS = "status";
|
|
19
22
|
const PRODUCT_STATUS_PUBLISHED = "published";
|
|
@@ -618,17 +621,43 @@ export async function buildSitemap({ baseUrl }) {
|
|
|
618
621
|
bey: beyUrls.length,
|
|
619
622
|
part: partUrls.length,
|
|
620
623
|
};
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
624
|
+
/*
|
|
625
|
+
* Drop every URL belonging to a CLOSED browse section.
|
|
626
|
+
*
|
|
627
|
+
* 🛑 A single filter over the assembled list, not a condition threaded
|
|
628
|
+
* through the eighteen fetchers above. Section URLs are emitted from several
|
|
629
|
+
* of them plus `staticPages`, so gating from the inside is five edits and
|
|
630
|
+
* five chances to miss the sixth — and `browsePageForPath` is the same rule
|
|
631
|
+
* the navbar and the footer use, so the sitemap cannot end up disagreeing
|
|
632
|
+
* with the chrome about which sections exist.
|
|
633
|
+
*
|
|
634
|
+
* Advertising a URL that answers 404 is worse than omitting it: it is Root
|
|
635
|
+
* Cause #81's host/sitemap disagreement in miniature, and it spends crawl
|
|
636
|
+
* budget on pages we have deliberately withdrawn.
|
|
637
|
+
*
|
|
638
|
+
* 🛑 Applied BEFORE the per-section digest below. The digest is what
|
|
639
|
+
* `scripts/deploy.mjs` asserts against, so counting pre-filter would log
|
|
640
|
+
* URLs the response does not carry.
|
|
641
|
+
*/
|
|
642
|
+
const settings = await safeRead(() => siteSettingsRepository.findById("global"), {
|
|
643
|
+
route: "sitemap.xml",
|
|
644
|
+
key: "sitemap.siteSettings",
|
|
645
|
+
// Fails OPEN: an unreadable settings document is not evidence that a
|
|
646
|
+
// section was closed, and silently dropping the catalogue from the sitemap
|
|
647
|
+
// on a transient Firestore error is the expensive direction of this guess.
|
|
648
|
+
fallback: null,
|
|
649
|
+
});
|
|
650
|
+
const closed = new Set(ALL_BROWSE_PAGES.filter((p) => !isBrowsePageEnabled(p, settings)));
|
|
651
|
+
const keep = (entry) => {
|
|
652
|
+
if (closed.size === 0)
|
|
653
|
+
return true;
|
|
654
|
+
const path = entry.url.startsWith("http")
|
|
655
|
+
? new URL(entry.url).pathname
|
|
656
|
+
: entry.url;
|
|
657
|
+
const page = browsePageForPath(path);
|
|
658
|
+
return page === null || !closed.has(page);
|
|
659
|
+
};
|
|
660
|
+
const all = [
|
|
632
661
|
...staticUrls,
|
|
633
662
|
...categoryUrls,
|
|
634
663
|
...brandUrls,
|
|
@@ -648,5 +677,42 @@ export async function buildSitemap({ baseUrl }) {
|
|
|
648
677
|
...scammerUrls,
|
|
649
678
|
...beyUrls,
|
|
650
679
|
...partUrls,
|
|
651
|
-
];
|
|
680
|
+
].filter(keep);
|
|
681
|
+
/*
|
|
682
|
+
* Recount AFTER the filter, and exclude a deliberately-closed section from
|
|
683
|
+
* the "produced 0 URLs" alarm.
|
|
684
|
+
*
|
|
685
|
+
* 🛑 Without this the feature would make its own monitoring cry wolf: the
|
|
686
|
+
* warning exists because `category: 0` once survived unnoticed for months,
|
|
687
|
+
* and a warning that fires every build for a reason everyone knows about is
|
|
688
|
+
* how the next real zero gets ignored. "Closed on purpose" and "broken" must
|
|
689
|
+
* not look the same in the log.
|
|
690
|
+
*/
|
|
691
|
+
const sectionPage = {
|
|
692
|
+
category: "categories",
|
|
693
|
+
brand: "brands",
|
|
694
|
+
franchise: "franchise",
|
|
695
|
+
store: "stores",
|
|
696
|
+
};
|
|
697
|
+
for (const name of Object.keys(sections)) {
|
|
698
|
+
const page = sectionPage[name];
|
|
699
|
+
if (page && closed.has(page))
|
|
700
|
+
sections[name] = 0;
|
|
701
|
+
}
|
|
702
|
+
const empty = Object.entries(sections)
|
|
703
|
+
.filter(([name, n]) => n === 0 && !closed.has(sectionPage[name]))
|
|
704
|
+
.map(([name]) => name);
|
|
705
|
+
const total = all.length;
|
|
706
|
+
if (closed.size > 0) {
|
|
707
|
+
serverLogger.info("sitemap: browse section(s) closed, URLs withheld", {
|
|
708
|
+
closed: [...closed],
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (empty.length > 0) {
|
|
712
|
+
serverLogger.warn(`sitemap: ${empty.length} section(s) produced 0 URLs`, { empty, sections, total });
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
serverLogger.info("sitemap: built", { sections, total });
|
|
716
|
+
}
|
|
717
|
+
return all;
|
|
652
718
|
}
|
|
@@ -39,7 +39,7 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "support", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat", "search"];
|
|
41
41
|
/** Everything else, with the reason it stays server-side. */
|
|
42
|
-
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["rateChanges", "id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
42
|
+
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["rateChanges", "id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging", "whatsappFeed"];
|
|
43
43
|
export interface ToPublicSiteSettingsOptions {
|
|
44
44
|
/**
|
|
45
45
|
* The resolved marker → wordmark → text watermark chain. Injected because
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export const PUBLIC_SITE_SETTINGS_FIELDS = [
|
|
7
7
|
"contact", // user/orders/[id]/payment — contact.whatsappNumber (narrowed: no address/upiVpa)
|
|
8
8
|
"payment", // CheckoutRouteClient — payment.otpCheckoutThreshold (narrowed)
|
|
9
|
-
"listings", // useListingTypeFlags — narrowed to listings.listingTypes; categoryTypes has no client reader
|
|
9
|
+
"listings", // useListingTypeFlags + LayoutShellClient nav filter — narrowed to listings.{listingTypes,browsePages}; categoryTypes has no client reader
|
|
10
10
|
"notificationChannels", // NARROWED: only {email,whatsapp,sms}.enabled — see the projection below
|
|
11
11
|
"announcementBar", // the homepage banner (AdminSiteSettingsView edits it)
|
|
12
12
|
/*
|
|
@@ -118,6 +118,18 @@ export const PRIVATE_SITE_SETTINGS_FIELDS = [
|
|
|
118
118
|
// checkbox, which disables itself when email is off — is behind the admin
|
|
119
119
|
// API, which returns the full document.
|
|
120
120
|
"messaging",
|
|
121
|
+
/*
|
|
122
|
+
* The WhatsApp catalogue feed kill switch. Read ONLY by
|
|
123
|
+
* `/whatsapp/{storeSlug}/feed.csv`, which is a server route doing its own
|
|
124
|
+
* settings read — the same shape as `auctionConfig` above.
|
|
125
|
+
*
|
|
126
|
+
* 🛑 Deliberately NOT public, unlike its sibling `search`. That one is public
|
|
127
|
+
* because its reader is a GUEST's search box, so a server-only flag could not
|
|
128
|
+
* reach the visitor it governs. This flag's only reader is Meta's crawler
|
|
129
|
+
* fetching a CSV; no browser consults it, and publishing "catalogue feeds are
|
|
130
|
+
* currently off platform-wide" is operational detail with no client to serve.
|
|
131
|
+
*/
|
|
132
|
+
"whatsappFeed",
|
|
121
133
|
];
|
|
122
134
|
export function toPublicSiteSettings(doc, opts) {
|
|
123
135
|
return {
|
|
@@ -137,6 +149,9 @@ export function toPublicSiteSettings(doc, opts) {
|
|
|
137
149
|
},
|
|
138
150
|
listings: {
|
|
139
151
|
listingTypes: doc.listings?.listingTypes,
|
|
152
|
+
// Read by LayoutShellClient to drop nav/footer links into a closed
|
|
153
|
+
// section. `categoryTypes` is still withheld — it has no client reader.
|
|
154
|
+
browsePages: doc.listings?.browsePages,
|
|
140
155
|
},
|
|
141
156
|
/*
|
|
142
157
|
* 🛑 Narrowed to the three booleans a client reads, not the group by
|
|
@@ -27,7 +27,7 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "isVerified", "sellerTier", "standing", "createdAt"];
|
|
29
29
|
/** Everything else, with the reason it stays server-side. */
|
|
30
|
-
export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "trustedSeller", "acceptsPoints", "searchTxt", "whatsappNotifySubscribed", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
30
|
+
export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "whatsappFeedEnabled", "trustedSeller", "acceptsPoints", "searchTxt", "whatsappNotifySubscribed", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
31
31
|
/**
|
|
32
32
|
* Accepted input: a `StoreDocument` (the common case) or a loosely-typed
|
|
33
33
|
* Firestore row. The legacy top-level counters below predate `stats` and are
|
|
@@ -104,6 +104,20 @@ export const PRIVATE_STORE_FIELDS = [
|
|
|
104
104
|
* someone can name the client that reads it.
|
|
105
105
|
*/
|
|
106
106
|
"whatsappNotifySubscribed",
|
|
107
|
+
/*
|
|
108
|
+
* 🛑 `whatsappFeedEnabled` — a seller's own DISTRIBUTION PLUMBING.
|
|
109
|
+
*
|
|
110
|
+
* Whether this store currently publishes `/whatsapp/{slug}/feed.csv` with
|
|
111
|
+
* rows in it. Absent means enabled, so the field is only ever present when a
|
|
112
|
+
* seller (or an admin) has switched the feed off.
|
|
113
|
+
*
|
|
114
|
+
* No public reader, which is this file's bar on its own. Beyond that, a buyer
|
|
115
|
+
* learns nothing from it — it says nothing about the goods, the price or the
|
|
116
|
+
* dispatch record — while publishing it would advertise which stores are
|
|
117
|
+
* reachable on WhatsApp at all, which is the same disclosure
|
|
118
|
+
* `whatsappNotifySubscribed` directly above is private to avoid.
|
|
119
|
+
*/
|
|
120
|
+
"whatsappFeedEnabled",
|
|
107
121
|
/*
|
|
108
122
|
* 🛑 `trustedSeller` — OUR RISK POSTURE, not a badge.
|
|
109
123
|
*
|
|
@@ -24,22 +24,26 @@ export declare function resolveStoreTabs(storeSlug: string, storeId: string | un
|
|
|
24
24
|
/**
|
|
25
25
|
* Where the bare `/stores/[slug]` route should land.
|
|
26
26
|
*
|
|
27
|
-
* Order:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Order: the first listing-type tab holding anything, else Coupons, else
|
|
28
|
+
* Reviews, else About. `about` is always present in `tabs`, so this never
|
|
29
|
+
* falls through to nothing.
|
|
30
30
|
*
|
|
31
|
-
* 🛑 About was promoted to
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* 🛑 About was promoted to FIRST on 2026-10-21 and REVERTED on 2026-10-24.
|
|
32
|
+
* The argument for it was that a stocked storefront opening on a filtered
|
|
33
|
+
* product grid "reads as a search result". What it actually produced is worse
|
|
34
|
+
* and was reported as a bug: `store-letitrip-official` carries a
|
|
35
|
+
* `storeDescription`, so every visit to `/stores/store-letitrip-official`
|
|
36
|
+
* 307'd to `/about` — a page with no merchandise on it — and the shop read as
|
|
37
|
+
* having **no products at all**. Measured the day of the revert: the store
|
|
38
|
+
* holds 22 published listings, its `/products` tab renders 12 of them and its
|
|
39
|
+
* WhatsApp feed emits all 22, while the landing page showed none of it.
|
|
36
40
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* a
|
|
41
|
+
* A storefront's job is to show stock. An introduction is a tab, and the tab
|
|
42
|
+
* is right there in the nav bar.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 Do NOT re-add a `hasAboutContent` parameter to re-litigate this. It was
|
|
45
|
+
* the mechanism of the regression, not a safeguard against it: having written
|
|
46
|
+
* a description is not evidence that a shopper wants to read one.
|
|
43
47
|
*/
|
|
44
|
-
export declare function getDefaultStoreTabPath(storeSlug: string, storeId: string | undefined, settings: SiteSettingsSnapshot
|
|
48
|
+
export declare function getDefaultStoreTabPath(storeSlug: string, storeId: string | undefined, settings: SiteSettingsSnapshot): Promise<string>;
|
|
45
49
|
export {};
|
|
@@ -27,7 +27,7 @@ import { ROUTES } from "../../../../next/routing/route-map.js";
|
|
|
27
27
|
import { STORE_PAGE_TABS } from "../../../../features/products/constants/listing-tabs.js";
|
|
28
28
|
import { isListingTypeEnabled, isCategoryTypeEnabled } from "../../../shared/listing-types/feature-flags.js";
|
|
29
29
|
import { pluginFor } from "../../../shared/listing-types/_registry.js";
|
|
30
|
-
import { listingTabCounts
|
|
30
|
+
import { listingTabCounts } from "../products/listing-tab-counts.js";
|
|
31
31
|
function tabLabel(base, count) {
|
|
32
32
|
if (!count)
|
|
33
33
|
return base;
|
|
@@ -116,29 +116,28 @@ export async function resolveStoreTabs(storeSlug, storeId, settings) {
|
|
|
116
116
|
/**
|
|
117
117
|
* Where the bare `/stores/[slug]` route should land.
|
|
118
118
|
*
|
|
119
|
-
* Order:
|
|
120
|
-
*
|
|
121
|
-
*
|
|
119
|
+
* Order: the first listing-type tab holding anything, else Coupons, else
|
|
120
|
+
* Reviews, else About. `about` is always present in `tabs`, so this never
|
|
121
|
+
* falls through to nothing.
|
|
122
122
|
*
|
|
123
|
-
* 🛑 About was promoted to
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* 🛑 About was promoted to FIRST on 2026-10-21 and REVERTED on 2026-10-24.
|
|
124
|
+
* The argument for it was that a stocked storefront opening on a filtered
|
|
125
|
+
* product grid "reads as a search result". What it actually produced is worse
|
|
126
|
+
* and was reported as a bug: `store-letitrip-official` carries a
|
|
127
|
+
* `storeDescription`, so every visit to `/stores/store-letitrip-official`
|
|
128
|
+
* 307'd to `/about` — a page with no merchandise on it — and the shop read as
|
|
129
|
+
* having **no products at all**. Measured the day of the revert: the store
|
|
130
|
+
* holds 22 published listings, its `/products` tab renders 12 of them and its
|
|
131
|
+
* WhatsApp feed emits all 22, while the landing page showed none of it.
|
|
128
132
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* a
|
|
133
|
+
* A storefront's job is to show stock. An introduction is a tab, and the tab
|
|
134
|
+
* is right there in the nav bar.
|
|
135
|
+
*
|
|
136
|
+
* 🛑 Do NOT re-add a `hasAboutContent` parameter to re-litigate this. It was
|
|
137
|
+
* the mechanism of the regression, not a safeguard against it: having written
|
|
138
|
+
* a description is not evidence that a shopper wants to read one.
|
|
135
139
|
*/
|
|
136
|
-
export async function getDefaultStoreTabPath(storeSlug, storeId, settings
|
|
140
|
+
export async function getDefaultStoreTabPath(storeSlug, storeId, settings) {
|
|
137
141
|
const { dropdownTabs, tabs } = await resolveStoreTabs(storeSlug, storeId, settings);
|
|
138
|
-
if (hasAboutContent) {
|
|
139
|
-
const about = tabs.find((t) => t.value === "about");
|
|
140
|
-
if (about)
|
|
141
|
-
return about.href;
|
|
142
|
-
}
|
|
143
142
|
return (dropdownTabs[0] ?? tabs[0]).href;
|
|
144
143
|
}
|
|
@@ -54,7 +54,6 @@ const QUERY_LIMIT = 500;
|
|
|
54
54
|
* uses.
|
|
55
55
|
*/
|
|
56
56
|
const ORDERS_COLLECTION = "orders";
|
|
57
|
-
const REVIEWS_COLLECTION = "reviews";
|
|
58
57
|
const PAYOUTS_COLLECTION = "payouts";
|
|
59
58
|
const REPORTS_COLLECTION = "reports";
|
|
60
59
|
const USERS_COLLECTION = "users";
|
|
@@ -142,9 +141,15 @@ function buildMetricCache(ctx, bucket, now, onSaturated) {
|
|
|
142
141
|
const snap = await build().limit(QUERY_LIMIT).get();
|
|
143
142
|
return { value: snap.size, saturated: snap.size >= QUERY_LIMIT };
|
|
144
143
|
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
/*
|
|
145
|
+
* 🛑 A `buildQuery()` helper used to sit here and was removed 2026-10-24. It
|
|
146
|
+
* was never CALLED — its only reference was `ReturnType<typeof buildQuery>` in
|
|
147
|
+
* the signature above, and since `Q` is declared one line up and the function
|
|
148
|
+
* returned `Q`, that resolved to `Q` anyway. So the whole function existed to
|
|
149
|
+
* name a type that already had a name, which is why it read as used to a human
|
|
150
|
+
* and as unused to eslint. Every caller builds its own query because each
|
|
151
|
+
* needs different `where` clauses.
|
|
152
|
+
*/
|
|
148
153
|
/** Seller metrics scope to the store; admin metrics span the platform. */
|
|
149
154
|
const storeScoped = (q) => bucket.scope === "seller" ? q.where(PRODUCT_FIELDS.STORE_ID, "==", bucket.ownerId) : q;
|
|
150
155
|
const compute = async (metric, windowHours) => {
|
|
@@ -219,6 +219,28 @@ export async function runCategoryTaxonomyBackfill(payload, ctx) {
|
|
|
219
219
|
skipped.push(doc.id);
|
|
220
220
|
continue;
|
|
221
221
|
}
|
|
222
|
+
/*
|
|
223
|
+
* 🛑 PARTIAL resolution is reported, not acted on — and the distinction is
|
|
224
|
+
* the reason. `unresolved` was set here and never read until 2026-10-24, so
|
|
225
|
+
* a product carrying one dangling category id had that id silently dropped
|
|
226
|
+
* from its closure: exactly the shortening the comment above this loop warns
|
|
227
|
+
* about ("dropping it would shorten the closure and un-list the product from
|
|
228
|
+
* a category it is genuinely in").
|
|
229
|
+
*
|
|
230
|
+
* It is NOT turned into a skip, because the two causes are indistinguishable
|
|
231
|
+
* from inside this job: a dangling id may be a category that was legitimately
|
|
232
|
+
* DELETED, in which case dropping it is the correct repair, or a data error,
|
|
233
|
+
* in which case writing the short chain is the defect. Skipping would leave
|
|
234
|
+
* the first case permanently un-repaired; guessing on a job that mutates
|
|
235
|
+
* every product in a subtree is worse than reporting it.
|
|
236
|
+
*/
|
|
237
|
+
if (unresolved) {
|
|
238
|
+
ctx.logger.warn("categoryTaxonomyBackfill: product has unresolvable category ids", {
|
|
239
|
+
productId: doc.id,
|
|
240
|
+
assigned: assignments.length,
|
|
241
|
+
resolved: resolved.length,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
222
244
|
/*
|
|
223
245
|
* The default is the DEEPEST assignment, and the tie-break is deliberately
|
|
224
246
|
* NOT consulted here: reading counts per product would be one extra read
|
|
@@ -6,7 +6,6 @@ import { CATEGORY_COUNTS_COLLECTION } from "../../../../features/categories/sche
|
|
|
6
6
|
import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names.js";
|
|
7
7
|
import { isListingRowAvailable } from "../../../shared/listing-types/_registry.js";
|
|
8
8
|
import { QUERY_LIMIT } from "../handlers/messages.js";
|
|
9
|
-
const STORES_COLLECTION = "stores";
|
|
10
9
|
const ORDERS_COLLECTION = "orders";
|
|
11
10
|
/** Page size for the products scan. Bounded reads, unbounded total coverage. */
|
|
12
11
|
const PAGE_SIZE = 500;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { adminNotificationsRepository } from "../../../../repositories/index.js";
|
|
2
|
-
export const onCatalogueSubmittedForApprovalHandler = async (event
|
|
2
|
+
export const onCatalogueSubmittedForApprovalHandler = async (event) => {
|
|
3
3
|
const before = event.before?.listingStatus;
|
|
4
4
|
const after = event.after?.listingStatus;
|
|
5
5
|
if (before === "pending_admin_approval" || after !== "pending_admin_approval")
|
|
@@ -38,17 +38,25 @@ export interface PublicSiteSettings {
|
|
|
38
38
|
otpCheckoutThreshold?: number;
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
|
-
* Which listing types the site offers
|
|
42
|
-
* rendering a
|
|
41
|
+
* Which listing types the site offers, and which browse sections are open —
|
|
42
|
+
* the client needs both to avoid rendering a nav entry or a browse tab that
|
|
43
|
+
* leads somewhere nothing will return.
|
|
43
44
|
*
|
|
44
45
|
* Deliberately NOT the whole `listings` block: `categoryTypes` has no client
|
|
45
46
|
* reader. This was `featureFlags.listingTypes` until that group was deleted
|
|
46
47
|
* on 2026-08-29; the projection stayed narrow for the same reason it always
|
|
47
48
|
* was — a public payload carries what a client demonstrably reads, nothing
|
|
48
49
|
* more (Root Cause #70).
|
|
50
|
+
*
|
|
51
|
+
* `browsePages` HAS a client reader and must be published: `LayoutShellClient`
|
|
52
|
+
* filters the navbar, the sidebar, the mobile sheet and the footer from it.
|
|
53
|
+
* Withholding it would leave every link to a closed section in the chrome,
|
|
54
|
+
* pointing at a route that now 404s — a dead-end CTA on every page of the
|
|
55
|
+
* site, which is a worse outcome than not having the flag at all.
|
|
49
56
|
*/
|
|
50
57
|
listings: {
|
|
51
58
|
listingTypes?: NonNullable<SiteSettingsDocument["listings"]>["listingTypes"];
|
|
59
|
+
browsePages?: NonNullable<SiteSettingsDocument["listings"]>["browsePages"];
|
|
52
60
|
};
|
|
53
61
|
/** D9 — off by default; `enabled: false` is emitted rather than omitted, same `always-present` reasoning as `disabledRoutes` below. */
|
|
54
62
|
chat?: {
|
|
@@ -155,7 +155,6 @@ export function segmentSheet(raster, params) {
|
|
|
155
155
|
let labelled;
|
|
156
156
|
let beforeBridge;
|
|
157
157
|
let merges = 0;
|
|
158
|
-
let sameMaterialDropped = 0;
|
|
159
158
|
/*
|
|
160
159
|
* 1 on the art path, where the flood never runs and the question of "was
|
|
161
160
|
* there anything to flood through" does not arise. Reporting 0 there would
|
|
@@ -179,7 +178,19 @@ export function segmentSheet(raster, params) {
|
|
|
179
178
|
const lines = detectLines(filtered, width, height);
|
|
180
179
|
const lineBarrier = thresholdAdaptive(lines.response, width, height, params.ridgeHysteresisLow, params.ridgeHysteresisHigh);
|
|
181
180
|
const material = sameMaterialScore(raster, lineBarrier, filtered, params.paperTolerance * 2);
|
|
182
|
-
|
|
181
|
+
/*
|
|
182
|
+
* 🛑 `sameMaterialDropped = Math.round(material.meanScore * 1000)` used to sit
|
|
183
|
+
* here and was removed 2026-10-24: the local was assigned and never read, is
|
|
184
|
+
* not in the `diagnostics` object this function returns, and is not declared on
|
|
185
|
+
* that type — so nothing anywhere could observe it.
|
|
186
|
+
*
|
|
187
|
+
* The `sameMaterialScore` call above stays, and that distinction is the point:
|
|
188
|
+
* `material.score` feeds `mergeWeakBoundaries` a few lines down, so the pass is
|
|
189
|
+
* load-bearing even though that one derived number was not. I deleted the call
|
|
190
|
+
* first, on a grep for `sameMaterialDropped` rather than for `material` — `tsc`
|
|
191
|
+
* caught it. Re-add the metric with a declared diagnostics field if it is ever
|
|
192
|
+
* actually wanted.
|
|
193
|
+
*/
|
|
183
194
|
const open = new Uint8Array(width * height);
|
|
184
195
|
for (let i = 0; i < open.length; i++)
|
|
185
196
|
open[i] = lineBarrier[i] ? 0 : 1;
|
|
@@ -24,8 +24,62 @@ interface FeatureFlagSnapshot {
|
|
|
24
24
|
listings?: {
|
|
25
25
|
listingTypes?: Partial<Record<ListingType, boolean>>;
|
|
26
26
|
categoryTypes?: Partial<Record<CategoryType, boolean>>;
|
|
27
|
+
browsePages?: Partial<Record<BrowsePageKey, boolean>>;
|
|
27
28
|
};
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A public BROWSE SECTION that can be withdrawn as a whole.
|
|
32
|
+
*
|
|
33
|
+
* 🛑 This is a THIRD axis, not a rename of either of the two above, and the
|
|
34
|
+
* distinction is what stops it becoming a duplicate control:
|
|
35
|
+
*
|
|
36
|
+
* `listingTypes` — what the marketplace SELLS (is there such a thing as an
|
|
37
|
+
* auction here?)
|
|
38
|
+
* `categoryTypes` — what a taxonomy ROW may be (may a category row be a
|
|
39
|
+
* brand?)
|
|
40
|
+
* `browsePages` — whether a public ENTRY POINT is open (is there a /brands
|
|
41
|
+
* directory to walk?)
|
|
42
|
+
*
|
|
43
|
+
* They are genuinely independent. `brand` rows still tag products and still
|
|
44
|
+
* drive `BrandDetailPageView`'s query with `/brands` closed; closing the
|
|
45
|
+
* directory withdraws the front door, it does not untag the catalogue. That is
|
|
46
|
+
* why this cannot be folded into `categoryTypes` — and `categories`, `stores`
|
|
47
|
+
* and `franchise` have no `categoryTypes` member to fold into at all.
|
|
48
|
+
*
|
|
49
|
+
* `stores` covers BOTH `/stores` and `/sellers`: they are two doors onto the
|
|
50
|
+
* same directory (the footer lists them as "Stores" and "Verified Sellers"),
|
|
51
|
+
* and leaving one open while closing the other publishes exactly what the
|
|
52
|
+
* other was closed to hide.
|
|
53
|
+
*/
|
|
54
|
+
export type BrowsePageKey = "categories" | "brands" | "franchise" | "stores";
|
|
55
|
+
export declare const ALL_BROWSE_PAGES: BrowsePageKey[];
|
|
56
|
+
/**
|
|
57
|
+
* `true` when the section is missing from the flag map OR explicitly enabled.
|
|
58
|
+
*
|
|
59
|
+
* Absent-means-enabled, matching its two siblings: a section has to be
|
|
60
|
+
* EXPLICITLY `false` to close. An install that predates this setting keeps
|
|
61
|
+
* every section open, so adding the field cannot take a site's navigation away
|
|
62
|
+
* from it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function isBrowsePageEnabled(page: BrowsePageKey, settings: FeatureFlagSnapshot | null | undefined): boolean;
|
|
65
|
+
export declare function enabledBrowsePages(settings: FeatureFlagSnapshot | null | undefined): BrowsePageKey[];
|
|
66
|
+
/** Narrow an untrusted string — a route param, a form value — to the union. */
|
|
67
|
+
export declare function isBrowsePageKey(value: unknown): value is BrowsePageKey;
|
|
68
|
+
/**
|
|
69
|
+
* Which browse section a link points into, or `null` for a link that is not in
|
|
70
|
+
* one. Used to drop nav, sidebar, mobile-sheet and footer entries for a closed
|
|
71
|
+
* section.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 Matches on an EXACT path or a `/`-delimited descendant, never a bare
|
|
74
|
+
* `startsWith`. `/store` is the SELLER DASHBOARD and `/stores` is the public
|
|
75
|
+
* directory, so a prefix test would hide a signed-in seller's own dashboard
|
|
76
|
+
* link the moment an admin closed the public storefronts — a different
|
|
77
|
+
* surface, a different audience, one character apart.
|
|
78
|
+
*
|
|
79
|
+
* Query strings and hashes are tolerated because footer and nav hrefs are
|
|
80
|
+
* hand-authored and some carry them.
|
|
81
|
+
*/
|
|
82
|
+
export declare function browsePageForPath(href: string | undefined | null): BrowsePageKey | null;
|
|
29
83
|
/**
|
|
30
84
|
* `true` when the type is missing from the flag map OR explicitly enabled.
|
|
31
85
|
* Defaulting to enabled keeps legacy data working — a flag has to be
|