@mohasinac/appkit 4.55.0-next.0 → 4.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/.build-stamp.json +5 -5
  2. package/dist/_internal/server/features/checkout/actions.js +61 -2
  3. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  4. package/dist/_internal/shared/features/checkout/config.js +1 -1
  5. package/dist/_internal/shared/features/site-settings/fees.d.ts +1 -1
  6. package/dist/_internal/shared/features/site-settings/fees.js +1 -1
  7. package/dist/_internal/shared/fees/calculator.d.ts +35 -3
  8. package/dist/_internal/shared/fees/calculator.js +34 -4
  9. package/dist/errors/error-codes.d.ts +15 -0
  10. package/dist/errors/error-codes.js +15 -0
  11. package/dist/errors/index.d.ts +1 -1
  12. package/dist/errors/index.js +1 -1
  13. package/dist/features/account/components/UserBidsView.js +4 -2
  14. package/dist/features/admin/components/AdminSiteSettingsView.js +18 -2
  15. package/dist/features/admin/schemas/firestore.d.ts +37 -2
  16. package/dist/features/admin/schemas/firestore.js +12 -1
  17. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  18. package/dist/features/checkout/schemas/firestore.d.ts +12 -1
  19. package/dist/features/layout/NavbarLayout.js +139 -32
  20. package/dist/features/media/messages/en.json +67 -67
  21. package/dist/features/orders/schemas/firestore.d.ts +1 -1
  22. package/dist/features/products/components/ProductDetailPageView.js +2 -25
  23. package/dist/features/products/components/ProductOwnerQuickActions.d.ts +11 -0
  24. package/dist/features/products/components/ProductOwnerQuickActions.js +120 -0
  25. package/dist/features/products/components/ProductsIndexListing.js +35 -5
  26. package/dist/features/products/components/ProductsIndexPageView.js +40 -7
  27. package/dist/providers/shipping-shiprocket/index.d.ts +13 -4
  28. package/dist/providers/shipping-shiprocket/index.js +34 -10
  29. package/dist/security/csp.d.ts +1 -16
  30. package/dist/security/csp.js +3 -5
  31. package/dist/seed/faq-seed-data.js +1 -1
  32. package/dist/seed/site-settings-seed-data.js +1 -1
  33. package/dist/seo/json-ld.d.ts +8 -1
  34. package/dist/seo/json-ld.js +39 -2
  35. package/dist/styles.css +382 -537
  36. package/dist/tailwind-utilities.css +1 -1
  37. package/dist/tokens/themes/index.d.ts +1 -3
  38. package/dist/tokens/themes/index.js +1 -11
  39. package/dist/tokens/tokens.css +0 -155
  40. package/dist/ui/components/GatedPrice.style.css +43 -43
  41. package/dist/ui/components/QuickCreateModal.style.css +78 -78
  42. package/dist/ui/forms/FormShell.style.css +260 -260
  43. package/package.json +1 -1
  44. package/dist/tokens/themes/technical-dark.d.ts +0 -30
  45. package/dist/tokens/themes/technical-dark.js +0 -103
  46. package/dist/tokens/themes/technical-light.d.ts +0 -22
  47. package/dist/tokens/themes/technical-light.js +0 -89
@@ -10,6 +10,9 @@ import { GENERIC_PRODUCT_LISTING_TYPES } from "../constants/listing-tabs.js";
10
10
  import { pluginFor } from "../../../_internal/shared/listing-types/_registry.js";
11
11
  import { listPublicProducts, parsePublicProductParams, defaultAvailabilityForListingTypes, } from "../../../_internal/server/features/products/list-public.js";
12
12
  import { parseSelectedListingTypes } from "../utils/listing-type.js";
13
+ import { isListingTypeEnabled } from "../../../_internal/shared/listing-types/feature-flags.js";
14
+ import { siteSettingsRepository } from "../../../repositories/index.js";
15
+ import { safeRead } from "../../../errors/safe-read.js";
13
16
  const DEFAULT_PAGE_SIZE = 24;
14
17
  const DEFAULT_SORT = sortBy(PRODUCT_FIELDS.CREATED_AT);
15
18
  export async function ProductsIndexPageView({ searchParams = {} }) {
@@ -47,11 +50,41 @@ export async function ProductsIndexPageView({ searchParams = {} }) {
47
50
  const effectiveTypes = selectedTypes.length > 0 ? selectedTypes : GENERIC_PRODUCT_LISTING_TYPES;
48
51
  // See ArtStickersListView — one shared filter implementation with
49
52
  // /api/products, so SSR and the client refetch agree by construction.
50
- const products = await listPublicProducts(parsePublicProductParams(searchParams, {
51
- listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
52
- pageSize: DEFAULT_PAGE_SIZE,
53
- sorts: DEFAULT_SORT,
54
- ...defaultAvailabilityForListingTypes(effectiveTypes),
55
- }));
56
- return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), _jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
53
+ const [products, settings] = await Promise.all([
54
+ listPublicProducts(parsePublicProductParams(searchParams, {
55
+ listingTypes: GENERIC_PRODUCT_LISTING_TYPES,
56
+ pageSize: DEFAULT_PAGE_SIZE,
57
+ sorts: DEFAULT_SORT,
58
+ ...defaultAvailabilityForListingTypes(effectiveTypes),
59
+ })),
60
+ /*
61
+ * Read in parallel with the listing — the banner below is the only
62
+ * consumer, and serialising a settings read in front of the page's main
63
+ * query would spend a round trip against Rule #6's budget for one link.
64
+ *
65
+ * 🛑 `safeRead`, so a settings failure hides the banner rather than
66
+ * failing the catalogue. Auctions have their own nav entry and their own
67
+ * page; the banner is a shortcut, and a shortcut is the correct thing to
68
+ * lose when its gate cannot be evaluated.
69
+ */
70
+ safeRead(() => siteSettingsRepository.findById("global"), {
71
+ route: "/products",
72
+ key: "products.siteSettings",
73
+ fallback: null,
74
+ }),
75
+ ]);
76
+ /*
77
+ * 🛑 Auctions are a LISTING TYPE an admin can switch off, and this banner
78
+ * advertised them unconditionally.
79
+ *
80
+ * Every other auction entry point already respects the flag — the navbar
81
+ * child (filtered by key in `LayoutShellClient`), the footer link
82
+ * (`FOOTER_LINK_LISTING_TYPES`), the store tabs (`tabs.ts`), the
83
+ * category/brand tabs, and the homepage section (`gatedListing`). This was
84
+ * the one surface left promoting a page that renders nothing on a site with
85
+ * auctions disabled, which is Root Cause #37's dead-link shape reached from
86
+ * the other direction.
87
+ */
88
+ const showAuctionsBanner = isListingTypeEnabled("auction", settings);
89
+ return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Products" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Discover amazing products and deals" }), showAuctionsBanner && (_jsx(Div, { className: "mt-3", children: _jsx(TextLink, { variant: "bare", href: String(ROUTES.PUBLIC.AUCTIONS), rounded: "full", paddingX: "sm", paddingY: "2xs", size: "xs", weight: "medium", layout: "inline-flex", align: "center", gap: "sm", className: "border border-primary/30 bg-primary/10 text-primary-700 dark:text-primary-400 hover:bg-primary/15 transition-colors", children: "\uD83C\uDFF7\uFE0F Looking for unique deals? Browse Auctions \u2192" }) }))] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing, { initialData: products ?? undefined }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
57
90
  }
@@ -12,10 +12,19 @@
12
12
  * `loginResponse` requires — the rest are ignored, as Zod ignores unknown
13
13
  * keys by default.
14
14
  *
15
- * 🛑 NOT VERIFIED: every other endpoint. Order creation, AWB assignment,
16
- * tracking, serviceability and label generation all MUTATE or bill against
17
- * a real carrier account, so none was called. Their field names below are
18
- * claims taken from the published docs, not facts read off a payload.
15
+ * ✅ ALSO VERIFIED (2026-10-20): `GET /courier/serviceability/` from pickup
16
+ * `413001`. HTTP 200, and `data.available_courier_companies` came back with
17
+ * 10-11 entries for Mumbai, Bengaluru and Delhi carrying `courier_name`,
18
+ * `rate` and `estimated_delivery_days` — so `serviceabilityResponse` below
19
+ * is read off a real payload, not inferred. It is a read-only quote: no
20
+ * shipment is created and nothing is booked.
21
+ *
22
+ * 🛑 NOT VERIFIED: order creation, AWB assignment, tracking and label
23
+ * generation. Each MUTATES or bills a real carrier account — creating an
24
+ * order can schedule a pickup, an AWB can consume an allocation, a cancel
25
+ * may still bill — so none was called, and none is discoverable without
26
+ * incurring it. Their field names below are claims from the published docs,
27
+ * not facts read off a payload.
19
28
  *
20
29
  * That asymmetry is the reason every response is parsed through Zod rather
21
30
  * than cast: an unverified contract that is wrong surfaces as a named error at
@@ -12,10 +12,19 @@
12
12
  * `loginResponse` requires — the rest are ignored, as Zod ignores unknown
13
13
  * keys by default.
14
14
  *
15
- * 🛑 NOT VERIFIED: every other endpoint. Order creation, AWB assignment,
16
- * tracking, serviceability and label generation all MUTATE or bill against
17
- * a real carrier account, so none was called. Their field names below are
18
- * claims taken from the published docs, not facts read off a payload.
15
+ * ✅ ALSO VERIFIED (2026-10-20): `GET /courier/serviceability/` from pickup
16
+ * `413001`. HTTP 200, and `data.available_courier_companies` came back with
17
+ * 10-11 entries for Mumbai, Bengaluru and Delhi carrying `courier_name`,
18
+ * `rate` and `estimated_delivery_days` — so `serviceabilityResponse` below
19
+ * is read off a real payload, not inferred. It is a read-only quote: no
20
+ * shipment is created and nothing is booked.
21
+ *
22
+ * 🛑 NOT VERIFIED: order creation, AWB assignment, tracking and label
23
+ * generation. Each MUTATES or bills a real carrier account — creating an
24
+ * order can schedule a pickup, an AWB can consume an allocation, a cancel
25
+ * may still bill — so none was called, and none is discoverable without
26
+ * incurring it. Their field names below are claims from the published docs,
27
+ * not facts read off a payload.
19
28
  *
20
29
  * That asymmetry is the reason every response is parsed through Zod rather
21
30
  * than cast: an unverified contract that is wrong surfaces as a named error at
@@ -312,15 +321,30 @@ export class ShiprocketShippingProvider extends IShippingProvider {
312
321
  }
313
322
  async checkServiceability(pincode, weight) {
314
323
  /*
315
- * 🛑 `pickup_postcode` is required by their API and we do not have it on
316
- * this contract. Taken from the registered pickup location's name is NOT
317
- * possible — it is a nickname, not a pincode — so this returns
318
- * "unserviceable" rather than guessing a origin pincode, which would
319
- * produce confidently wrong rates.
324
+ * `pickup_postcode` is required by their API and comes from
325
+ * `SHIPROCKET_PICKUP_PINCODE`.
326
+ *
327
+ * 🛑 This comment used to say "we do not have it on this contract", which
328
+ * was wrong and cost the whole method: the consumer's `appkit.config.js →
329
+ * business.registeredAddress` has carried `postalCode` all along. Nothing
330
+ * set the var, so every call returned `{ isServiceable: false }` WITHOUT
331
+ * calling the API — checkout would have told every buyer we cannot ship to
332
+ * them, for every destination. A quote that is silently always "no" is
333
+ * indistinguishable from a carrier with no coverage, which is why it had no
334
+ * symptom. Verified live once the var was populated: 10-11 couriers quoted
335
+ * from 413001 to Mumbai, Bengaluru and Delhi.
336
+ *
337
+ * 🛑 appkit has no reader for a consumer config file, so the DEFAULT is the
338
+ * consumer's job: `src/providers.config.ts` populates this var from the
339
+ * business block at boot. Deliberately not threaded through
340
+ * `configurePlatformSupplier` — that seam's own 🛑 says it "is NOT a cache
341
+ * of site settings and must not become one"; it answers *who the invoice
342
+ * supplier is*, and a carrier pickup field there is mission creep on a seam
343
+ * with one stated purpose. One var, one reader, no precedence rule.
320
344
  */
321
345
  const origin = process.env.SHIPROCKET_PICKUP_PINCODE?.trim();
322
346
  if (!origin) {
323
- serverLogger.warn("shiprocket: SHIPROCKET_PICKUP_PINCODE unset — serviceability cannot be checked", { pincode });
347
+ serverLogger.warn("shiprocket: no pickup postcode — set business.registeredAddress.postalCode in appkit.config.js, or SHIPROCKET_PICKUP_PINCODE directly", { pincode });
324
348
  return { isServiceable: false, couriers: [] };
325
349
  }
326
350
  try {
@@ -12,20 +12,5 @@
12
12
  */
13
13
  /** Generate a 16-byte base64 nonce for a single request. */
14
14
  export declare function generateNonce(): string;
15
- export interface BuildCSPOptions {
16
- /**
17
- * Add `'wasm-unsafe-eval'` to `script-src`, permitting
18
- * `WebAssembly.compile`/`instantiate` from bytes.
19
- *
20
- * Required by any page that compiles WebAssembly at runtime — without it the
21
- * production policy (`script-src 'self' 'nonce-…'`) blocks compilation
22
- * outright, because WebAssembly compilation counts as script evaluation.
23
- *
24
- * Narrower than `'unsafe-eval'` on purpose: it unblocks WASM only and still
25
- * forbids `eval` and `new Function` for JavaScript. Default `false`, so
26
- * existing consumers are unaffected.
27
- */
28
- wasm?: boolean;
29
- }
30
15
  /** Build the full CSP header value for the given nonce. */
31
- export declare function buildCSP(nonce: string, options?: BuildCSPOptions): string;
16
+ export declare function buildCSP(nonce: string): string;
@@ -17,15 +17,13 @@ export function generateNonce() {
17
17
  return btoa(String.fromCharCode(...bytes));
18
18
  }
19
19
  /** Build the full CSP header value for the given nonce. */
20
- export function buildCSP(nonce, options = {}) {
20
+ export function buildCSP(nonce) {
21
21
  const isDev = process.env.NODE_ENV === "development";
22
- const wasm = options.wasm === true ? " 'wasm-unsafe-eval'" : "";
23
22
  return [
24
23
  "default-src 'self'",
25
24
  isDev
26
- ? // 'unsafe-eval' already subsumes 'wasm-unsafe-eval' in dev.
27
- "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
28
- : `script-src 'self' 'nonce-${nonce}'${wasm}`,
25
+ ? "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
26
+ : `script-src 'self' 'nonce-${nonce}'`,
29
27
  "style-src 'self' 'unsafe-inline'",
30
28
  "img-src 'self' data: https: blob:",
31
29
  "font-src 'self' data:",
@@ -525,7 +525,7 @@ const faqSeedRecords = [
525
525
  {
526
526
  id: "faq-emi-installments",
527
527
  question: "Can I pay in installments (EMI)?",
528
- answer: html(`<p>Yes — on eligible orders. If the seller has EMI enabled and your order with them meets the minimum order value, "Pay in EMI" appears as a payment option at checkout. Choose a tenure of 2–6 months, pay a small token amount upfront, and the remaining balance plus a small monthly surcharge is billed automatically in equal installments each month. Your order ships as soon as the token payment is confirmed — you don't wait for the full amount to be paid.</p>`),
528
+ answer: html(`<p>Yes — on eligible orders. If the seller has EMI enabled and your order with them meets the minimum order value, "Pay in EMI" appears as a payment option at checkout. Choose a tenure of 2–6 months, pay a small token amount upfront, and the remaining balance plus a small monthly surcharge is billed automatically in equal installments each month. Your order ships once the EMI is fully paid — the token confirms your commitment and secures the item, it is not a dispatch trigger. A seller may opt an individual listing into earlier dispatch, and that is stated on the listing.</p>`),
529
529
  category: "orders_payment",
530
530
  showOnHomepage: false,
531
531
  showInFooter: false,
@@ -474,7 +474,7 @@ export const siteSettingsSeedData = {
474
474
  minimumTransactionFee: 0,
475
475
  gatewayFeePercent: 2,
476
476
  codDepositPercent: 10,
477
- codHandlingFeeMin: 200,
477
+ codHandlingFeeCap: 200,
478
478
  codHandlingFeePercent: 10,
479
479
  // Enabled so a seeded install actually shows the add-ons — see the matching
480
480
  // comment on DEFAULT_SITE_SETTINGS_DATA.commissions.
@@ -29,7 +29,14 @@ export interface ProductJsonLdInput {
29
29
  price: number;
30
30
  currency: string;
31
31
  mainImage?: string;
32
- images?: string[];
32
+ /**
33
+ * Prefer `productImageUrls(doc.images)`. The object form is accepted so a
34
+ * document holding `ProductImage[]` cannot take the page down — see the
35
+ * note on `imageUrlOf` in `productJsonLd`.
36
+ */
37
+ images?: readonly (string | {
38
+ url?: string;
39
+ })[];
33
40
  sellerName?: string;
34
41
  category?: string;
35
42
  status?: string;
@@ -27,7 +27,27 @@ const SITE_NAME = process.env.NEXT_PUBLIC_SITE_NAME ?? "App";
27
27
  * an empty value so `JSON.stringify` drops the key rather than emitting `""`.
28
28
  */
29
29
  function absoluteUrl(value, base = SITE_URL) {
30
- const v = value?.trim();
30
+ /*
31
+ * 🛑 `unknown` + a typeof test, NOT `string | null | undefined` + `?.trim()`.
32
+ *
33
+ * This took production down for hours. The signature said `string`, every
34
+ * caller type-checked, and then `ProductImage[]` replaced `string[]` on the
35
+ * product document — so `images.map(absoluteUrl)` received OBJECTS, `?.trim`
36
+ * was not a function, and the throw escaped into the RSC render and 500'd
37
+ * `/[locale]/products/[slug]` and `/[locale]/auctions/[id]`: **72 recorded
38
+ * failures**, the whole page lost to a decorative metadata field.
39
+ *
40
+ * A declared parameter type is a compile-time claim about CALLERS. It is
41
+ * worth nothing at the boundary where data arrives from Firestore, which is
42
+ * exactly where this function sits — and a schema migration is precisely the
43
+ * event that makes the claim false everywhere at once.
44
+ *
45
+ * So the contract is now behavioural: anything that is not a usable string
46
+ * is DROPPED, and the key is omitted. JSON-LD is optional enrichment —
47
+ * losing one image costs a rich-result thumbnail, while throwing costs the
48
+ * entire page. Those are not close.
49
+ */
50
+ const v = typeof value === "string" ? value.trim() : undefined;
31
51
  if (!v)
32
52
  return undefined;
33
53
  if (/^(https?:)?\/\//i.test(v) || v.startsWith("data:"))
@@ -92,11 +112,28 @@ export function productJsonLd(product) {
92
112
  const url = `${SITE_URL}${product.detailPath ?? `/products/${product.slug}`}`;
93
113
  // Absolutise, then de-duplicate: `mainImage` is very often also `images[0]`,
94
114
  // which produced the same URL twice in the emitted array.
115
+ /*
116
+ * 🛑 Accepts BOTH shapes on purpose. `absoluteUrl` now drops a non-string
117
+ * instead of throwing, which stops the 500 — but silently dropping every
118
+ * image would trade a crash for an invisible loss of the `image` property
119
+ * that Product rich results REQUIRE. Reading `.url` off an object keeps the
120
+ * rich result working whichever shape the document holds, so a caller that
121
+ * forgets `productImageUrls()` degrades to nothing worse than correct.
122
+ */
123
+ /*
124
+ * The cast narrows to `{ url?: string }` rather than `{ url?: unknown }`
125
+ * (`audit-unknown-leakage` blocks the latter, correctly — an `unknown` in a
126
+ * member position defeats checking on every read below it). The assertion is
127
+ * safe without being trusted: `absoluteUrl` tests `typeof value === "string"`
128
+ * itself, so a document whose `url` is a number is dropped rather than
129
+ * reaching `.trim()`. Narrow for the compiler, verify at runtime.
130
+ */
131
+ const imageUrlOf = (entry) => typeof entry === "string" ? entry : entry?.url;
95
132
  const images = [
96
133
  ...(product.mainImage ? [product.mainImage] : []),
97
134
  ...(product.images || []),
98
135
  ]
99
- .map((img) => absoluteUrl(img))
136
+ .map((img) => absoluteUrl(imageUrlOf(img)))
100
137
  .filter((img) => Boolean(img))
101
138
  .filter((img, i, arr) => arr.indexOf(img) === i);
102
139
  return {