@growupsell/commerce 0.0.0-stage → 1.0.0-alpha.2

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.
@@ -0,0 +1,472 @@
1
+ import {
2
+ isSelectionComplete,
3
+ productPricing,
4
+ resolveSku,
5
+ sellableSkus
6
+ } from "./chunk-W2CJSBV4.js";
7
+
8
+ // src/rules/promotions.ts
9
+ function promoCodeStatus(promo, now) {
10
+ if (promo.status !== "active") return promo.status;
11
+ const expires = Date.parse(promo.expiresAt);
12
+ if (Number.isNaN(expires)) return promo.status;
13
+ return expires < now.getTime() ? "expired" : "active";
14
+ }
15
+
16
+ // src/rules/stock.ts
17
+ function quantityLimitFor(settings, customerType) {
18
+ const limit = customerType === "business" ? settings.maxQuantityBusiness : settings.maxQuantityIndividual;
19
+ return limit > 0 ? limit : null;
20
+ }
21
+ var isCustomMode = (mode) => mode === "made_to_order_advance" || mode === "custom_fixed_price";
22
+ function purchaseLimit(input) {
23
+ const custom = isCustomMode(input.fulfillmentMode);
24
+ const threshold = input.lowStockThreshold ?? 5;
25
+ const stockLeft = custom ? null : Math.max(input.stockQty - input.inCart, 0);
26
+ const cap = Math.min(custom || input.isDigital ? 1 : Infinity, input.customerLimit ?? Infinity);
27
+ const maxToAdd = Math.max(Math.min(cap - input.inCart, stockLeft ?? Infinity), 0);
28
+ const outOfStock = stockLeft === 0;
29
+ return {
30
+ maxToAdd,
31
+ outOfStock,
32
+ limitReached: !outOfStock && maxToAdd === 0,
33
+ lowStock: stockLeft !== null && stockLeft > 0 && stockLeft <= threshold,
34
+ stockLeft
35
+ };
36
+ }
37
+
38
+ // src/rules/purchase.ts
39
+ function purchaseMode(product, settings) {
40
+ if (!settings.onlineOrdersEnabled) return "inquiry";
41
+ if (product.fulfillmentMode === "made_to_order_advance") return "build";
42
+ if (product.fulfillmentMode === "custom_fixed_price") return "customize";
43
+ return "cart";
44
+ }
45
+ function productPurchase(input) {
46
+ const { product, customer } = input;
47
+ const mode = purchaseMode(product, input.settings);
48
+ const complete = isSelectionComplete(product.variantGroups, input.selectedVariantIds);
49
+ const sku = complete ? resolveSku(product.skus, input.selectedVariantIds) : null;
50
+ const limit = sku ? purchaseLimit({
51
+ stockQty: sku.stockQty,
52
+ inCart: input.cartLines.filter((line) => line.skuId === sku.id).reduce((sum, line) => sum + line.quantity, 0),
53
+ fulfillmentMode: product.fulfillmentMode,
54
+ isDigital: product.hasDigitalDelivery,
55
+ customerLimit: quantityLimitFor(input.settings, customer?.type ?? null)
56
+ }) : null;
57
+ const canAdd = limit !== null && limit.maxToAdd > 0;
58
+ return {
59
+ mode,
60
+ complete,
61
+ sku,
62
+ pricing: sku ? productPricing(sku) : null,
63
+ limit,
64
+ canAddToCart: mode === "cart" && canAdd,
65
+ canCustomize: (mode === "build" || mode === "customize") && canAdd,
66
+ canSubscribe: customer !== null && sku !== null && product.availableForSubscription && input.features.includes("subscription"),
67
+ canWishlist: customer !== null && sku !== null
68
+ };
69
+ }
70
+
71
+ // src/rules/reviews.ts
72
+ function ratingBreakdown(summary) {
73
+ const { reviewCount, starCounts } = summary;
74
+ return [5, 4, 3, 2, 1].map((stars) => {
75
+ const count = starCounts[stars];
76
+ return {
77
+ stars,
78
+ count,
79
+ percent: reviewCount > 0 ? Math.round(count / reviewCount * 100) : 0
80
+ };
81
+ });
82
+ }
83
+ function roundToHalfStar(rating) {
84
+ if (typeof rating !== "number" || Number.isNaN(rating)) return 0;
85
+ return Math.round(Math.min(Math.max(rating, 0), 5) * 2) / 2;
86
+ }
87
+
88
+ // src/rules/text.ts
89
+ var NAMED_ENTITIES = {
90
+ amp: "&",
91
+ lt: "<",
92
+ gt: ">",
93
+ quot: '"',
94
+ apos: "'",
95
+ nbsp: "\xA0"
96
+ };
97
+ function decodeEntity(entity, body) {
98
+ if (body.startsWith("#")) {
99
+ const hex = body[1] === "x" || body[1] === "X";
100
+ const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);
101
+ return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : entity;
102
+ }
103
+ return NAMED_ENTITIES[body.toLowerCase()] ?? entity;
104
+ }
105
+ function htmlToText(html) {
106
+ if (!html) return "";
107
+ return html.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<[^>]*>/g, " ").replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, decodeEntity).replace(/\s+/g, " ").trim();
108
+ }
109
+ function truncateText(text, maxLength) {
110
+ if (text.length <= maxLength) return text;
111
+ if (maxLength < 1) return "";
112
+ const space = text.slice(0, maxLength).lastIndexOf(" ");
113
+ const head = space > 0 ? text.slice(0, space) : text.slice(0, maxLength - 1);
114
+ return `${head.trimEnd()}\u2026`;
115
+ }
116
+ var LATIN_WITH_MARKS = /([A-Za-z\u00C0-\u024F\u1E00-\u1EFF])[\u0300-\u036F]+/g;
117
+ function slugify(name) {
118
+ let slug = name.trim();
119
+ if (typeof "".normalize === "function") {
120
+ slug = slug.normalize("NFKD").replace(LATIN_WITH_MARKS, "$1").normalize("NFC");
121
+ }
122
+ return slug.toLowerCase().replace(/&/g, "and").replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
123
+ }
124
+
125
+ // src/rules/urls.ts
126
+ var LOCAL_HOST = /^(localhost|\d{1,3}(\.\d{1,3}){3}|\[::1\])(:\d+)?$/;
127
+ function siteOrigin(input) {
128
+ const value = input.domain?.trim() || input.host.trim();
129
+ const hostAndPort = value.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/[/?#].*$/, "").toLowerCase();
130
+ if (!hostAndPort) return "";
131
+ return `${LOCAL_HOST.test(hostAndPort) ? "http" : "https"}://${hostAndPort}`;
132
+ }
133
+ function absoluteUrl(origin, pathOrUrl) {
134
+ const value = pathOrUrl?.trim();
135
+ if (!value) return void 0;
136
+ if (/^https?:\/\//i.test(value)) return value;
137
+ if (value.startsWith("//") && !value.startsWith("///")) return `https:${value}`;
138
+ return `${origin.replace(/\/+$/, "")}/${value.replace(/^\/+/, "")}`;
139
+ }
140
+
141
+ // src/rules/seo.ts
142
+ var CONTEXT = "https://schema.org";
143
+ function descriptionField(html) {
144
+ const description = htmlToText(html);
145
+ return description ? { description } : {};
146
+ }
147
+ function serializeJsonLd(data) {
148
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
149
+ }
150
+ function organizationJsonLd(input) {
151
+ const logo = absoluteUrl(input.url, input.logo);
152
+ const sameAs = (input.sameAs ?? []).flatMap((link) => link?.trim() ? [link.trim()] : []);
153
+ return {
154
+ "@context": CONTEXT,
155
+ "@type": "Organization",
156
+ name: input.name,
157
+ url: input.url,
158
+ ...descriptionField(input.description),
159
+ ...logo ? { logo } : {},
160
+ ...sameAs.length ? { sameAs } : {}
161
+ };
162
+ }
163
+ function webSiteJsonLd(input) {
164
+ return {
165
+ "@context": CONTEXT,
166
+ "@type": "WebSite",
167
+ name: input.name,
168
+ url: input.url,
169
+ ...descriptionField(input.description)
170
+ };
171
+ }
172
+ function breadcrumbJsonLd(items) {
173
+ const list = items.filter((item) => item.name.trim() && item.url.trim());
174
+ if (!list.length) return null;
175
+ return {
176
+ "@context": CONTEXT,
177
+ "@type": "BreadcrumbList",
178
+ itemListElement: list.map((item, index) => ({
179
+ "@type": "ListItem",
180
+ position: index + 1,
181
+ name: item.name,
182
+ item: item.url
183
+ }))
184
+ };
185
+ }
186
+ function faqJsonLd(faqs) {
187
+ const valid = faqs.map((faq) => ({ question: htmlToText(faq.question), answer: htmlToText(faq.answer) })).filter((faq) => faq.question && faq.answer);
188
+ if (!valid.length) return null;
189
+ return {
190
+ "@context": CONTEXT,
191
+ "@type": "FAQPage",
192
+ mainEntity: valid.map((faq) => ({
193
+ "@type": "Question",
194
+ name: faq.question,
195
+ acceptedAnswer: { "@type": "Answer", text: faq.answer }
196
+ }))
197
+ };
198
+ }
199
+ function articleJsonLd(input) {
200
+ return {
201
+ "@context": CONTEXT,
202
+ "@type": "Article",
203
+ headline: input.headline,
204
+ ...descriptionField(input.description),
205
+ ...input.image ? { image: [input.image] } : {},
206
+ mainEntityOfPage: { "@type": "WebPage", "@id": input.url },
207
+ ...input.datePublished ? { datePublished: input.datePublished } : {},
208
+ ...input.dateModified ? { dateModified: input.dateModified } : {},
209
+ ...input.authorName ? { author: { "@type": "Person", name: input.authorName } } : {}
210
+ };
211
+ }
212
+ function isRecord(value) {
213
+ return typeof value === "object" && value !== null;
214
+ }
215
+ function faqsFromSections(sections) {
216
+ const faqs = [];
217
+ for (const item of sections.flatMap((s) => s.items)) {
218
+ const components = Array.isArray(item.data) ? item.data : [item.data];
219
+ for (const component of components) {
220
+ if (!isRecord(component) || component.slug !== "faqs") continue;
221
+ if (!Array.isArray(component.items)) continue;
222
+ for (const entry of component.items) {
223
+ if (!isRecord(entry)) continue;
224
+ const { question, answer } = entry;
225
+ if (typeof question !== "string" || typeof answer !== "string") continue;
226
+ if (question.trim() && answer.trim())
227
+ faqs.push({ question: question.trim(), answer: answer.trim() });
228
+ }
229
+ }
230
+ }
231
+ return faqs;
232
+ }
233
+ var AVAILABILITY = {
234
+ inStock: `${CONTEXT}/InStock`,
235
+ outOfStock: `${CONTEXT}/OutOfStock`,
236
+ madeToOrder: `${CONTEXT}/MadeToOrder`
237
+ };
238
+ function originOf(url) {
239
+ return /^https?:\/\/[^/?#]+/i.exec(url.trim())?.[0] ?? "";
240
+ }
241
+ function productOffers(skus, input) {
242
+ if (!skus.length) return void 0;
243
+ const availability = input.madeToOrder ? AVAILABILITY.madeToOrder : skus.some((sku) => sku.stockQty > 0) ? AVAILABILITY.inStock : AVAILABILITY.outOfStock;
244
+ const prices = skus.map((sku) => sku.price);
245
+ const low = Math.min(...prices);
246
+ const high = Math.max(...prices);
247
+ if (low === high) {
248
+ return {
249
+ "@type": "Offer",
250
+ url: input.url,
251
+ priceCurrency: input.currency,
252
+ price: low,
253
+ availability
254
+ };
255
+ }
256
+ return {
257
+ "@type": "AggregateOffer",
258
+ priceCurrency: input.currency,
259
+ lowPrice: low,
260
+ highPrice: high,
261
+ offerCount: skus.length,
262
+ availability
263
+ };
264
+ }
265
+ function productJsonLd(input) {
266
+ const { product, url } = input;
267
+ const skus = sellableSkus(product.skus);
268
+ const origin = originOf(url);
269
+ const images = product.images.flatMap((image) => {
270
+ const absolute = absoluteUrl(origin, image.url);
271
+ return absolute ? [absolute] : [];
272
+ });
273
+ const brandName = input.brandName?.trim();
274
+ const offers = productOffers(skus, {
275
+ url,
276
+ currency: input.currency,
277
+ madeToOrder: product.fulfillmentMode === "made_to_order_advance" || product.fulfillmentMode === "custom_fixed_price"
278
+ });
279
+ const [onlySku] = skus.length === 1 ? skus : [];
280
+ return {
281
+ "@context": CONTEXT,
282
+ "@type": "Product",
283
+ name: product.name,
284
+ url,
285
+ ...descriptionField(product.description),
286
+ ...images.length ? { image: images } : {},
287
+ ...onlySku ? { sku: onlySku.code } : {},
288
+ ...brandName ? { brand: { "@type": "Brand", name: brandName } } : {},
289
+ ...product.reviewCount > 0 ? {
290
+ aggregateRating: {
291
+ "@type": "AggregateRating",
292
+ ratingValue: product.rating,
293
+ reviewCount: product.reviewCount,
294
+ bestRating: 5,
295
+ worstRating: 1
296
+ }
297
+ } : {},
298
+ ...offers ? { offers } : {}
299
+ };
300
+ }
301
+ function pageMetadata(input) {
302
+ const siteName = input.business.name?.trim() ?? "";
303
+ const pageTitle = input.title?.trim() ?? "";
304
+ const text = htmlToText(input.description) || htmlToText(input.business.description);
305
+ const origin = input.origin.trim().replace(/\/+$/, "");
306
+ const image = input.image?.trim() || input.business.logo;
307
+ return {
308
+ title: [pageTitle, siteName].filter(Boolean).join(" | "),
309
+ description: text ? truncateText(text, input.descriptionLength ?? 160) : null,
310
+ canonicalUrl: origin ? `${origin}/${(input.path ?? "").trim().replace(/^\/+/, "")}` : null,
311
+ imageUrl: absoluteUrl(origin, image) ?? null,
312
+ siteName
313
+ };
314
+ }
315
+
316
+ // src/rules/validation.ts
317
+ var LOCAL_PART = /^[A-Za-z0-9_+-]+(\.[A-Za-z0-9_+-]+)*$/;
318
+ var DOMAIN_LABEL = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/;
319
+ var TOP_LABEL = /^[A-Za-z]{2,}$/;
320
+ var E164 = /^\+[1-9]\d{6,14}$/;
321
+ function isEmail(value) {
322
+ const email = (value ?? "").trim();
323
+ if (email.length === 0 || email.length > 254) return false;
324
+ const at = email.indexOf("@");
325
+ if (at < 1 || at !== email.lastIndexOf("@")) return false;
326
+ const local = email.slice(0, at);
327
+ if (local.length > 64 || !LOCAL_PART.test(local)) return false;
328
+ const labels = email.slice(at + 1).split(".");
329
+ if (labels.length < 2) return false;
330
+ if (!labels.every((label) => label.length <= 63 && DOMAIN_LABEL.test(label))) return false;
331
+ return TOP_LABEL.test(labels[labels.length - 1]);
332
+ }
333
+ function isPhone(value) {
334
+ return E164.test((value ?? "").trim());
335
+ }
336
+ function toE164(value, options = {}) {
337
+ let digits = value.replace(/[\s\-.()]/g, "");
338
+ if (digits.startsWith("00")) digits = `+${digits.slice(2)}`;
339
+ if (!digits.startsWith("+")) {
340
+ const dialCode = options.dialCode?.trim().replace(/^\+/, "");
341
+ if (!dialCode || !/^\d{1,4}$/.test(dialCode)) return null;
342
+ digits = `+${dialCode}${digits.startsWith("0") ? digits.slice(1) : digits}`;
343
+ }
344
+ return isPhone(digits) ? digits : null;
345
+ }
346
+ function isOtp(value) {
347
+ return /^\d{6}$/.test((value ?? "").trim());
348
+ }
349
+
350
+ // src/rules/video.ts
351
+ var URL_PARTS = /^(?:([a-z][a-z0-9+.-]*):\/\/|\/\/)?([^/?#]*)([^?#]*)(?:\?([^#]*))?/i;
352
+ var YOUTUBE_HOSTS = /* @__PURE__ */ new Set([
353
+ "youtube.com",
354
+ "www.youtube.com",
355
+ "m.youtube.com",
356
+ "music.youtube.com",
357
+ "youtube-nocookie.com",
358
+ "www.youtube-nocookie.com"
359
+ ]);
360
+ var INSTAGRAM_HOSTS = /* @__PURE__ */ new Set(["instagram.com", "www.instagram.com", "m.instagram.com"]);
361
+ var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
362
+ var YOUTUBE_ID_PATHS = /* @__PURE__ */ new Set(["embed", "shorts", "live", "v"]);
363
+ var INSTAGRAM_KINDS = { reel: "reel", reels: "reel", p: "p", tv: "tv" };
364
+ var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "mov", "avi", "wmv", "webm", "ogg", "m4v"]);
365
+ function urlParts(url) {
366
+ const match = URL_PARTS.exec((url ?? "").trim());
367
+ if (!match) return null;
368
+ const [, scheme, authority = "", path = "", query = ""] = match;
369
+ if (scheme !== void 0 && !/^https?$/i.test(scheme)) return null;
370
+ if (authority.includes("@")) return null;
371
+ return {
372
+ host: authority.replace(/:\d*$/, "").toLowerCase(),
373
+ segments: path.split("/").filter((segment) => segment !== ""),
374
+ query
375
+ };
376
+ }
377
+ function queryValue(query, key) {
378
+ for (const pair of query.split("&")) {
379
+ const eq = pair.indexOf("=");
380
+ if (eq > 0 && pair.slice(0, eq) === key) return pair.slice(eq + 1);
381
+ }
382
+ return null;
383
+ }
384
+ function youtubeVideoId(url) {
385
+ const parts = urlParts(url);
386
+ if (!parts) return null;
387
+ const { host, segments, query } = parts;
388
+ let id = null;
389
+ if (host === "youtu.be") {
390
+ if (segments.length === 1) id = segments[0];
391
+ } else if (YOUTUBE_HOSTS.has(host)) {
392
+ if (segments.length === 1 && segments[0] === "watch") id = queryValue(query, "v");
393
+ else if (segments.length === 2 && YOUTUBE_ID_PATHS.has(segments[0])) id = segments[1];
394
+ }
395
+ return id !== null && YOUTUBE_ID.test(id) ? id : null;
396
+ }
397
+ function youtubeEmbedUrl(url, options = {}) {
398
+ const id = youtubeVideoId(url);
399
+ if (!id) return null;
400
+ return `https://www.youtube.com/embed/${id}?rel=0${options.autoplay ? "&autoplay=1" : ""}`;
401
+ }
402
+ function youtubeWatchUrl(id) {
403
+ return `https://www.youtube.com/watch?v=${id}`;
404
+ }
405
+ var THUMBNAIL_FILES = {
406
+ default: "default",
407
+ medium: "mqdefault",
408
+ high: "hqdefault",
409
+ standard: "sddefault",
410
+ max: "maxresdefault"
411
+ };
412
+ function youtubeThumbnailUrl(url, quality = "high") {
413
+ const id = youtubeVideoId(url);
414
+ return id ? `https://img.youtube.com/vi/${id}/${THUMBNAIL_FILES[quality]}.jpg` : null;
415
+ }
416
+ function instagramEmbedUrl(url) {
417
+ const parts = urlParts(url);
418
+ if (!parts || !INSTAGRAM_HOSTS.has(parts.host)) return null;
419
+ const [kind, code] = parts.segments;
420
+ const embedKind = kind === void 0 ? void 0 : INSTAGRAM_KINDS[kind];
421
+ if (!embedKind || code === void 0 || !/^[A-Za-z0-9_-]+$/.test(code)) return null;
422
+ return `https://www.instagram.com/${embedKind}/${code}/embed`;
423
+ }
424
+ function videoEmbed(url, options = {}) {
425
+ const youtube = youtubeEmbedUrl(url, options);
426
+ if (youtube) return { provider: "youtube", embedUrl: youtube };
427
+ const instagram = instagramEmbedUrl(url);
428
+ return instagram ? { provider: "instagram", embedUrl: instagram } : null;
429
+ }
430
+ function isVideoFileUrl(url) {
431
+ const segments = urlParts(url)?.segments ?? [];
432
+ const last = segments[segments.length - 1];
433
+ if (last === void 0) return false;
434
+ const dot = last.lastIndexOf(".");
435
+ return dot >= 0 && VIDEO_EXTENSIONS.has(last.slice(dot + 1).toLowerCase());
436
+ }
437
+
438
+ export {
439
+ promoCodeStatus,
440
+ quantityLimitFor,
441
+ purchaseLimit,
442
+ purchaseMode,
443
+ productPurchase,
444
+ ratingBreakdown,
445
+ roundToHalfStar,
446
+ htmlToText,
447
+ truncateText,
448
+ slugify,
449
+ siteOrigin,
450
+ absoluteUrl,
451
+ serializeJsonLd,
452
+ organizationJsonLd,
453
+ webSiteJsonLd,
454
+ breadcrumbJsonLd,
455
+ faqJsonLd,
456
+ articleJsonLd,
457
+ faqsFromSections,
458
+ productJsonLd,
459
+ pageMetadata,
460
+ isEmail,
461
+ isPhone,
462
+ toE164,
463
+ isOtp,
464
+ youtubeVideoId,
465
+ youtubeEmbedUrl,
466
+ youtubeWatchUrl,
467
+ youtubeThumbnailUrl,
468
+ instagramEmbedUrl,
469
+ videoEmbed,
470
+ isVideoFileUrl
471
+ };
472
+ //# sourceMappingURL=chunk-JBOZQJ4X.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/promotions.ts","../src/rules/stock.ts","../src/rules/purchase.ts","../src/rules/reviews.ts","../src/rules/text.ts","../src/rules/urls.ts","../src/rules/seo.ts","../src/rules/validation.ts","../src/rules/video.ts"],"sourcesContent":["/** Promo-code rules. */\nimport type { PromoCode } from '../types/promotions';\n\n/**\n * The status to show for a promo code at `now`: an `active` code whose `expiresAt` lies before\n * `now` is `expired` (the backend's rule, `expire_date < now`); `redeemed` and `expired` stay as\n * the backend sent them, and an unparsable `expiresAt` keeps the backend's status.\n * Fixes the storefront comparing against the device clock inline (`ApplyDiscountModal`).\n */\nexport function promoCodeStatus(\n promo: Pick<PromoCode, 'status' | 'expiresAt'>,\n now: Date,\n): PromoCode['status'] {\n if (promo.status !== 'active') return promo.status;\n const expires = Date.parse(promo.expiresAt);\n if (Number.isNaN(expires)) return promo.status;\n return expires < now.getTime() ? 'expired' : 'active';\n}\n","/**\n * Stock and quantity limits: how many of a SKU the shopper may still add. Mirrors the backend's\n * add-to-cart checks (`orders/serializers.py`), so the app never offers a quantity the backend\n * then refuses.\n */\nimport type { FulfillmentMode } from '../types/cart';\nimport type { CheckoutSettings } from '../types/checkout';\nimport type { CustomerProfile } from '../types/customer';\n\n/**\n * The most units of one SKU this customer may order: business customers get\n * `maxQuantityBusiness`, everyone else (guests included) `maxQuantityIndividual`. Null when that\n * value is 0 or less (no limit). The old product page ignored this limit and offered the stock.\n */\nexport function quantityLimitFor(\n settings: Pick<CheckoutSettings, 'maxQuantityIndividual' | 'maxQuantityBusiness'>,\n customerType: CustomerProfile['type'] | null,\n): number | null {\n const limit =\n customerType === 'business' ? settings.maxQuantityBusiness : settings.maxQuantityIndividual;\n return limit > 0 ? limit : null;\n}\n\nexport interface PurchaseLimitInput {\n /** The SKU's `stockQty`. */\n stockQty: number;\n /** Units of this SKU already in the cart. */\n inCart: number;\n fulfillmentMode: FulfillmentMode;\n /** The product has digital delivery. */\n isDigital: boolean;\n /** From `quantityLimitFor()`; null for none. */\n customerLimit: number | null;\n /** Stock left at or below this is \"low\". Default 5. */\n lowStockThreshold?: number;\n}\n\nexport interface PurchaseLimit {\n /** Units that may still be added; 0 when none (see `outOfStock` and `limitReached`). */\n maxToAdd: number;\n /** No stock left after what is in the cart. */\n outOfStock: boolean;\n /** Stock is left, but the cart already holds as many as allowed. */\n limitReached: boolean;\n /** Some stock is left, at most the threshold. */\n lowStock: boolean;\n /** Stock left after what is in the cart; null when stock does not apply (made to order). */\n stockLeft: number | null;\n}\n\nconst isCustomMode = (mode: FulfillmentMode) =>\n mode === 'made_to_order_advance' || mode === 'custom_fixed_price';\n\n/**\n * How many more units the shopper may add, and why not when none: stock minus what is in the\n * cart, at most the customer's limit; made-to-order and custom products ignore stock and allow\n * one, digital products one per cart. Returns 0 (with `outOfStock` or `limitReached`) when\n * nothing can be added; the old storefront offered `max(1, …)` and ignored the cart.\n * `always_available_stock` is not honoured: the backend's add-to-cart ignores it too (W66).\n */\nexport function purchaseLimit(input: PurchaseLimitInput): PurchaseLimit {\n const custom = isCustomMode(input.fulfillmentMode);\n const threshold = input.lowStockThreshold ?? 5;\n const stockLeft = custom ? null : Math.max(input.stockQty - input.inCart, 0);\n const cap = Math.min(custom || input.isDigital ? 1 : Infinity, input.customerLimit ?? Infinity);\n const maxToAdd = Math.max(Math.min(cap - input.inCart, stockLeft ?? Infinity), 0);\n const outOfStock = stockLeft === 0;\n return {\n maxToAdd,\n outOfStock,\n limitReached: !outOfStock && maxToAdd === 0,\n lowStock: stockLeft !== null && stockLeft > 0 && stockLeft <= threshold,\n stockLeft,\n };\n}\n","/**\n * Product-page purchase state: what the shopper is buying, at what price, how many, and which\n * buttons are enabled. Combines the SKU, pricing and stock rules for one selection.\n */\nimport type { CartLine, FulfillmentMode } from '../types/cart';\nimport type { CheckoutSettings } from '../types/checkout';\nimport type { CustomerProfile } from '../types/customer';\nimport type { ProductDetail, ProductSku } from '../types/product';\nimport { productPricing } from './pricing';\nimport type { ProductPricing } from './pricing';\nimport { isSelectionComplete, resolveSku } from './sku';\nimport { purchaseLimit, quantityLimitFor } from './stock';\nimport type { PurchaseLimit } from './stock';\n\n/**\n * How a product is bought: `cart` (add to cart), `build` (made to order with an advance),\n * `customize` (custom product at a fixed price) or `inquiry` (online orders are off).\n */\nexport type PurchaseMode = 'cart' | 'build' | 'customize' | 'inquiry';\n\n/**\n * How a product is bought: `inquiry` when the business takes no online orders; `build` for\n * made-to-order products; `customize` for custom fixed-price products; otherwise `cart`.\n */\nexport function purchaseMode(\n product: { fulfillmentMode: FulfillmentMode },\n settings: Pick<CheckoutSettings, 'onlineOrdersEnabled'>,\n): PurchaseMode {\n if (!settings.onlineOrdersEnabled) return 'inquiry';\n if (product.fulfillmentMode === 'made_to_order_advance') return 'build';\n if (product.fulfillmentMode === 'custom_fixed_price') return 'customize';\n return 'cart';\n}\n\nexport interface ProductPurchaseInput {\n product: Pick<\n ProductDetail,\n 'variantGroups' | 'skus' | 'fulfillmentMode' | 'hasDigitalDelivery' | 'availableForSubscription'\n >;\n selectedVariantIds: readonly number[];\n /** The cart's lines (to count what is already in it). */\n cartLines: readonly Pick<CartLine, 'skuId' | 'quantity'>[];\n settings: Pick<\n CheckoutSettings,\n 'onlineOrdersEnabled' | 'maxQuantityIndividual' | 'maxQuantityBusiness'\n >;\n /** The signed-in customer, or null for a guest. */\n customer: Pick<CustomerProfile, 'type'> | null;\n /** The business's enabled features (`subscription`, …). */\n features: readonly string[];\n}\n\nexport interface ProductPurchase {\n mode: PurchaseMode;\n /** One option of every variant group is selected. */\n complete: boolean;\n /** The SKU being bought; null while the selection is incomplete or matches no sellable SKU. */\n sku: ProductSku | null;\n pricing: ProductPricing | null;\n limit: PurchaseLimit | null;\n canAddToCart: boolean;\n /** The build / customize call to action (made-to-order and custom products). */\n canCustomize: boolean;\n canSubscribe: boolean;\n canWishlist: boolean;\n}\n\n/**\n * The purchase state of a product page for one selection: the SKU (only once the selection is\n * complete), its pricing and quantity limit (counting what is already in the cart), and which\n * actions are enabled. Add-to-cart and the custom call to action need a SKU with something left\n * to add; subscribing needs a customer, the `subscription` feature and a subscribable product;\n * the wishlist needs a customer and a SKU (the old storefront wishlisted the product id when no\n * SKU was selected).\n */\nexport function productPurchase(input: ProductPurchaseInput): ProductPurchase {\n const { product, customer } = input;\n const mode = purchaseMode(product, input.settings);\n const complete = isSelectionComplete(product.variantGroups, input.selectedVariantIds);\n const sku = complete ? resolveSku(product.skus, input.selectedVariantIds) : null;\n\n const limit = sku\n ? purchaseLimit({\n stockQty: sku.stockQty,\n inCart: input.cartLines\n .filter((line) => line.skuId === sku.id)\n .reduce((sum, line) => sum + line.quantity, 0),\n fulfillmentMode: product.fulfillmentMode,\n isDigital: product.hasDigitalDelivery,\n customerLimit: quantityLimitFor(input.settings, customer?.type ?? null),\n })\n : null;\n const canAdd = limit !== null && limit.maxToAdd > 0;\n\n return {\n mode,\n complete,\n sku,\n pricing: sku ? productPricing(sku) : null,\n limit,\n canAddToCart: mode === 'cart' && canAdd,\n canCustomize: (mode === 'build' || mode === 'customize') && canAdd,\n canSubscribe:\n customer !== null &&\n sku !== null &&\n product.availableForSubscription &&\n input.features.includes('subscription'),\n canWishlist: customer !== null && sku !== null,\n };\n}\n","/** Rating display rules: the per-star breakdown and star rounding. Apps format the numbers. */\nimport type { ReviewSummary } from '../types/reviews';\n\n/** One row of a rating breakdown. */\nexport interface RatingBreakdownRow {\n stars: 1 | 2 | 3 | 4 | 5;\n count: number;\n /** Whole percent of all reviews. */\n percent: number;\n}\n\n/**\n * Five rows, 5 stars first: the count per whole star and its whole percent of all reviews\n * (0 without reviews). Half-star reviews are counted in no bucket by the backend, so the\n * percents may add up to less than 100.\n */\nexport function ratingBreakdown(\n summary: Pick<ReviewSummary, 'reviewCount' | 'starCounts'>,\n): RatingBreakdownRow[] {\n const { reviewCount, starCounts } = summary;\n return ([5, 4, 3, 2, 1] as const).map((stars) => {\n const count = starCounts[stars];\n return {\n stars,\n count,\n percent: reviewCount > 0 ? Math.round((count / reviewCount) * 100) : 0,\n };\n });\n}\n\n/**\n * A rating for star icons: clamped to 0–5 (non-numbers and NaN → 0) and rounded to the nearest\n * half star, so 4.6 shows 4.5 stars (the old storefront showed 5 there but 4.5 for 4.3).\n */\nexport function roundToHalfStar(rating: number): number {\n if (typeof rating !== 'number' || Number.isNaN(rating)) return 0;\n return Math.round(Math.min(Math.max(rating, 0), 5) * 2) / 2;\n}\n","/** Plain-text helpers for meta tags, JSON-LD and links. No `Intl`, no DOM. */\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: '\\u00A0',\n};\n\nfunction decodeEntity(entity: string, body: string): string {\n if (body.startsWith('#')) {\n const hex = body[1] === 'x' || body[1] === 'X';\n const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);\n return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : entity;\n }\n return NAMED_ENTITIES[body.toLowerCase()] ?? entity;\n}\n\n/**\n * The text of an HTML fragment: `<script>` and `<style>` dropped with their content, every other\n * tag replaced by a space, entities decoded **once** (`&amp; &lt; &gt; &quot; &apos; &nbsp;`,\n * decimal and hex; unknown ones left as written), whitespace collapsed, trimmed.\n * Fixes the old `stripHtml`, which decoded twice (`&amp;lt;` became `<`) and ignored numeric\n * entities.\n */\nexport function htmlToText(html: string | null | undefined): string {\n if (!html) return '';\n return html\n .replace(/<(script|style)\\b[^>]*>[\\s\\S]*?<\\/\\1\\s*>/gi, ' ')\n .replace(/<[^>]*>/g, ' ')\n .replace(/&(#\\d+|#x[0-9a-f]+|[a-z]+);/gi, decodeEntity)\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/**\n * `text` cut to at most `maxLength` characters: unchanged when it fits, else cut at the last space\n * that keeps room for the `…` it appends (a hard cut when there is none).\n */\nexport function truncateText(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n if (maxLength < 1) return '';\n const space = text.slice(0, maxLength).lastIndexOf(' ');\n const head = space > 0 ? text.slice(0, space) : text.slice(0, maxLength - 1);\n return `${head.trimEnd()}…`;\n}\n\nconst LATIN_WITH_MARKS = /([A-Za-z\\u00C0-\\u024F\\u1E00-\\u1EFF])[\\u0300-\\u036F]+/g;\n\n/**\n * A URL path segment from a name: lower case, `&` → `and`, accents on Latin letters removed,\n * every run of characters that are not letters, marks or digits (any script) → `-`, no `-` at\n * either end. ASCII names give the storefront's `slugifyName` result, so sitemap URLs do not\n * change; Hindi and other scripts are kept (ADR-025). May be `''`: callers choose a fallback.\n * Fixes the storefront's five slug rules, of which `replaceAll(' ', '-')` kept `/ # ? %`\n * (a `/` in a name made the product page 404).\n */\nexport function slugify(name: string): string {\n let slug = name.trim();\n // Hermes builds without Intl may lack normalize: accents are then kept, nothing breaks.\n if (typeof ''.normalize === 'function') {\n slug = slug.normalize('NFKD').replace(LATIN_WITH_MARKS, '$1').normalize('NFC');\n }\n return slug\n .toLowerCase()\n .replace(/&/g, 'and')\n .replace(/[^\\p{L}\\p{M}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '');\n}\n","/** Site origin and absolute URLs, parsed by hand (no `URL`, ADR-009). */\n\nconst LOCAL_HOST = /^(localhost|\\d{1,3}(\\.\\d{1,3}){3}|\\[::1\\])(:\\d+)?$/;\n\n/**\n * The site's origin: `domain` when set, else `host`; scheme, path, query, hash and trailing\n * slashes removed, lower case; `http://` for `localhost`, IPv4 addresses and `[::1]` (any port),\n * else `https://`. `''` when both are empty.\n * Replaces two storefront copies, neither of which removed a path from `domain`.\n */\nexport function siteOrigin(input: { host: string; domain?: string | null }): string {\n const value = input.domain?.trim() || input.host.trim();\n const hostAndPort = value\n .replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, '')\n .replace(/[/?#].*$/, '')\n .toLowerCase();\n if (!hostAndPort) return '';\n return `${LOCAL_HOST.test(hostAndPort) ? 'http' : 'https'}://${hostAndPort}`;\n}\n\n/**\n * `pathOrUrl` as an absolute URL on `origin`: `http(s)://…` as is, `//host/…` as\n * `https://host/…`, anything else appended to `origin` with one `/`. Undefined when empty.\n * Fixes the old helper, which turned `//cdn/x.jpg` into `origin//cdn/x.jpg`.\n */\nexport function absoluteUrl(\n origin: string,\n pathOrUrl: string | null | undefined,\n): string | undefined {\n const value = pathOrUrl?.trim();\n if (!value) return undefined;\n if (/^https?:\\/\\//i.test(value)) return value;\n if (value.startsWith('//') && !value.startsWith('///')) return `https:${value}`;\n return `${origin.replace(/\\/+$/, '')}/${value.replace(/^\\/+/, '')}`;\n}\n","/**\n * SEO: schema.org JSON-LD builders and page metadata. Plain data in, plain objects out; the app\n * renders them (a `<script type=\"application/ld+json\">`, its framework's metadata API).\n */\nimport type { ProductDetail, ProductSku } from '../types/product';\nimport type { ProductSection } from '../types/productPage';\nimport { sellableSkus } from './sku';\nimport { htmlToText, truncateText } from './text';\nimport { absoluteUrl } from './urls';\n\n/** A schema.org JSON-LD object. */\nexport type JsonLd = Record<string, unknown>;\n\nconst CONTEXT = 'https://schema.org';\n\n/** `{ description }` as plain text, or nothing when the text is empty. */\nfunction descriptionField(html: string | null | undefined): { description?: string } {\n const description = htmlToText(html);\n return description ? { description } : {};\n}\n\n/**\n * JSON for a `<script type=\"application/ld+json\">`: `<` is written as `<`, so the data can\n * never close the script tag, and U+2028/U+2029 are escaped for old JavaScript parsers.\n */\nexport function serializeJsonLd(data: JsonLd): string {\n return JSON.stringify(data)\n .replace(/</g, '\\\\u003c')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * The business as a schema.org `Organization`: description as plain text and left out when\n * empty, `logo` made absolute on `url`, empty `sameAs` links dropped.\n */\nexport function organizationJsonLd(input: {\n name: string;\n url: string;\n logo?: string | null;\n description?: string | null;\n sameAs?: readonly (string | null)[];\n}): JsonLd {\n const logo = absoluteUrl(input.url, input.logo);\n const sameAs = (input.sameAs ?? []).flatMap((link) => (link?.trim() ? [link.trim()] : []));\n return {\n '@context': CONTEXT,\n '@type': 'Organization',\n name: input.name,\n url: input.url,\n ...descriptionField(input.description),\n ...(logo ? { logo } : {}),\n ...(sameAs.length ? { sameAs } : {}),\n };\n}\n\n/** The storefront as a schema.org `WebSite`; the description is plain text, left out when empty. */\nexport function webSiteJsonLd(input: {\n name: string;\n url: string;\n description?: string | null;\n}): JsonLd {\n return {\n '@context': CONTEXT,\n '@type': 'WebSite',\n name: input.name,\n url: input.url,\n ...descriptionField(input.description),\n };\n}\n\n/**\n * A schema.org `BreadcrumbList`, positions from 1. Items without a name or URL are dropped;\n * null when none is left.\n */\nexport function breadcrumbJsonLd(items: readonly { name: string; url: string }[]): JsonLd | null {\n const list = items.filter((item) => item.name.trim() && item.url.trim());\n if (!list.length) return null;\n return {\n '@context': CONTEXT,\n '@type': 'BreadcrumbList',\n itemListElement: list.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n };\n}\n\n/**\n * A schema.org `FAQPage`. Questions and answers become plain text; pairs with an empty side are\n * dropped; null when none is left.\n */\nexport function faqJsonLd(faqs: readonly { question: string; answer: string }[]): JsonLd | null {\n const valid = faqs\n .map((faq) => ({ question: htmlToText(faq.question), answer: htmlToText(faq.answer) }))\n .filter((faq) => faq.question && faq.answer);\n if (!valid.length) return null;\n return {\n '@context': CONTEXT,\n '@type': 'FAQPage',\n mainEntity: valid.map((faq) => ({\n '@type': 'Question',\n name: faq.question,\n acceptedAnswer: { '@type': 'Answer', text: faq.answer },\n })),\n };\n}\n\n/** A blog post as a schema.org `Article`; optional fields are left out when empty. */\nexport function articleJsonLd(input: {\n headline: string;\n url: string;\n description?: string | null;\n image?: string | null;\n datePublished?: string | null;\n dateModified?: string | null;\n authorName?: string | null;\n}): JsonLd {\n return {\n '@context': CONTEXT,\n '@type': 'Article',\n headline: input.headline,\n ...descriptionField(input.description),\n ...(input.image ? { image: [input.image] } : {}),\n mainEntityOfPage: { '@type': 'WebPage', '@id': input.url },\n ...(input.datePublished ? { datePublished: input.datePublished } : {}),\n ...(input.dateModified ? { dateModified: input.dateModified } : {}),\n ...(input.authorName ? { author: { '@type': 'Person', name: input.authorName } } : {}),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * The question/answer pairs of every `faqs` component in a product's sections. A section item's\n * `data` is one component or a list of them; a `faqs` component holds `items: [{ question,\n * answer }]`. Both sides are trimmed, pairs with an empty side dropped, other shapes ignored.\n */\nexport function faqsFromSections(\n sections: readonly ProductSection[],\n): Array<{ question: string; answer: string }> {\n const faqs: Array<{ question: string; answer: string }> = [];\n for (const item of sections.flatMap((s) => s.items)) {\n const components: unknown[] = Array.isArray(item.data) ? item.data : [item.data];\n for (const component of components) {\n if (!isRecord(component) || component.slug !== 'faqs') continue;\n if (!Array.isArray(component.items)) continue;\n for (const entry of component.items as unknown[]) {\n if (!isRecord(entry)) continue;\n const { question, answer } = entry;\n if (typeof question !== 'string' || typeof answer !== 'string') continue;\n if (question.trim() && answer.trim())\n faqs.push({ question: question.trim(), answer: answer.trim() });\n }\n }\n }\n return faqs;\n}\n\nconst AVAILABILITY = {\n inStock: `${CONTEXT}/InStock`,\n outOfStock: `${CONTEXT}/OutOfStock`,\n madeToOrder: `${CONTEXT}/MadeToOrder`,\n};\n\n/** `scheme://host` of an absolute URL, or `''`. */\nfunction originOf(url: string): string {\n return /^https?:\\/\\/[^/?#]+/i.exec(url.trim())?.[0] ?? '';\n}\n\n/** The JSON-LD offers of a product's sellable SKUs; undefined when none can be bought. */\nfunction productOffers(\n skus: readonly ProductSku[],\n input: { url: string; currency: string; madeToOrder: boolean },\n): JsonLd | undefined {\n if (!skus.length) return undefined;\n const availability = input.madeToOrder\n ? AVAILABILITY.madeToOrder\n : skus.some((sku) => sku.stockQty > 0)\n ? AVAILABILITY.inStock\n : AVAILABILITY.outOfStock;\n const prices = skus.map((sku) => sku.price);\n const low = Math.min(...prices);\n const high = Math.max(...prices);\n if (low === high) {\n return {\n '@type': 'Offer',\n url: input.url,\n priceCurrency: input.currency,\n price: low,\n availability,\n };\n }\n return {\n '@type': 'AggregateOffer',\n priceCurrency: input.currency,\n lowPrice: low,\n highPrice: high,\n offerCount: skus.length,\n availability,\n };\n}\n\n/**\n * A product as a schema.org `Product`. Offers cover only the sellable SKUs (`sellableSkus`):\n * one `Offer` when they share one price (with the SKU code when there is exactly one SKU), an\n * `AggregateOffer` from the lowest to the highest price otherwise, none when nothing can be\n * bought (ADR-025). Availability is `MadeToOrder` for made-to-order and custom products, else\n * `InStock` when any SKU has stock. `brand` is the product's own brand, only when `brandName` is\n * given (never the business); `aggregateRating` only when there are reviews; the description is\n * plain text and left out when empty; images are made absolute on the page's origin.\n * Fixes the old product page: the deleted base SKU's price and code (`sku[0]`), the business\n * name as brand, the hard-coded `INR`, the empty description and the missing rating.\n */\nexport function productJsonLd(input: {\n product: Pick<\n ProductDetail,\n 'name' | 'description' | 'images' | 'skus' | 'rating' | 'reviewCount' | 'fulfillmentMode'\n >;\n /** The page's canonical URL. */\n url: string;\n /** ISO 4217 code of the business's currency, e.g. `INR`. Never assumed. */\n currency: string;\n /** The product's own brand (from `catalog.brands()`), never the business name. */\n brandName?: string | null;\n}): JsonLd {\n const { product, url } = input;\n const skus = sellableSkus(product.skus);\n const origin = originOf(url);\n const images = product.images.flatMap((image) => {\n const absolute = absoluteUrl(origin, image.url);\n return absolute ? [absolute] : [];\n });\n const brandName = input.brandName?.trim();\n const offers = productOffers(skus, {\n url,\n currency: input.currency,\n madeToOrder:\n product.fulfillmentMode === 'made_to_order_advance' ||\n product.fulfillmentMode === 'custom_fixed_price',\n });\n const [onlySku] = skus.length === 1 ? skus : [];\n return {\n '@context': CONTEXT,\n '@type': 'Product',\n name: product.name,\n url,\n ...descriptionField(product.description),\n ...(images.length ? { image: images } : {}),\n ...(onlySku ? { sku: onlySku.code } : {}),\n ...(brandName ? { brand: { '@type': 'Brand', name: brandName } } : {}),\n ...(product.reviewCount > 0\n ? {\n aggregateRating: {\n '@type': 'AggregateRating',\n ratingValue: product.rating,\n reviewCount: product.reviewCount,\n bestRating: 5,\n worstRating: 1,\n },\n }\n : {}),\n ...(offers ? { offers } : {}),\n };\n}\n\n/** Framework-neutral page metadata; the app maps it to its own head tags or metadata API. */\nexport interface PageMetadata {\n /** `\"<page title> | <business name>\"`, or whichever of the two exists; `''` when neither. */\n title: string;\n /** Plain text, at most `descriptionLength` characters; null when there is none. */\n description: string | null;\n /** `origin` + the canonical path; null without an origin. */\n canonicalUrl: string | null;\n /** The page image, else the business logo, absolute on `origin`; null when neither. */\n imageUrl: string | null;\n siteName: string;\n}\n\n/**\n * Title, description, canonical URL and image of a page. The description is the page's, else\n * the business's, as plain text cut at a word to `descriptionLength` (default 160, ADR-025).\n * Pass the **canonical** path the app builds (with `slugify`), not the requested one.\n * Fixes the old `generateCommonMetadata`: the \"Whitelabel Apps\" title fallback, the full\n * description in every tag, the requested slug as canonical, and the made-up `twitter:site`.\n */\nexport function pageMetadata(input: {\n origin: string;\n business: { name: string | null; description: string | null; logo: string | null };\n title?: string | null;\n description?: string | null;\n image?: string | null;\n path?: string | null;\n descriptionLength?: number;\n}): PageMetadata {\n const siteName = input.business.name?.trim() ?? '';\n const pageTitle = input.title?.trim() ?? '';\n const text = htmlToText(input.description) || htmlToText(input.business.description);\n const origin = input.origin.trim().replace(/\\/+$/, '');\n const image = input.image?.trim() || input.business.logo;\n return {\n title: [pageTitle, siteName].filter(Boolean).join(' | '),\n description: text ? truncateText(text, input.descriptionLength ?? 160) : null,\n canonicalUrl: origin ? `${origin}/${(input.path ?? '').trim().replace(/^\\/+/, '')}` : null,\n imageUrl: absoluteUrl(origin, image) ?? null,\n siteName,\n };\n}\n","/**\n * Input checks for email, phone and OTP. Each accepts only what the backend accepts at login,\n * so a shopper is never told \"valid\" and then refused (ADR-025).\n */\n\nconst LOCAL_PART = /^[A-Za-z0-9_+-]+(\\.[A-Za-z0-9_+-]+)*$/;\nconst DOMAIN_LABEL = /^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$/;\nconst TOP_LABEL = /^[A-Za-z]{2,}$/;\nconst E164 = /^\\+[1-9]\\d{6,14}$/;\n\n/**\n * An email address the backend's login accepts: at most 254 characters; a local part of 1–64\n * `[A-Za-z0-9_.+-]` with no leading, trailing or double dot; a domain of two or more labels,\n * the last of at least 2 letters. Leading and trailing spaces are ignored.\n * Replaces four different storefront checks, none of which matched the backend's login pattern\n * (`\\S+@\\S+\\.\\S+` accepted `a@b@c.d`).\n */\nexport function isEmail(value: string | null | undefined): boolean {\n const email = (value ?? '').trim();\n if (email.length === 0 || email.length > 254) return false;\n const at = email.indexOf('@');\n if (at < 1 || at !== email.lastIndexOf('@')) return false;\n const local = email.slice(0, at);\n if (local.length > 64 || !LOCAL_PART.test(local)) return false;\n const labels = email.slice(at + 1).split('.');\n if (labels.length < 2) return false;\n if (!labels.every((label) => label.length <= 63 && DOMAIN_LABEL.test(label))) return false;\n return TOP_LABEL.test(labels[labels.length - 1]!);\n}\n\n/**\n * A phone number in E.164 form (`+` and 7–15 digits, no leading 0), as the backend stores it.\n * Fixes the storefront's \"more than 3 digits\" check, which accepted a dial code plus one digit.\n */\nexport function isPhone(value: string | null | undefined): boolean {\n return E164.test((value ?? '').trim());\n}\n\n/**\n * The E.164 form of what a phone input gives, or null when it cannot be one: spaces, `-`, `.`,\n * `(` and `)` are removed and a `00` prefix becomes `+`; a number without `+` needs `dialCode`\n * (e.g. `'91'`), and loses one leading trunk `0` before it is prefixed. A country is never\n * assumed (the storefront's gift-card form assumed India).\n */\nexport function toE164(value: string, options: { dialCode?: string } = {}): string | null {\n let digits = value.replace(/[\\s\\-.()]/g, '');\n if (digits.startsWith('00')) digits = `+${digits.slice(2)}`;\n if (!digits.startsWith('+')) {\n const dialCode = options.dialCode?.trim().replace(/^\\+/, '');\n if (!dialCode || !/^\\d{1,4}$/.test(dialCode)) return null;\n digits = `+${dialCode}${digits.startsWith('0') ? digits.slice(1) : digits}`;\n }\n return isPhone(digits) ? digits : null;\n}\n\n/** A one-time password: exactly 6 digits (the backend always issues 6). */\nexport function isOtp(value: string | null | undefined): boolean {\n return /^\\d{6}$/.test((value ?? '').trim());\n}\n","/**\n * Video links: YouTube and Instagram embeds, thumbnails, and \"is this a video file\". URLs are\n * parsed by hand (no `URL`: not every platform has it, ADR-009). Replaces three different\n * storefront YouTube parsers, none of which anchored the host.\n */\n\ninterface UrlParts {\n host: string;\n segments: string[];\n query: string;\n}\n\nconst URL_PARTS = /^(?:([a-z][a-z0-9+.-]*):\\/\\/|\\/\\/)?([^/?#]*)([^?#]*)(?:\\?([^#]*))?/i;\nconst YOUTUBE_HOSTS = new Set([\n 'youtube.com',\n 'www.youtube.com',\n 'm.youtube.com',\n 'music.youtube.com',\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n]);\nconst INSTAGRAM_HOSTS = new Set(['instagram.com', 'www.instagram.com', 'm.instagram.com']);\nconst YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;\nconst YOUTUBE_ID_PATHS = new Set(['embed', 'shorts', 'live', 'v']);\nconst INSTAGRAM_KINDS: Record<string, string> = { reel: 'reel', reels: 'reel', p: 'p', tv: 'tv' };\nconst VIDEO_EXTENSIONS = new Set(['mp4', 'mov', 'avi', 'wmv', 'webm', 'ogg', 'm4v']);\n\n/** Host (lower case, no port), path segments and query of an http(s) or scheme-less URL. */\nfunction urlParts(url: string | null | undefined): UrlParts | null {\n const match = URL_PARTS.exec((url ?? '').trim());\n if (!match) return null;\n const [, scheme, authority = '', path = '', query = ''] = match;\n if (scheme !== undefined && !/^https?$/i.test(scheme)) return null;\n if (authority.includes('@')) return null;\n return {\n host: authority.replace(/:\\d*$/, '').toLowerCase(),\n segments: path.split('/').filter((segment) => segment !== ''),\n query,\n };\n}\n\nfunction queryValue(query: string, key: string): string | null {\n for (const pair of query.split('&')) {\n const eq = pair.indexOf('=');\n if (eq > 0 && pair.slice(0, eq) === key) return pair.slice(eq + 1);\n }\n return null;\n}\n\n/**\n * The 11-character video id of a YouTube link, or null. Accepts, with or without `http(s)://`,\n * the hosts `youtube.com` (also `www.`, `m.`, `music.`), `youtube-nocookie.com` and `youtu.be`\n * (whole host, any case) and the forms `/watch?…v=<id>` (`v` anywhere in the query),\n * `youtu.be/<id>`, `/embed/<id>`, `/shorts/<id>`, `/live/<id>` and `/v/<id>`. A bare id, a\n * playlist or channel link, or a 12-character value is null.\n * Fixes the storefront's parsers missing `watch?list=…&v=` and Shorts, matching any host that\n * contains `youtube.com`, and taking the first 11 characters of longer values.\n */\nexport function youtubeVideoId(url: string | null | undefined): string | null {\n const parts = urlParts(url);\n if (!parts) return null;\n const { host, segments, query } = parts;\n let id: string | null = null;\n if (host === 'youtu.be') {\n if (segments.length === 1) id = segments[0]!;\n } else if (YOUTUBE_HOSTS.has(host)) {\n if (segments.length === 1 && segments[0] === 'watch') id = queryValue(query, 'v');\n else if (segments.length === 2 && YOUTUBE_ID_PATHS.has(segments[0]!)) id = segments[1]!;\n }\n return id !== null && YOUTUBE_ID.test(id) ? id : null;\n}\n\n/**\n * The embed URL of a YouTube link: `https://www.youtube.com/embed/<id>?rel=0` (no unrelated\n * videos after the merchant's, ADR-025), plus `&autoplay=1` when asked. Null when not a video.\n */\nexport function youtubeEmbedUrl(url: string, options: { autoplay?: boolean } = {}): string | null {\n const id = youtubeVideoId(url);\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}?rel=0${options.autoplay ? '&autoplay=1' : ''}`;\n}\n\n/**\n * `https://www.youtube.com/watch?v=<id>`: the form to save to the backend, whose showcase check\n * accepts only a few link forms.\n */\nexport function youtubeWatchUrl(id: string): string {\n return `https://www.youtube.com/watch?v=${id}`;\n}\n\nconst THUMBNAIL_FILES = {\n default: 'default',\n medium: 'mqdefault',\n high: 'hqdefault',\n standard: 'sddefault',\n max: 'maxresdefault',\n} as const;\n\n/** The thumbnail image of a YouTube link (`high` quality unless asked), or null. */\nexport function youtubeThumbnailUrl(\n url: string,\n quality: keyof typeof THUMBNAIL_FILES = 'high',\n): string | null {\n const id = youtubeVideoId(url);\n return id ? `https://img.youtube.com/vi/${id}/${THUMBNAIL_FILES[quality]}.jpg` : null;\n}\n\n/**\n * The embed URL of an Instagram reel, post or TV link\n * (`instagram.com/(reel|reels|p|tv)/<code>` → `https://www.instagram.com/<reel|p|tv>/<code>/embed`),\n * or null. The host is matched whole, like YouTube's.\n */\nexport function instagramEmbedUrl(url: string | null | undefined): string | null {\n const parts = urlParts(url);\n if (!parts || !INSTAGRAM_HOSTS.has(parts.host)) return null;\n const [kind, code] = parts.segments;\n const embedKind = kind === undefined ? undefined : INSTAGRAM_KINDS[kind];\n if (!embedKind || code === undefined || !/^[A-Za-z0-9_-]+$/.test(code)) return null;\n return `https://www.instagram.com/${embedKind}/${code}/embed`;\n}\n\n/** How to embed a video link: YouTube first, then Instagram; null for anything else. */\nexport function videoEmbed(\n url: string,\n options: { autoplay?: boolean } = {},\n): { provider: 'youtube' | 'instagram'; embedUrl: string } | null {\n const youtube = youtubeEmbedUrl(url, options);\n if (youtube) return { provider: 'youtube', embedUrl: youtube };\n const instagram = instagramEmbedUrl(url);\n return instagram ? { provider: 'instagram', embedUrl: instagram } : null;\n}\n\n/**\n * True when the link's **path** ends in a video file extension (`.mp4 .mov .avi .wmv .webm .ogg\n * .m4v`, any case); query and hash are ignored. Fixes the storefront matching the extension\n * anywhere in the URL (`/.mov/photo.jpg`, `?f=.webm`).\n */\nexport function isVideoFileUrl(url: string | null | undefined): boolean {\n const segments = urlParts(url)?.segments ?? [];\n const last = segments[segments.length - 1];\n if (last === undefined) return false;\n const dot = last.lastIndexOf('.');\n return dot >= 0 && VIDEO_EXTENSIONS.has(last.slice(dot + 1).toLowerCase());\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-NNKXIBK4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}