@mohasinac/appkit 4.37.1 → 4.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.d.ts +20 -1
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +21 -1
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.d.ts +40 -0
- package/dist/_internal/client/features/digital-code/DigitalContentPoolManager.js +67 -0
- package/dist/_internal/server/features/bundles/og.d.ts +0 -1
- package/dist/_internal/server/features/bundles/og.js +2 -18
- package/dist/_internal/server/features/catalogue/og.d.ts +0 -1
- package/dist/_internal/server/features/catalogue/og.js +1 -9
- package/dist/_internal/server/features/checkout/actions.js +18 -0
- package/dist/_internal/server/features/classified/og.d.ts +0 -1
- package/dist/_internal/server/features/classified/og.js +1 -9
- package/dist/_internal/server/features/digital-code/download.d.ts +60 -0
- package/dist/_internal/server/features/digital-code/download.js +128 -0
- package/dist/_internal/server/features/digital-code/og.d.ts +0 -1
- package/dist/_internal/server/features/digital-code/og.js +1 -9
- package/dist/_internal/server/features/digital-code/pool.d.ts +101 -0
- package/dist/_internal/server/features/digital-code/pool.js +208 -0
- package/dist/_internal/server/features/digital-code/upload.d.ts +51 -0
- package/dist/_internal/server/features/digital-code/upload.js +144 -0
- package/dist/_internal/server/features/grouped/og.d.ts +7 -4
- package/dist/_internal/server/features/grouped/og.js +7 -6
- package/dist/_internal/server/features/item-requests/og.d.ts +0 -1
- package/dist/_internal/server/features/item-requests/og.js +2 -18
- package/dist/_internal/server/features/live/og.d.ts +0 -1
- package/dist/_internal/server/features/live/og.js +1 -9
- package/dist/_internal/server/features/prize-draws/og.js +5 -10
- package/dist/_internal/server/features/products/og.js +5 -2
- package/dist/_internal/server/features/seo/sitemap.js +10 -2
- package/dist/_internal/server/features/shared/listing-adapters.d.ts +1 -2
- package/dist/_internal/server/features/shared/listing-adapters.js +5 -3
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +40 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +153 -40
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +33 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -25
- package/dist/_internal/server/notifications/send-recorder.d.ts +79 -0
- package/dist/_internal/server/notifications/send-recorder.js +126 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +31 -4
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/config.js +2 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/config.js +3 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/classified/config.js +3 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +3 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/live/config.js +3 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +3 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/config.js +2 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
- package/dist/client/observability/reportClientError.js +11 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.js +10 -0
- package/dist/core/hooks/useSyncManager.js +11 -1
- package/dist/features/account/hooks/useNotifications.js +25 -5
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -3
- package/dist/features/auctions/components/LiveBidPrice.js +2 -2
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +1 -1
- package/dist/features/auctions/components/LiveMinIncrement.js +8 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useAuctions.js +12 -1
- package/dist/features/auctions/hooks/useRealtimeBids.d.ts +3 -0
- package/dist/features/auctions/hooks/useRealtimeBids.js +138 -36
- package/dist/features/blog/components/BlogIndexPageView.js +10 -1
- package/dist/features/blog/repository/blog.repository.d.ts +14 -0
- package/dist/features/blog/repository/blog.repository.js +23 -1
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +2 -2
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -2
- package/dist/features/categories/components/BundleCollage.js +10 -5
- package/dist/features/categories/components/BundleDetailView.js +7 -3
- package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
- package/dist/features/categories/components/CategoryProductsListing.js +4 -1
- package/dist/features/categories/components/CategoryTree.js +1 -1
- package/dist/features/categories/repository/categories.repository.d.ts +34 -2
- package/dist/features/categories/repository/categories.repository.js +31 -9
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -2
- package/dist/features/contact/email.js +36 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +2 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -4
- package/dist/features/live/components/LiveItemDetailPageView.js +2 -2
- package/dist/features/orders/repository/orders.repository.d.ts +21 -1
- package/dist/features/orders/repository/orders.repository.js +26 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +2 -2
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +4 -1
- package/dist/features/products/actions/product-actions.d.ts +9 -0
- package/dist/features/products/actions/product-actions.js +11 -0
- package/dist/features/products/columns/productTableColumns.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +4 -1
- package/dist/features/products/components/BidHistory.js +2 -2
- package/dist/features/products/components/CompareOverlay.js +2 -2
- package/dist/features/products/components/GroupMemberPicker.js +5 -5
- package/dist/features/products/components/ListingBottomActions.js +12 -3
- package/dist/features/products/components/MakeOfferButton.js +7 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +4 -4
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
- package/dist/features/products/components/PrizeDrawCollage.js +16 -5
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +4 -1
- package/dist/features/products/components/PrizeRevealModal.js +2 -2
- package/dist/features/products/components/ProductDetailActions.js +10 -3
- package/dist/features/products/components/ProductFilters.js +2 -2
- package/dist/features/products/components/ProductGrid.js +5 -2
- package/dist/features/products/components/ProductsIndexListing.js +12 -1
- package/dist/features/products/components/ProductsIndexPageView.js +26 -0
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -2
- package/dist/features/products/constants/listing-tabs.d.ts +36 -12
- package/dist/features/products/constants/listing-tabs.js +38 -14
- package/dist/features/products/constants/sieve.d.ts +14 -0
- package/dist/features/products/constants/sieve.js +39 -0
- package/dist/features/products/repository/products.repository.d.ts +35 -0
- package/dist/features/products/repository/products.repository.js +116 -3
- package/dist/features/products/schemas/firestore.d.ts +37 -0
- package/dist/features/seller/actions/seller-actions.js +4 -13
- package/dist/features/seller/components/SellerProductShell.d.ts +7 -1
- package/dist/features/seller/components/SellerProductShell.js +11 -2
- package/dist/features/stores/api/[storeSlug]/reviews/route.d.ts +6 -1
- package/dist/features/stores/api/[storeSlug]/reviews/route.js +89 -30
- package/dist/features/stores/components/StoresIndexPageView.js +17 -1
- package/dist/features/tester/seed-data/authored/admin__users-trust.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__bidding.js +5 -4
- package/dist/features/tester/seed-data/authored/buying__browsing-search.js +85 -85
- package/dist/features/tester/seed-data/authored/buying__image-tile-layout.js +2 -2
- package/dist/features/tester/seed-data/authored/buying__offers.js +1 -1
- package/dist/features/tester/seed-data/authored/buying__product-detail.js +53 -15
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-extras.js +6 -6
- package/dist/features/tester/seed-data/authored/content-discovery__search.js +6 -4
- package/dist/features/tester/seed-data/authored/design-ux__carousel-arrow-bounds.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__homepage-carousels.js +2 -2
- package/dist/features/tester/seed-data/authored/design-ux__status-badge-legibility.js +4 -4
- package/dist/features/tester/seed-data/authored/design-ux__sticky-cta-bar.js +8 -8
- package/dist/features/tester/seed-data/authored/public-pages__core-listing-pages.js +8 -8
- package/dist/features/tester/seed-data/authored/seo__canonical-and-host.js +16 -0
- package/dist/features/tester/seed-data/authored/seo__og-images.js +17 -0
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +41 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +36 -6
- package/dist/features/tester/utils/phases.js +78 -12
- package/dist/features/wishlist/components/WishlistPage.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/providers/db-firebase/rtdb-paths.d.ts +7 -0
- package/dist/providers/db-firebase/rtdb-paths.js +7 -0
- package/dist/react/contexts/SessionContext.js +40 -1
- package/dist/react/hooks/useCanSeePrices.d.ts +37 -0
- package/dist/react/hooks/useCanSeePrices.js +40 -0
- package/dist/seed/addresses-seed-data.js +36 -2
- package/dist/seed/users-seed-data.js +10 -2
- package/dist/seo/json-ld.d.ts +21 -0
- package/dist/seo/json-ld.js +45 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +12 -0
- package/dist/styles.css +41 -0
- package/dist/ui/components/FilePickerButton.d.ts +32 -0
- package/dist/ui/components/FilePickerButton.js +23 -0
- package/dist/ui/components/GatedPrice.d.ts +66 -0
- package/dist/ui/components/GatedPrice.js +80 -0
- package/dist/ui/components/GatedPrice.style.css +40 -0
- package/dist/ui/components/PriceDisplay.d.ts +12 -0
- package/dist/ui/components/PriceDisplay.js +17 -17
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +8 -0
- package/package.json +1 -1
|
@@ -390,9 +390,9 @@ const rawTesterChecklistItems = [
|
|
|
390
390
|
href: "/products",
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
|
-
key: "
|
|
394
|
-
label: "
|
|
395
|
-
description: "
|
|
393
|
+
key: "every-listing-type-reachable-from-nav-or-products",
|
|
394
|
+
label: "Every listing type is reachable — the five with a main-nav entry from the nav, and the four without one from the /products Listing type filter",
|
|
395
|
+
description: "Reachability is the invariant, not the route to it. Auctions, pre-orders, prize draws and art/stickers used to have no entry in the /products type filter and were unreachable from the main catalogue; they were added there, and have now been moved back out because each owns a nav-linked page of its own. Classifieds, digital codes and live items have NO nav entry, so /products is still their only discovery path — dropping them from it is the same bug in a new place.",
|
|
396
396
|
href: "/products",
|
|
397
397
|
},
|
|
398
398
|
{
|
|
@@ -456,7 +456,19 @@ const rawTesterChecklistItems = [
|
|
|
456
456
|
pageLabel: "Product / Auction / Pre-order Detail",
|
|
457
457
|
href: "/products",
|
|
458
458
|
cases: [
|
|
459
|
-
{ key: "standard-detail", label: "Standard product detail page loads correctly", href: "/products/product-tester-standard-1" },
|
|
459
|
+
{ key: "standard-detail", label: "Standard product detail page loads correctly, and a SIGNED-OUT visitor sees \"Sign in to see price\" where the price was", href: "/products/product-tester-standard-1" },
|
|
460
|
+
{
|
|
461
|
+
key: "standard-detail-price-signed-in",
|
|
462
|
+
label: "SIGNED IN, the same product detail page shows the real price (₹199.00) and no sign-in prompt",
|
|
463
|
+
description: "The twin of the guest case above, and the half that is easy to forget: gating a price must not cost the coverage of the price itself. A gate that hides the amount from everyone passes the guest case perfectly and is a total regression.",
|
|
464
|
+
href: "/products/product-tester-standard-1",
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
key: "price-does-not-flash-sign-in-prompt-on-hard-reload",
|
|
468
|
+
label: "SIGNED IN, hard-reloading a product page never flashes \"Sign in to see price\" before the real price appears",
|
|
469
|
+
description: "The session provider is mounted with no user and resolves asynchronously, so `loading` starts true on every hard load — for signed-in visitors too. A gate written as \"no user means show the prompt\" therefore accuses an already-signed-in buyer of being signed out for several hundred milliseconds on every page load. The gate has THREE states for this reason: while the session is resolving it renders a neutral placeholder, never the prompt. This is the single most likely regression in the price-gating work and it is invisible to every static check.",
|
|
470
|
+
href: "/products/product-tester-standard-1",
|
|
471
|
+
},
|
|
460
472
|
{
|
|
461
473
|
key: "tester-fixtures-hidden-from-the-public",
|
|
462
474
|
label: "SIGNED OUT, no product on /products has an id containing \"tester\" — the sandbox is invisible to the public",
|
|
@@ -477,8 +489,14 @@ const rawTesterChecklistItems = [
|
|
|
477
489
|
},
|
|
478
490
|
{
|
|
479
491
|
key: "auction-detail",
|
|
480
|
-
label: "
|
|
481
|
-
description: "The countdown (\"Ends in 2d 5h 30m\", ticking every second — not a static date) must appear directly above every \"Place a bid\" button: the info panel, the desktop and mobile compact bid-summary cards,
|
|
492
|
+
label: "SIGNED OUT, the auction detail page hides every bid figure and still shows a live, ticking countdown",
|
|
493
|
+
description: "The countdown (\"Ends in 2d 5h 30m\", ticking every second — not a static date) must appear directly above every \"Place a bid\" button: the info panel, the desktop and mobile compact bid-summary cards, and the mobile sticky bottom bar. It should switch to \"Ended\" once the end time passes. A signed-out visitor additionally sees NO bid amounts: the current bid reads \"Sign in to see the current bid\" and the bid form is replaced by \"Bidding is for members\" — the form was never usable signed out, so showing it with live figures was both a disclosure and a dead end.",
|
|
494
|
+
href: "/auctions/auction-tester-sandbox-cycle-1",
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
key: "auction-detail-bid-signed-in",
|
|
498
|
+
label: "SIGNED IN, the same auction shows the real current bid (₹15,000.00), the minimum increment and a working bid form",
|
|
499
|
+
description: "The twin of the guest case above. A gate that hides the bid from signed-in buyers too would pass the guest case and break bidding entirely.",
|
|
482
500
|
href: "/auctions/auction-tester-sandbox-cycle-1",
|
|
483
501
|
},
|
|
484
502
|
{ key: "preorder-detail", label: "Pre-order detail page shows expected ship date correctly", href: "/pre-orders/preorder-tester-sandbox-1" },
|
|
@@ -4232,6 +4250,12 @@ const rawTesterChecklistItems = [
|
|
|
4232
4250
|
label: "The apex host redirects to the www host, and does so permanently rather than temporarily",
|
|
4233
4251
|
description: "BEFORE: the apex 307'd to www. A 307 is TEMPORARY and explicitly tells a search engine NOT to move the index entry, so every page kept two competing addresses. AFTER: the redirect is permanent and the destination is the host the sitemap advertises.",
|
|
4234
4252
|
},
|
|
4253
|
+
{
|
|
4254
|
+
key: "gated-price-is-declared-in-structured-data",
|
|
4255
|
+
label: "A product and an auction page still publish offers.price to search engines, AND declare the sign-in gate with isAccessibleForFree:false plus a cssSelector that matches a real element",
|
|
4256
|
+
description: "Prices are hidden from signed-out visitors, and Googlebot crawls signed out — so the crawler renders 'Sign in to see price' while the JSON-LD beside it declares a real amount. Google requires marked-up content to be visible to the user, so that gap has to be DECLARED (paywalled-content markup) rather than left implicit, which risks the rich result. Serving prices to crawler user-agents but not to people would be cloaking and is deliberately not done. The selector half matters as much as the flag: a cssSelector matching nothing is indistinguishable from having made no declaration at all.",
|
|
4257
|
+
href: "/products/product-beyblade-burst-valkyrie",
|
|
4258
|
+
},
|
|
4235
4259
|
{
|
|
4236
4260
|
key: "locale-prefix-redirects-to-bare",
|
|
4237
4261
|
label: "A locale-prefixed path redirects to the unprefixed one",
|
|
@@ -4362,6 +4386,12 @@ const rawTesterChecklistItems = [
|
|
|
4362
4386
|
label: "A product's social preview image shows that product's own title and image",
|
|
4363
4387
|
href: "/products/product-beyblade-burst-valkyrie",
|
|
4364
4388
|
},
|
|
4389
|
+
{
|
|
4390
|
+
key: "og-image-carries-no-price",
|
|
4391
|
+
label: "No social preview image renders a ₹ amount — product, bundle, classified, digital code, live item, catalogue item, prize draw or item request",
|
|
4392
|
+
description: "An OG image is a public image URL with the amount burned into the pixels. No sign-in gate reaches it and no structured-data declaration describes it, so it is the one surface where hiding a price on the page buys nothing. Eight og.tsx renderers carried a ₹ figure before 2026-09-14; the auction and pre-order cards already used a non-money accent (end date / release date) and are the pattern the rest now follow.",
|
|
4393
|
+
href: "/products/product-beyblade-burst-valkyrie",
|
|
4394
|
+
},
|
|
4365
4395
|
{
|
|
4366
4396
|
key: "og-image-brand-logo-present",
|
|
4367
4397
|
label: "A brand's social preview card shows the brand's cover image rather than an empty slot",
|
|
@@ -25,22 +25,88 @@ export const DEFAULT_TESTER_PHASE_SIZE = 25;
|
|
|
25
25
|
* already be in catalog order (e.g. sorted by the `order` field).
|
|
26
26
|
*/
|
|
27
27
|
export function assignDefaultPhases(itemsInCatalogOrder, targetPhaseSize = DEFAULT_TESTER_PHASE_SIZE) {
|
|
28
|
-
|
|
28
|
+
/*
|
|
29
|
+
* 🛑 PAGES ARE INTERLEAVED ACROSS GROUPS, NOT PACKED IN CATALOGUE ORDER.
|
|
30
|
+
*
|
|
31
|
+
* This used to walk the catalogue straight through, so a group's phase number
|
|
32
|
+
* was an accident of where its `...group(...)` spread sat in a 6,200-line
|
|
33
|
+
* literal. `admin` is the 9th spread, so its 231 cases landed at phases
|
|
34
|
+
* **25–32** — and any run that stops early is therefore GUARANTEED to lose
|
|
35
|
+
* the largest, most privileged group in its entirety. The same held for every
|
|
36
|
+
* infrastructure group in the tail: page-wiring (32–33), cta-layout (33),
|
|
37
|
+
* addresses and search-and-nav (34).
|
|
38
|
+
*
|
|
39
|
+
* That is not hypothetical. Run run-1789300124915 stopped at phase 17 of 34
|
|
40
|
+
* and ten of fourteen groups had ZERO coverage — 527 cases, including all 233
|
|
41
|
+
* admin ones — while the run looked about half done.
|
|
42
|
+
*
|
|
43
|
+
* Round-robin over groups fixes the shape rather than the symptom: an early
|
|
44
|
+
* stop now costs a proportional slice of every group instead of the whole
|
|
45
|
+
* tail. Groups are visited largest-first within each round so the big ones do
|
|
46
|
+
* not bunch up at the end as the small ones run out.
|
|
47
|
+
*
|
|
48
|
+
* A phase may now span several groups. That is fine for a tester session —
|
|
49
|
+
* it is ~25 cases either way — and every read path already groups by the
|
|
50
|
+
* STORED `item.phase`, never by position.
|
|
51
|
+
*
|
|
52
|
+
* 🛑 The RETURN IS STILL POSITIONAL. Items keep their catalogue order and
|
|
53
|
+
* their `order` field; only the phase LABEL changes. Nothing downstream
|
|
54
|
+
* reorders, and `fetch-cases` still sorts by (phase, order) to build batches.
|
|
55
|
+
*/
|
|
56
|
+
const pageOrder = [];
|
|
57
|
+
const pageSize = new Map();
|
|
58
|
+
const pageGroup = new Map();
|
|
59
|
+
for (const item of itemsInCatalogOrder) {
|
|
60
|
+
const pageId = `${item.groupKey}␟${item.pageKey}`;
|
|
61
|
+
if (!pageSize.has(pageId)) {
|
|
62
|
+
pageOrder.push(pageId);
|
|
63
|
+
pageSize.set(pageId, 0);
|
|
64
|
+
pageGroup.set(pageId, item.groupKey);
|
|
65
|
+
}
|
|
66
|
+
pageSize.set(pageId, pageSize.get(pageId) + 1);
|
|
67
|
+
}
|
|
68
|
+
// Queues of pages per group, each still in catalogue order within its group.
|
|
69
|
+
const byGroup = new Map();
|
|
70
|
+
for (const pageId of pageOrder) {
|
|
71
|
+
const g = pageGroup.get(pageId);
|
|
72
|
+
if (!byGroup.has(g))
|
|
73
|
+
byGroup.set(g, []);
|
|
74
|
+
byGroup.get(g).push(pageId);
|
|
75
|
+
}
|
|
76
|
+
const groupsLargestFirst = [...byGroup.entries()]
|
|
77
|
+
.map(([g, pages]) => ({
|
|
78
|
+
g,
|
|
79
|
+
pages,
|
|
80
|
+
cases: pages.reduce((n, p) => n + pageSize.get(p), 0),
|
|
81
|
+
}))
|
|
82
|
+
.sort((a, b) => b.cases - a.cases || a.g.localeCompare(b.g))
|
|
83
|
+
.map((e) => e.g);
|
|
84
|
+
const cursor = new Map(groupsLargestFirst.map((g) => [g, 0]));
|
|
85
|
+
const interleaved = [];
|
|
86
|
+
let remaining = pageOrder.length;
|
|
87
|
+
while (remaining > 0) {
|
|
88
|
+
for (const g of groupsLargestFirst) {
|
|
89
|
+
const i = cursor.get(g);
|
|
90
|
+
const pages = byGroup.get(g);
|
|
91
|
+
if (i >= pages.length)
|
|
92
|
+
continue;
|
|
93
|
+
interleaved.push(pages[i]);
|
|
94
|
+
cursor.set(g, i + 1);
|
|
95
|
+
remaining--;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Pack the interleaved page sequence into phases, never splitting a page.
|
|
99
|
+
const phaseOfPage = new Map();
|
|
29
100
|
let currentPhase = 1;
|
|
30
101
|
let currentPhaseCount = 0;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const pageId = `${item.groupKey}␟${item.pageKey}`;
|
|
35
|
-
const isNewPage = pageId !== lastPageKey;
|
|
36
|
-
// Only allow a phase boundary between pages, never mid-page.
|
|
37
|
-
if (isNewPage && currentPhaseCount >= targetPhaseSize) {
|
|
102
|
+
for (const pageId of interleaved) {
|
|
103
|
+
const size = pageSize.get(pageId);
|
|
104
|
+
if (currentPhaseCount > 0 && currentPhaseCount >= targetPhaseSize) {
|
|
38
105
|
currentPhase += 1;
|
|
39
106
|
currentPhaseCount = 0;
|
|
40
107
|
}
|
|
41
|
-
|
|
42
|
-
currentPhaseCount +=
|
|
43
|
-
lastPageKey = pageId;
|
|
108
|
+
phaseOfPage.set(pageId, currentPhase);
|
|
109
|
+
currentPhaseCount += size;
|
|
44
110
|
}
|
|
45
|
-
return
|
|
111
|
+
return itemsInCatalogOrder.map((item) => phaseOfPage.get(`${item.groupKey}␟${item.pageKey}`));
|
|
46
112
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Div, Stack, Text } from "../../../ui";
|
|
2
|
+
import { Button, Div, GatedPrice, Stack, Text } from "../../../ui";
|
|
3
3
|
import { MediaImage } from "../../media/MediaImage";
|
|
4
4
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
5
5
|
const CLS_REMOVE_BTN = "self-start text-[var(--appkit-color-text-faint)] transition hover:text-error";
|
|
6
6
|
export function WishlistCard({ item, onRemove, onProductClick, }) {
|
|
7
7
|
return (_jsxs(Div, { layout: "flex", gap: "4", surface: "card", padding: "sm", children: [_jsx(Div, { role: onProductClick ? "button" : undefined, tabIndex: onProductClick ? 0 : undefined, onClick: onProductClick ? () => onProductClick(item) : undefined, onKeyDown: onProductClick
|
|
8
8
|
? (e) => e.key === "Enter" && onProductClick(item)
|
|
9
|
-
: undefined, className: `relative h-20 w-20 flex-shrink-0 overflow-hidden bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] ${onProductClick ? "cursor-pointer" : ""}`, rounded: "lg", children: item.productImage && (_jsx(MediaImage, { src: item.productImage, alt: item.productTitle ?? "", size: "thumbnail" })) }), _jsxs(Stack, { justify: "between", className: "flex-1", children: [_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: item.productTitle }), item.productPrice !== undefined && (_jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: formatCurrency(item.productPrice, item.productCurrency) }))] }), onRemove && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onRemove(item.id), "aria-label": "Remove from wishlist", className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
|
|
9
|
+
: undefined, className: `relative h-20 w-20 flex-shrink-0 overflow-hidden bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] ${onProductClick ? "cursor-pointer" : ""}`, rounded: "lg", children: item.productImage && (_jsx(MediaImage, { src: item.productImage, alt: item.productTitle ?? "", size: "thumbnail" })) }), _jsxs(Stack, { justify: "between", className: "flex-1", children: [_jsx(Text, { truncate: 2, weight: "medium", color: "primary", children: item.productTitle }), item.productPrice !== undefined && (_jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: _jsx(GatedPrice, { children: formatCurrency(item.productPrice, item.productCurrency) }) }))] }), onRemove && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onRemove(item.id), "aria-label": "Remove from wishlist", className: CLS_REMOVE_BTN, children: "\u2715" }))] }));
|
|
10
10
|
}
|
|
11
11
|
export function WishlistPage({ items, isLoading, onRemove, onProductClick, emptyLabel = "Your wishlist is empty", }) {
|
|
12
12
|
if (isLoading) {
|
package/dist/index.d.ts
CHANGED
|
@@ -341,6 +341,7 @@ export { blogPostJsonLd } from "./seo/index";
|
|
|
341
341
|
export { breadcrumbJsonLd } from "./seo/index";
|
|
342
342
|
export { createSeoConfig } from "./seo/index";
|
|
343
343
|
export { faqJsonLd } from "./seo/index";
|
|
344
|
+
export { gatedPriceWebPageJsonLd } from "./seo/index";
|
|
344
345
|
export { generateAuctionMetadata } from "./seo/index";
|
|
345
346
|
export { generateBlogMetadata } from "./seo/index";
|
|
346
347
|
export { generateCategoryMetadata } from "./seo/index";
|
|
@@ -3001,7 +3002,7 @@ export { Heading, Spinner, IconButton, Progress, IndeterminateProgress, Paginati
|
|
|
3001
3002
|
export { ModalFooter, UnsavedChangesModal, ResponsiveView, Textarea, Checkbox } from "./ui/index";
|
|
3002
3003
|
export { ActionRow } from "./ui/index";
|
|
3003
3004
|
export type { ActionRowProps, ActionRowAlign } from "./ui/index";
|
|
3004
|
-
export { ToastProvider, StarRating, StepperNav, ViewToggle, PriceDisplay, SummaryCard } from "./ui/index";
|
|
3005
|
+
export { ToastProvider, StarRating, StepperNav, ViewToggle, PriceDisplay, GatedPrice, PricesOnly, SummaryCard } from "./ui/index";
|
|
3005
3006
|
export { CountdownDisplay, ItemRow, HorizontalScroller } from "./ui/index";
|
|
3006
3007
|
export { CardBody, MenuTrigger, MenuContent, MenuSeparator, RoleBadge, SkipToMain } from "./ui/index";
|
|
3007
3008
|
export { Tabs, TabsList, TabsTrigger, TabsContent, PasswordStrengthIndicator, TablePagination } from "./ui/index";
|
package/dist/index.js
CHANGED
|
@@ -598,6 +598,9 @@ export { createSeoConfig } from "./seo/index";
|
|
|
598
598
|
// faqJsonLd - Shared export for faq json ld.
|
|
599
599
|
export { faqJsonLd } from "./seo/index";
|
|
600
600
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
601
|
+
// gatedPriceWebPageJsonLd - Shared export for gated price web page json ld.
|
|
602
|
+
export { gatedPriceWebPageJsonLd } from "./seo/index";
|
|
603
|
+
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
601
604
|
// generateAuctionMetadata - Helper for generate auction metadata.
|
|
602
605
|
export { generateAuctionMetadata } from "./seo/index";
|
|
603
606
|
// [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
|
|
@@ -5364,7 +5367,7 @@ export { Button } from "./ui/index";
|
|
|
5364
5367
|
export { Heading, Spinner, IconButton, Progress, IndeterminateProgress, Pagination, PageLoader } from "./ui/index";
|
|
5365
5368
|
export { ModalFooter, UnsavedChangesModal, ResponsiveView, Textarea, Checkbox } from "./ui/index";
|
|
5366
5369
|
export { ActionRow } from "./ui/index";
|
|
5367
|
-
export { ToastProvider, StarRating, StepperNav, ViewToggle, PriceDisplay, SummaryCard } from "./ui/index";
|
|
5370
|
+
export { ToastProvider, StarRating, StepperNav, ViewToggle, PriceDisplay, GatedPrice, PricesOnly, SummaryCard } from "./ui/index";
|
|
5368
5371
|
export { CountdownDisplay, ItemRow, HorizontalScroller } from "./ui/index";
|
|
5369
5372
|
export { CardBody, MenuTrigger, MenuContent, MenuSeparator, RoleBadge, SkipToMain } from "./ui/index";
|
|
5370
5373
|
export { Tabs, TabsList, TabsTrigger, TabsContent, PasswordStrengthIndicator, TablePagination } from "./ui/index";
|
|
@@ -19,4 +19,11 @@ export declare const RTDB_PATHS: {
|
|
|
19
19
|
readonly AUTH_EVENTS: "auth_events";
|
|
20
20
|
readonly PAYMENT_EVENTS: "payment_events";
|
|
21
21
|
readonly BULK_EVENTS: "bulk_events";
|
|
22
|
+
/**
|
|
23
|
+
* Tester-only. A one-line ping per email decision so the harness can poll
|
|
24
|
+
* instead of reading a real mailbox — see `send-recorder.ts` for why, and for
|
|
25
|
+
* the gate that keeps real users' mail out of it. Pruned by
|
|
26
|
+
* `cleanupRtdbEvents` alongside the other three.
|
|
27
|
+
*/
|
|
28
|
+
readonly EMAIL_EVENTS: "email_events";
|
|
22
29
|
};
|
|
@@ -19,4 +19,11 @@ export const RTDB_PATHS = {
|
|
|
19
19
|
AUTH_EVENTS: "auth_events",
|
|
20
20
|
PAYMENT_EVENTS: "payment_events",
|
|
21
21
|
BULK_EVENTS: "bulk_events",
|
|
22
|
+
/**
|
|
23
|
+
* Tester-only. A one-line ping per email decision so the harness can poll
|
|
24
|
+
* instead of reading a real mailbox — see `send-recorder.ts` for why, and for
|
|
25
|
+
* the gate that keeps real users' mail out of it. Pruned by
|
|
26
|
+
* `cleanupRtdbEvents` alongside the other three.
|
|
27
|
+
*/
|
|
28
|
+
EMAIL_EVENTS: "email_events",
|
|
22
29
|
};
|
|
@@ -19,7 +19,22 @@ const DEFAULT_ENDPOINTS = {
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
const SESSION_COOKIE = "__session";
|
|
21
21
|
const SESSION_ID_COOKIE = "__session_id";
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How often a VISIBLE tab pings session activity and re-reads RBAC flags.
|
|
24
|
+
*
|
|
25
|
+
* Each tick costs two function invocations (`sessionActivity`, then the
|
|
26
|
+
* piggybacked profile re-fetch below), per open tab, wall-clock rather than
|
|
27
|
+
* activity-gated. At the original 5 minutes that was 24 invocations/hour for
|
|
28
|
+
* every tab any signed-in user had left open, and it contributed to the Vercel
|
|
29
|
+
* overage that paused the account (2026-09-14).
|
|
30
|
+
*
|
|
31
|
+
* 15 minutes is safe because it is a BACKSTOP, not the primary freshness
|
|
32
|
+
* mechanism: `RoleGuard` calls `refreshUser()` on every mount of a role-gated
|
|
33
|
+
* layout, so navigating into /admin, /store or /user re-reads the flags
|
|
34
|
+
* immediately, and the visibility handler below re-reads them the moment a
|
|
35
|
+
* backgrounded tab is looked at again.
|
|
36
|
+
*/
|
|
37
|
+
const ACTIVITY_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
23
38
|
// ---------------------------------------------------------------------------
|
|
24
39
|
// Context
|
|
25
40
|
// ---------------------------------------------------------------------------
|
|
@@ -166,6 +181,14 @@ export function SessionProvider({ children, initialUser, endpoints: endpointOver
|
|
|
166
181
|
const currentSessionId = getSessionIdFromCookie();
|
|
167
182
|
if (!currentSessionId || !user)
|
|
168
183
|
return;
|
|
184
|
+
// A hidden tab is not "activity", so reporting it as such is both wasteful
|
|
185
|
+
// and wrong — an abandoned background tab used to keep both requests below
|
|
186
|
+
// firing indefinitely. The visibility handler re-runs this immediately when
|
|
187
|
+
// the tab is looked at again, so nothing is merely deferred into staleness.
|
|
188
|
+
if (typeof document !== "undefined" &&
|
|
189
|
+
document.visibilityState === "hidden") {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
169
192
|
try {
|
|
170
193
|
await apiClient.post(ep.sessionActivity, {
|
|
171
194
|
sessionId: currentSessionId,
|
|
@@ -203,6 +226,22 @@ export function SessionProvider({ children, initialUser, endpoints: endpointOver
|
|
|
203
226
|
useEffect(() => {
|
|
204
227
|
updateSessionActivityRef.current = updateSessionActivity;
|
|
205
228
|
}, [updateSessionActivity]);
|
|
229
|
+
// Re-read session activity and RBAC flags the moment a backgrounded tab is
|
|
230
|
+
// looked at again. This is what lets the interval above be 15 minutes and the
|
|
231
|
+
// hidden-tab skip be safe: the user-visible freshness guarantee is now "fresh
|
|
232
|
+
// whenever you are actually looking at it", which is strictly better than the
|
|
233
|
+
// old fixed 5-minute tick that fired whether anyone was there or not.
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (typeof document === "undefined")
|
|
236
|
+
return;
|
|
237
|
+
const onVisible = () => {
|
|
238
|
+
if (document.visibilityState === "visible") {
|
|
239
|
+
void updateSessionActivityRef.current?.();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
243
|
+
return () => document.removeEventListener("visibilitychange", onVisible);
|
|
244
|
+
}, []);
|
|
206
245
|
// -----------------------------------------------------------------------
|
|
207
246
|
// Refresh helpers
|
|
208
247
|
// -----------------------------------------------------------------------
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface CanSeePricesState {
|
|
2
|
+
/** The viewer is signed in and may be shown money amounts. */
|
|
3
|
+
canSeePrices: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The session has not resolved yet. Render a neutral placeholder while this
|
|
6
|
+
* is true — NEVER the sign-in prompt. See the note below.
|
|
7
|
+
*/
|
|
8
|
+
isAuthResolving: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* May the current viewer be shown money amounts on a public listing surface?
|
|
12
|
+
*
|
|
13
|
+
* Prices, current bids, deposits and per-entry amounts are hidden from
|
|
14
|
+
* signed-out visitors so the catalogue cannot be read at scale without an
|
|
15
|
+
* account. Signed-in users see everything exactly as before.
|
|
16
|
+
*
|
|
17
|
+
* ## Why there are THREE states, not two
|
|
18
|
+
*
|
|
19
|
+
* `SessionProvider` is mounted `initialUser={null}` because the root locale
|
|
20
|
+
* layout must stay static (Root Cause #82), so `loading` starts `true` on every
|
|
21
|
+
* hard load — for signed-in users too, until Firebase persistence resolves and
|
|
22
|
+
* `/api/user/profile` returns. A naive `!user` test therefore renders
|
|
23
|
+
* "Sign in to see price" at an already-signed-in user for several hundred
|
|
24
|
+
* milliseconds on every page load.
|
|
25
|
+
*
|
|
26
|
+
* So: `isAuthResolving` means "don't know yet" and must render a neutral
|
|
27
|
+
* placeholder. Only `canSeePrices === false && isAuthResolving === false` is a
|
|
28
|
+
* confirmed guest.
|
|
29
|
+
*
|
|
30
|
+
* ## Why `useOptionalSession`
|
|
31
|
+
*
|
|
32
|
+
* This is library code that ADJUSTS for the viewer rather than requiring one.
|
|
33
|
+
* `useSession()` throws without a provider, which would turn an optional
|
|
34
|
+
* refinement into a breaking contract for every consumer (Root Cause #20). With
|
|
35
|
+
* no provider we fail CLOSED — no session means no prices.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useCanSeePrices(): CanSeePricesState;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useOptionalSession } from "../contexts/SessionContext";
|
|
3
|
+
/**
|
|
4
|
+
* May the current viewer be shown money amounts on a public listing surface?
|
|
5
|
+
*
|
|
6
|
+
* Prices, current bids, deposits and per-entry amounts are hidden from
|
|
7
|
+
* signed-out visitors so the catalogue cannot be read at scale without an
|
|
8
|
+
* account. Signed-in users see everything exactly as before.
|
|
9
|
+
*
|
|
10
|
+
* ## Why there are THREE states, not two
|
|
11
|
+
*
|
|
12
|
+
* `SessionProvider` is mounted `initialUser={null}` because the root locale
|
|
13
|
+
* layout must stay static (Root Cause #82), so `loading` starts `true` on every
|
|
14
|
+
* hard load — for signed-in users too, until Firebase persistence resolves and
|
|
15
|
+
* `/api/user/profile` returns. A naive `!user` test therefore renders
|
|
16
|
+
* "Sign in to see price" at an already-signed-in user for several hundred
|
|
17
|
+
* milliseconds on every page load.
|
|
18
|
+
*
|
|
19
|
+
* So: `isAuthResolving` means "don't know yet" and must render a neutral
|
|
20
|
+
* placeholder. Only `canSeePrices === false && isAuthResolving === false` is a
|
|
21
|
+
* confirmed guest.
|
|
22
|
+
*
|
|
23
|
+
* ## Why `useOptionalSession`
|
|
24
|
+
*
|
|
25
|
+
* This is library code that ADJUSTS for the viewer rather than requiring one.
|
|
26
|
+
* `useSession()` throws without a provider, which would turn an optional
|
|
27
|
+
* refinement into a breaking contract for every consumer (Root Cause #20). With
|
|
28
|
+
* no provider we fail CLOSED — no session means no prices.
|
|
29
|
+
*/
|
|
30
|
+
export function useCanSeePrices() {
|
|
31
|
+
const session = useOptionalSession();
|
|
32
|
+
// No provider: fail closed, and don't claim to be mid-resolution — there is
|
|
33
|
+
// nothing in flight that will ever change the answer.
|
|
34
|
+
if (!session)
|
|
35
|
+
return { canSeePrices: false, isAuthResolving: false };
|
|
36
|
+
return {
|
|
37
|
+
canSeePrices: Boolean(session.user?.uid),
|
|
38
|
+
isAuthResolving: session.loading,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* addresses live entirely in store-addresses-seed-data.ts (merged in at the API route
|
|
4
4
|
* level) — this file only carries ownerType:"user" entries now, to avoid seeding two
|
|
5
5
|
* overlapping sources of store address data.
|
|
6
|
-
* WHAT: Exports
|
|
6
|
+
* WHAT: Exports 4 user addresses. Top-level collection (SB-UNI-A) with ownerType discriminator.
|
|
7
7
|
* PII encrypted via HMAC blind indices (emailIndex, phoneIndex). Composite indexes
|
|
8
8
|
* (ownerType, ownerId, createdAt desc) + (ownerType, ownerId, isDefault).
|
|
9
9
|
*
|
|
10
10
|
* EXPORTS:
|
|
11
|
-
* addressesSeedData — Array of
|
|
11
|
+
* addressesSeedData — Array of 4 user address documents
|
|
12
12
|
*
|
|
13
13
|
* @tag domain:addresses,shipping
|
|
14
14
|
* @tag layer:seed
|
|
@@ -74,5 +74,39 @@ const _rawAddressesSeedData = [
|
|
|
74
74
|
createdAt: daysAgo(365),
|
|
75
75
|
updatedAt: daysAgo(5),
|
|
76
76
|
},
|
|
77
|
+
/*
|
|
78
|
+
* Automated checklist runner — delivery address.
|
|
79
|
+
*
|
|
80
|
+
* 🛑 THIS ROW IS WHY CHECKOUT CAN BE TESTED AT ALL.
|
|
81
|
+
*
|
|
82
|
+
* `user-claude-tester` had no address, so /checkout stopped dead at "Step 1 of 3:
|
|
83
|
+
* Shipping Address — No saved addresses yet." Every order, payment, coupon-at-placement,
|
|
84
|
+
* offer-checkout and forfeited-bid case died there: 117 blocked cases, 27% of everything
|
|
85
|
+
* the run could not test, and the single largest blocker by a wide margin (measured,
|
|
86
|
+
* run 1789300124915).
|
|
87
|
+
*
|
|
88
|
+
* It has to be SEEDED rather than created by the runner mid-run. `addresses` is PRESERVE
|
|
89
|
+
* tier in tester/scripts/lib/collections.mjs — teardown deliberately never touches it,
|
|
90
|
+
* because real people's saved addresses live there — so a row the runner created at
|
|
91
|
+
* runtime would survive every future run with nothing owning its cleanup. Seeded, it is
|
|
92
|
+
* idempotent: `appkit-seed load` upserts this exact id, and re-running changes nothing.
|
|
93
|
+
*/
|
|
94
|
+
{
|
|
95
|
+
id: "addr-claude-tester-home",
|
|
96
|
+
ownerType: "user",
|
|
97
|
+
ownerId: "user-claude-tester",
|
|
98
|
+
label: "Home",
|
|
99
|
+
fullName: "Claude (automated)",
|
|
100
|
+
phone: "+91-99999-00019",
|
|
101
|
+
addressLine1: "1 Automation Street",
|
|
102
|
+
addressLine2: "Koramangala",
|
|
103
|
+
city: "Bengaluru",
|
|
104
|
+
state: "Karnataka",
|
|
105
|
+
postalCode: "560034",
|
|
106
|
+
country: "India",
|
|
107
|
+
isDefault: true,
|
|
108
|
+
createdAt: daysAgo(1),
|
|
109
|
+
updatedAt: daysAgo(1),
|
|
110
|
+
},
|
|
77
111
|
];
|
|
78
112
|
export const addressesSeedData = _rawAddressesSeedData;
|
|
@@ -546,8 +546,16 @@ export const usersSeedData = [
|
|
|
546
546
|
isBot: true,
|
|
547
547
|
emailVerified: true,
|
|
548
548
|
disabled: false,
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
// 🛑 This MUST name a store whose own `ownerId` is this uid. It used to say
|
|
550
|
+
// "store-tester-qa-seller", which is owned by `user-tester-qa` — so the app, which
|
|
551
|
+
// resolves sellers by `ownerId`, answered every /api/store/* call with 403
|
|
552
|
+
// "No store found for this account" while the client, reading `role` and `storeId`
|
|
553
|
+
// off this document, rendered the entire Store Panel over it. 36 checklist cases were
|
|
554
|
+
// blocked by that one-way pointer (measured, run 1789300124915).
|
|
555
|
+
// See stores-tester-seed-data.ts for why the shared sandbox could not simply be
|
|
556
|
+
// repointed at this account instead.
|
|
557
|
+
storeId: "store-claude-tester",
|
|
558
|
+
storeSlug: "store-claude-tester",
|
|
551
559
|
storeStatus: "approved",
|
|
552
560
|
publicProfile: {
|
|
553
561
|
isPublic: false,
|
package/dist/seo/json-ld.d.ts
CHANGED
|
@@ -84,6 +84,27 @@ export interface BlogPostJsonLdInput {
|
|
|
84
84
|
metaTitle?: string;
|
|
85
85
|
metaDescription?: string;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Declares that this page's price is behind a sign-in gate.
|
|
89
|
+
*
|
|
90
|
+
* Prices are hidden from signed-out visitors, and Googlebot crawls signed out —
|
|
91
|
+
* so the crawler sees "Sign in to see price" while the Product JSON-LD beside it
|
|
92
|
+
* declares a real `offers.price`. Left undeclared, that is a structured-data
|
|
93
|
+
* mismatch: Google requires marked-up content to be visible to the user, and an
|
|
94
|
+
* unexplained gap can cost the rich result.
|
|
95
|
+
*
|
|
96
|
+
* `isAccessibleForFree: false` + `hasPart`/`cssSelector` is the supported way to
|
|
97
|
+
* say "the markup is richer than what an anonymous visitor sees". It lives on a
|
|
98
|
+
* `WebPage` node rather than on the Product, because `isAccessibleForFree` is a
|
|
99
|
+
* `CreativeWork` property and `Product` is not a `CreativeWork`.
|
|
100
|
+
*
|
|
101
|
+
* Serving the price to crawler user-agents but not to people would be cloaking,
|
|
102
|
+
* which is a harder violation; this declares the gate instead of hiding it.
|
|
103
|
+
*
|
|
104
|
+
* 🛑 Emit this on EVERY page that emits `offers.price`. The audit enforces the
|
|
105
|
+
* pairing precisely so nobody removes one half later.
|
|
106
|
+
*/
|
|
107
|
+
export declare function gatedPriceWebPageJsonLd(path: string): Record<string, JsonLdValue>;
|
|
87
108
|
export declare function productJsonLd(product: ProductJsonLdInput): Record<string, JsonLdValue>;
|
|
88
109
|
export declare function reviewJsonLd(review: ReviewJsonLdInput): Record<string, JsonLdValue>;
|
|
89
110
|
export declare function aggregateRatingJsonLd(product: Pick<ProductJsonLdInput, "title" | "slug">, stats: {
|
package/dist/seo/json-ld.js
CHANGED
|
@@ -39,6 +39,51 @@ function absoluteUrl(value, base = SITE_URL) {
|
|
|
39
39
|
import { ProductStatusValues } from "../features/products/schemas";
|
|
40
40
|
import { getDefaultCurrency } from "../core/baseline-resolver";
|
|
41
41
|
// --- Helpers -----------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* The CSS selector the price gate renders in every state.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 Must equal `GATED_PRICE_CLASS` in `ui/components/GatedPrice.tsx`. It is
|
|
46
|
+
* duplicated as a literal here rather than imported because `seo/` is reached
|
|
47
|
+
* from server-only metadata paths and must not pull in a `"use client"` module
|
|
48
|
+
* (Root Cause #76). `audit-guest-price-leak` cross-checks the two.
|
|
49
|
+
*/
|
|
50
|
+
const GATED_PRICE_SELECTOR = ".appkit-gated-price";
|
|
51
|
+
/**
|
|
52
|
+
* Declares that this page's price is behind a sign-in gate.
|
|
53
|
+
*
|
|
54
|
+
* Prices are hidden from signed-out visitors, and Googlebot crawls signed out —
|
|
55
|
+
* so the crawler sees "Sign in to see price" while the Product JSON-LD beside it
|
|
56
|
+
* declares a real `offers.price`. Left undeclared, that is a structured-data
|
|
57
|
+
* mismatch: Google requires marked-up content to be visible to the user, and an
|
|
58
|
+
* unexplained gap can cost the rich result.
|
|
59
|
+
*
|
|
60
|
+
* `isAccessibleForFree: false` + `hasPart`/`cssSelector` is the supported way to
|
|
61
|
+
* say "the markup is richer than what an anonymous visitor sees". It lives on a
|
|
62
|
+
* `WebPage` node rather than on the Product, because `isAccessibleForFree` is a
|
|
63
|
+
* `CreativeWork` property and `Product` is not a `CreativeWork`.
|
|
64
|
+
*
|
|
65
|
+
* Serving the price to crawler user-agents but not to people would be cloaking,
|
|
66
|
+
* which is a harder violation; this declares the gate instead of hiding it.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Emit this on EVERY page that emits `offers.price`. The audit enforces the
|
|
69
|
+
* pairing precisely so nobody removes one half later.
|
|
70
|
+
*/
|
|
71
|
+
export function gatedPriceWebPageJsonLd(path) {
|
|
72
|
+
return {
|
|
73
|
+
"@context": "https://schema.org",
|
|
74
|
+
"@type": "WebPage",
|
|
75
|
+
// Absolutised against the ONE canonical host, never a second definition of
|
|
76
|
+
// it (Root Cause #81) — a WebPage node naming a host the sitemap does not
|
|
77
|
+
// advertise is the same split-identity bug in miniature.
|
|
78
|
+
url: absoluteUrl(path),
|
|
79
|
+
isAccessibleForFree: false,
|
|
80
|
+
hasPart: {
|
|
81
|
+
"@type": "WebPageElement",
|
|
82
|
+
isAccessibleForFree: false,
|
|
83
|
+
cssSelector: GATED_PRICE_SELECTOR,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
42
87
|
export function productJsonLd(product) {
|
|
43
88
|
const url = `${SITE_URL}/products/${product.slug}`;
|
|
44
89
|
// Absolutise, then de-duplicate: `mainImage` is very often also `images[0]`,
|
package/dist/server.d.ts
CHANGED
|
@@ -556,6 +556,9 @@ export { getDigitalCodeForDetail } from "./_internal/server/features/digital-cod
|
|
|
556
556
|
export { toClientDigitalCode, type DigitalCodeClientShape } from "./_internal/server/features/digital-code/adapters";
|
|
557
557
|
export { buildDigitalCodeMetadata, type DigitalCodeMetadataOptions } from "./_internal/server/features/digital-code/metadata";
|
|
558
558
|
export { renderDigitalCodeOg, renderDigitalCodeOgImage, renderDigitalCodeOgFromDoc, type DigitalCodeOgData } from "./_internal/server/features/digital-code/og";
|
|
559
|
+
export { addPoolEntries, listPoolEntries, deletePoolEntry, recountPool, assertCanUploadContentKind, DIGITAL_CONTENT_PREFIX, type AddPoolEntryInput, } from "./_internal/server/features/digital-code/pool";
|
|
560
|
+
export { signDigitalContentUpload, finalizeDigitalContentUpload, contentKindForMime, type SignedDigitalContent, type FinalizedDigitalContent, } from "./_internal/server/features/digital-code/upload";
|
|
561
|
+
export { resolveCodeAssetForOrder, codeAssetHeaders, CODE_DOWNLOAD_STATUSES, type CodeAssetResolution, } from "./_internal/server/features/digital-code/download";
|
|
559
562
|
export { getLiveItemForDetail } from "./_internal/server/features/live/data";
|
|
560
563
|
export { toClientLiveItem, type LiveItemClientShape } from "./_internal/server/features/live/adapters";
|
|
561
564
|
export { buildLiveItemMetadata, type LiveItemMetadataOptions } from "./_internal/server/features/live/metadata";
|
package/dist/server.js
CHANGED
|
@@ -1444,6 +1444,18 @@ export { getDigitalCodeForDetail } from "./_internal/server/features/digital-cod
|
|
|
1444
1444
|
export { toClientDigitalCode } from "./_internal/server/features/digital-code/adapters";
|
|
1445
1445
|
export { buildDigitalCodeMetadata } from "./_internal/server/features/digital-code/metadata";
|
|
1446
1446
|
export { renderDigitalCodeOg, renderDigitalCodeOgImage, renderDigitalCodeOgFromDoc } from "./_internal/server/features/digital-code/og";
|
|
1447
|
+
/*
|
|
1448
|
+
* The digital-CONTENT pool: the half of this feature that had no writer at all
|
|
1449
|
+
* until 2026-09-14, so every digital-code purchase delivered nothing.
|
|
1450
|
+
*
|
|
1451
|
+
* Server-only, and that is structural rather than tidy — `pool` and `download`
|
|
1452
|
+
* both reach `getAdminDb`/`getAdminStorage`, and `upload` mints signed URLs.
|
|
1453
|
+
* None of it may appear in `index.ts` or `client.ts` (the Turbopack
|
|
1454
|
+
* client-bundle trap, Root Cause #6/#24).
|
|
1455
|
+
*/
|
|
1456
|
+
export { addPoolEntries, listPoolEntries, deletePoolEntry, recountPool, assertCanUploadContentKind, DIGITAL_CONTENT_PREFIX, } from "./_internal/server/features/digital-code/pool";
|
|
1457
|
+
export { signDigitalContentUpload, finalizeDigitalContentUpload, contentKindForMime, } from "./_internal/server/features/digital-code/upload";
|
|
1458
|
+
export { resolveCodeAssetForOrder, codeAssetHeaders, CODE_DOWNLOAD_STATUSES, } from "./_internal/server/features/digital-code/download";
|
|
1447
1459
|
// ── Live-item listing — data + adapters + metadata + OG ──────────────────────
|
|
1448
1460
|
export { getLiveItemForDetail } from "./_internal/server/features/live/data";
|
|
1449
1461
|
export { toClientLiveItem } from "./_internal/server/features/live/adapters";
|