@snowcone-app/sdk 0.1.12 → 0.1.13
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/README.md +99 -63
- package/dist/{chunk-UJFJ7REN.js → chunk-6MV7TDTM.js} +186 -18
- package/dist/index.cjs +756 -170
- package/dist/index.d.cts +130 -29
- package/dist/index.d.ts +130 -29
- package/dist/index.js +561 -144
- package/dist/react.cjs +203 -20
- package/dist/react.d.cts +13 -1
- package/dist/react.d.ts +13 -1
- package/dist/react.js +18 -3
- package/dist/{websocket-B8_XAwWx.d.ts → websocket-Dum3OooZ.d.cts} +55 -3
- package/dist/{websocket-B8_XAwWx.d.cts → websocket-Dum3OooZ.d.ts} +55 -3
- package/package.json +21 -11
- package/dist/chunk-HOYSZQET.js +0 -476
- package/dist/chunk-IIUCW2O4.js +0 -457
- package/dist/websocket-GXMYofWp.d.cts +0 -330
- package/dist/websocket-GXMYofWp.d.ts +0 -330
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
DEFAULT_ARTWORK_URL: () => DEFAULT_ARTWORK_URL,
|
|
39
39
|
DEFAULT_ASPECT_RATIO: () => DEFAULT_ASPECT_RATIO,
|
|
40
40
|
DEFAULT_COLOR: () => DEFAULT_COLOR,
|
|
41
|
+
DEFAULT_MOCKUP_BASE: () => DEFAULT_MOCKUP_BASE,
|
|
41
42
|
DEFAULT_PLACEMENT_DIMENSIONS: () => DEFAULT_PLACEMENT_DIMENSIONS,
|
|
42
43
|
Elements: () => Elements,
|
|
43
44
|
ErrorManager: () => ErrorManager,
|
|
@@ -62,9 +63,9 @@ __export(index_exports, {
|
|
|
62
63
|
VueAdapter: () => VueAdapter,
|
|
63
64
|
adapterRegistry: () => adapterRegistry,
|
|
64
65
|
autoRegister: () => autoRegister,
|
|
66
|
+
buildMockupUrl: () => buildMockupUrl2,
|
|
65
67
|
componentRegistry: () => componentRegistry,
|
|
66
68
|
computeDisabledChoices: () => computeDisabledChoices,
|
|
67
|
-
config: () => config,
|
|
68
69
|
createAddToCartEvent: () => createAddToCartEvent,
|
|
69
70
|
createAddToCartHandler: () => createAddToCartHandler,
|
|
70
71
|
createCartDetail: () => createCartDetail,
|
|
@@ -104,6 +105,7 @@ __export(index_exports, {
|
|
|
104
105
|
getEffectiveAlignment: () => getEffectiveAlignment,
|
|
105
106
|
getIncompleteSelectionMessage: () => getIncompleteSelectionMessage,
|
|
106
107
|
getMissingSelections: () => getMissingSelections,
|
|
108
|
+
getMockupUrl: () => getMockupUrl,
|
|
107
109
|
getOptionRenderType: () => getOptionRenderType,
|
|
108
110
|
getPricePreview: () => getPricePreview,
|
|
109
111
|
getProduct: () => getProduct,
|
|
@@ -121,7 +123,6 @@ __export(index_exports, {
|
|
|
121
123
|
prepareOptionRenderData: () => prepareOptionRenderData,
|
|
122
124
|
registerStandardComponents: () => registerStandardComponents,
|
|
123
125
|
resolveBestCombination: () => resolveBestCombination,
|
|
124
|
-
resolveMockupConfig: () => resolveMockupConfig,
|
|
125
126
|
resolveMockupId: () => resolveMockupId,
|
|
126
127
|
resolveVariantId: () => resolveVariantId,
|
|
127
128
|
retryOperation: () => retryOperation,
|
|
@@ -185,10 +186,19 @@ var CatalogProductSchema = import_zod.z.object({
|
|
|
185
186
|
).optional(),
|
|
186
187
|
placements: import_zod.z.array(
|
|
187
188
|
import_zod.z.object({
|
|
189
|
+
// The one additive multi-placement field (rev 5 P1): a URL-safe slug
|
|
190
|
+
// used as the public param key (`asset.front`). Optional — older catalog
|
|
191
|
+
// docs predate it.
|
|
192
|
+
key: import_zod.z.string().optional(),
|
|
188
193
|
label: import_zod.z.string(),
|
|
189
|
-
type:
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
// Live data has placements with `type: null` (and ingestion may omit it
|
|
195
|
+
// entirely), not just "image"/"color". The previous strict enum threw on
|
|
196
|
+
// real catalog docs. Accept image | color | null | absent.
|
|
197
|
+
type: import_zod.z.enum(["image", "color"]).nullable().optional(),
|
|
198
|
+
// Live data carries width/height of 0 (and `.int()` rejected nothing of
|
|
199
|
+
// import here). Drop `.int()` and allow nullable so 0 / null / float pass.
|
|
200
|
+
width: import_zod.z.number().nullable().optional(),
|
|
201
|
+
height: import_zod.z.number().nullable().optional(),
|
|
192
202
|
defaultScaleMode: import_zod.z.enum(["fill", "fit"]).optional(),
|
|
193
203
|
fitMarginTop: import_zod.z.number().int().optional(),
|
|
194
204
|
fitMarginRight: import_zod.z.number().int().optional(),
|
|
@@ -209,7 +219,7 @@ var SignatureCache = class {
|
|
|
209
219
|
memoryCache = /* @__PURE__ */ new Map();
|
|
210
220
|
maxMemoryEntries;
|
|
211
221
|
maxLocalStorageEntries;
|
|
212
|
-
localStorageKey = "
|
|
222
|
+
localStorageKey = "snowcone_signature_cache";
|
|
213
223
|
localStorageAvailable;
|
|
214
224
|
constructor(maxMemoryEntries = 500, maxLocalStorageEntries = 100) {
|
|
215
225
|
this.maxMemoryEntries = maxMemoryEntries;
|
|
@@ -306,8 +316,8 @@ var SignatureCache = class {
|
|
|
306
316
|
};
|
|
307
317
|
|
|
308
318
|
// src/mockup/service.ts
|
|
309
|
-
var DEFAULT_IMAGE_URL = typeof process !== "undefined" && process.env?.
|
|
310
|
-
var DEFAULT_SIGNER_URL = typeof process !== "undefined" && process.env?.
|
|
319
|
+
var DEFAULT_IMAGE_URL = typeof process !== "undefined" && process.env?.SNOWCONE_IMAGE_URL || "https://cdn.snowcone.app";
|
|
320
|
+
var DEFAULT_SIGNER_URL = typeof process !== "undefined" && process.env?.SNOWCONE_SIGNER_URL || "https://s.snowcone.app/sign";
|
|
311
321
|
var RATE_LIMIT_PER_MINUTE = 450;
|
|
312
322
|
var RATE_LIMIT_PER_SECOND = 20;
|
|
313
323
|
var MockupServiceImpl = class {
|
|
@@ -315,11 +325,11 @@ var MockupServiceImpl = class {
|
|
|
315
325
|
cache;
|
|
316
326
|
rateLimitState;
|
|
317
327
|
fetch;
|
|
318
|
-
constructor(
|
|
328
|
+
constructor(config, customFetch) {
|
|
319
329
|
this.config = {
|
|
320
|
-
imageUrl:
|
|
321
|
-
signerUrl:
|
|
322
|
-
|
|
330
|
+
imageUrl: config.imageUrl || DEFAULT_IMAGE_URL,
|
|
331
|
+
signerUrl: config.signerUrl || DEFAULT_SIGNER_URL,
|
|
332
|
+
shop: config.shop || typeof process !== "undefined" && process.env?.SNOWCONE_SHOP_ID || ""
|
|
323
333
|
};
|
|
324
334
|
this.cache = new SignatureCache();
|
|
325
335
|
this.fetch = customFetch || fetch.bind(globalThis);
|
|
@@ -332,12 +342,21 @@ var MockupServiceImpl = class {
|
|
|
332
342
|
if (!options.design || !options.product) {
|
|
333
343
|
throw new Error("Missing required options: design and product are required");
|
|
334
344
|
}
|
|
335
|
-
if (!this.config.
|
|
336
|
-
throw new Error("
|
|
345
|
+
if (!this.config.shop) {
|
|
346
|
+
throw new Error("Shop ID is required for mockup generation");
|
|
347
|
+
}
|
|
348
|
+
for (const element of options.design) {
|
|
349
|
+
if (element.type === "image" && element.imageUrl) {
|
|
350
|
+
if (element.imageUrl.startsWith("blob:")) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`[snowcone-sdk] Cannot generate mockup: design element for placement "${element.placement}" uses a blob URL ("${element.imageUrl.slice(0, 60)}..."). Blob URLs are local to this browser tab and cannot be downloaded by the mockup rendering server. Upload the image to a publicly accessible URL or convert it to a data URL before requesting a mockup.`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
337
356
|
}
|
|
338
357
|
this.checkRateLimits();
|
|
339
358
|
const relativeUrl = this.buildMockupUrlWithParams(options);
|
|
340
|
-
const cacheKey = `${relativeUrl}:${this.config.
|
|
359
|
+
const cacheKey = `${relativeUrl}:${this.config.shop}`;
|
|
341
360
|
const cachedUrl = this.cache.get(cacheKey);
|
|
342
361
|
if (cachedUrl) {
|
|
343
362
|
return cachedUrl;
|
|
@@ -422,17 +441,12 @@ var MockupServiceImpl = class {
|
|
|
422
441
|
}
|
|
423
442
|
}
|
|
424
443
|
async getSignedUrl(relativeUrl) {
|
|
425
|
-
const urlWithAccountId = relativeUrl + (relativeUrl.includes("?") ? "&" : "?") + `
|
|
444
|
+
const urlWithAccountId = relativeUrl + (relativeUrl.includes("?") ? "&" : "?") + `shop=${encodeURIComponent(this.config.shop)}`;
|
|
426
445
|
const cacheKey = urlWithAccountId;
|
|
427
446
|
const cachedUrl = this.cache.get(cacheKey);
|
|
428
447
|
if (cachedUrl) {
|
|
429
448
|
return cachedUrl;
|
|
430
449
|
}
|
|
431
|
-
if (this.config.signerUrl?.includes("localhost") || this.config.signerUrl === "mock") {
|
|
432
|
-
const signedUrl = this.config.imageUrl + urlWithAccountId + "&sig=bypass-sig-for-k6-load-test";
|
|
433
|
-
this.cache.set(cacheKey, signedUrl);
|
|
434
|
-
return signedUrl;
|
|
435
|
-
}
|
|
436
450
|
const signerUrl = new URL(this.config.signerUrl);
|
|
437
451
|
signerUrl.searchParams.set("url", urlWithAccountId);
|
|
438
452
|
try {
|
|
@@ -488,6 +502,456 @@ function createDevFetcher(baseUrl) {
|
|
|
488
502
|
};
|
|
489
503
|
}
|
|
490
504
|
|
|
505
|
+
// ../mockup-url/src/index.ts
|
|
506
|
+
var PARAMS = {
|
|
507
|
+
shop: "shop",
|
|
508
|
+
asset: "asset",
|
|
509
|
+
assets: "assets",
|
|
510
|
+
placement: "placement",
|
|
511
|
+
variant: "variant",
|
|
512
|
+
mockup: "mockup",
|
|
513
|
+
width: "width",
|
|
514
|
+
grain: "grain",
|
|
515
|
+
aspect: "aspect",
|
|
516
|
+
/** Live preview/calibration mask overrides (not cached). */
|
|
517
|
+
maskOverrides: "maskOverrides",
|
|
518
|
+
/**
|
|
519
|
+
* Render-only option picks (the `affectsCombinations:false` attributes), as a
|
|
520
|
+
* base64 JSON map `{ attributeName: choiceLabel }`. rendercenter already parses
|
|
521
|
+
* this and resolves per-choice underlays from it; the builder emits it so the
|
|
522
|
+
* edge resolver can forward render-only picks (e.g. a cap's Crown/Strap color)
|
|
523
|
+
* without changing rendercenter's grammar.
|
|
524
|
+
*/
|
|
525
|
+
optionSelections: "optionSelections",
|
|
526
|
+
signature: "signature",
|
|
527
|
+
seal: "seal"
|
|
528
|
+
};
|
|
529
|
+
function asSimpleAsset(design) {
|
|
530
|
+
if (design.length !== 1) return null;
|
|
531
|
+
const el = design[0];
|
|
532
|
+
if (el.type === "color" || el.hex) return null;
|
|
533
|
+
if (!el.imageUrl) return null;
|
|
534
|
+
if (el.position || el.size || el.tiles != null) return null;
|
|
535
|
+
if (el.alignment && el.alignment !== "center") return null;
|
|
536
|
+
return { imageUrl: el.imageUrl, placement: el.placement };
|
|
537
|
+
}
|
|
538
|
+
var ALLOWED_WIDTHS = [
|
|
539
|
+
400,
|
|
540
|
+
600,
|
|
541
|
+
800,
|
|
542
|
+
1e3,
|
|
543
|
+
1200,
|
|
544
|
+
1400,
|
|
545
|
+
1600,
|
|
546
|
+
1800,
|
|
547
|
+
2e3,
|
|
548
|
+
2500,
|
|
549
|
+
3e3,
|
|
550
|
+
4e3
|
|
551
|
+
];
|
|
552
|
+
function normalizeWidth(width) {
|
|
553
|
+
for (let i = ALLOWED_WIDTHS.length - 1; i >= 0; i--) {
|
|
554
|
+
if (ALLOWED_WIDTHS[i] <= width) return ALLOWED_WIDTHS[i];
|
|
555
|
+
}
|
|
556
|
+
return ALLOWED_WIDTHS[0];
|
|
557
|
+
}
|
|
558
|
+
function normalizeDesignElements(design) {
|
|
559
|
+
return design.map((element) => {
|
|
560
|
+
const normalized = {};
|
|
561
|
+
const keys = Object.keys(element).sort();
|
|
562
|
+
for (const key of keys) {
|
|
563
|
+
if (key === "width" || key === "height" || key === "type") continue;
|
|
564
|
+
if (key === "alignment" && element[key] === "center") {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const value = element[key];
|
|
568
|
+
if (value !== void 0 && value !== null) normalized[key] = value;
|
|
569
|
+
}
|
|
570
|
+
return normalized;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function buildMockupUrl(options, cfg) {
|
|
574
|
+
for (const element of options.design) {
|
|
575
|
+
if (element.type === "image" && element.imageUrl?.startsWith("blob:")) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`[snowcone] Cannot generate mockup: design element for placement "${element.placement}" uses a blob URL ("${element.imageUrl.slice(0, 60)}..."). Blob URLs are local to this browser tab and cannot be downloaded by the mockup rendering server. Upload the image to a publicly accessible URL or convert it to a data URL before requesting a mockup.`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const width = normalizeWidth(options.width);
|
|
582
|
+
const base = cfg.mockupBaseUrl.replace(/\/+$/, "");
|
|
583
|
+
const path = `/${encodeURIComponent(options.productId)}`;
|
|
584
|
+
let queryString = `${PARAMS.shop}=${encodeURIComponent(cfg.shop)}`;
|
|
585
|
+
const simple = asSimpleAsset(options.design);
|
|
586
|
+
if (simple) {
|
|
587
|
+
queryString += `&${PARAMS.asset}=${encodeURIComponent(simple.imageUrl)}`;
|
|
588
|
+
if (simple.placement) {
|
|
589
|
+
queryString += `&${PARAMS.placement}=${encodeURIComponent(simple.placement)}`;
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
const encodedAssets = encodeURIComponent(
|
|
593
|
+
btoa(JSON.stringify(normalizeDesignElements(options.design)))
|
|
594
|
+
);
|
|
595
|
+
queryString += `&${PARAMS.assets}=${encodedAssets}`;
|
|
596
|
+
}
|
|
597
|
+
queryString += `&${PARAMS.variant}=${encodeURIComponent(options.variantId)}`;
|
|
598
|
+
queryString += `&${PARAMS.mockup}=${encodeURIComponent(options.mockupId)}`;
|
|
599
|
+
queryString += `&${PARAMS.width}=${width}`;
|
|
600
|
+
if (options.optionSelections && Object.keys(options.optionSelections).length > 0) {
|
|
601
|
+
const sorted = {};
|
|
602
|
+
for (const k of Object.keys(options.optionSelections).sort()) {
|
|
603
|
+
sorted[k] = options.optionSelections[k];
|
|
604
|
+
}
|
|
605
|
+
const optionSelectionsBase64 = btoa(JSON.stringify(sorted));
|
|
606
|
+
queryString += `&${PARAMS.optionSelections}=${encodeURIComponent(
|
|
607
|
+
optionSelectionsBase64
|
|
608
|
+
)}`;
|
|
609
|
+
}
|
|
610
|
+
if (options.effects?.grain) {
|
|
611
|
+
queryString += `&${PARAMS.grain}=${options.effects.grain}`;
|
|
612
|
+
}
|
|
613
|
+
if (options.aspect && options.aspect !== "16:9") {
|
|
614
|
+
queryString += `&${PARAMS.aspect}=${encodeURIComponent(options.aspect)}`;
|
|
615
|
+
}
|
|
616
|
+
if (options.maskOverrides && options.maskOverrides.length > 0) {
|
|
617
|
+
const maskOverridesBase64 = btoa(JSON.stringify(options.maskOverrides));
|
|
618
|
+
queryString += `&${PARAMS.maskOverrides}=${encodeURIComponent(maskOverridesBase64)}`;
|
|
619
|
+
}
|
|
620
|
+
return `${base}${path}?${queryString}`;
|
|
621
|
+
}
|
|
622
|
+
var PARAM_PREFIX = {
|
|
623
|
+
asset: "asset.",
|
|
624
|
+
color: "color.",
|
|
625
|
+
tile: "tile.",
|
|
626
|
+
align: "align.",
|
|
627
|
+
opt: "opt."
|
|
628
|
+
};
|
|
629
|
+
var DEFAULT_PUBLIC_BASE_URL = "https://img.snowcone.app";
|
|
630
|
+
function buildPublicMockupUrl(options, cfg = {}) {
|
|
631
|
+
const base = (cfg.baseUrl ?? DEFAULT_PUBLIC_BASE_URL).replace(/\/+$/, "");
|
|
632
|
+
const path = `/${encodeURIComponent(options.productCode)}`;
|
|
633
|
+
const enc = encodeURIComponent;
|
|
634
|
+
const parts = [];
|
|
635
|
+
parts.push(`${PARAMS.shop}=${enc(options.shop)}`);
|
|
636
|
+
if (options.design) {
|
|
637
|
+
for (const key of Object.keys(options.design).sort()) {
|
|
638
|
+
const fill = options.design[key];
|
|
639
|
+
const k = enc(key);
|
|
640
|
+
if (typeof fill === "string") {
|
|
641
|
+
parts.push(`${PARAM_PREFIX.asset}${k}=${enc(fill)}`);
|
|
642
|
+
} else if ("color" in fill) {
|
|
643
|
+
parts.push(`${PARAM_PREFIX.color}${k}=${enc(fill.color)}`);
|
|
644
|
+
} else {
|
|
645
|
+
parts.push(`${PARAM_PREFIX.asset}${k}=${enc(fill.src)}`);
|
|
646
|
+
if (fill.tile !== void 0) {
|
|
647
|
+
parts.push(`${PARAM_PREFIX.tile}${k}=${fill.tile}`);
|
|
648
|
+
}
|
|
649
|
+
if (fill.align !== void 0) {
|
|
650
|
+
parts.push(`${PARAM_PREFIX.align}${k}=${enc(fill.align)}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} else if (options.assetUrl !== void 0) {
|
|
655
|
+
parts.push(`${PARAMS.asset}=${enc(options.assetUrl)}`);
|
|
656
|
+
}
|
|
657
|
+
if (options.placement !== void 0) {
|
|
658
|
+
parts.push(`${PARAMS.placement}=${enc(options.placement)}`);
|
|
659
|
+
}
|
|
660
|
+
if (options.options) {
|
|
661
|
+
for (const attr of Object.keys(options.options).sort()) {
|
|
662
|
+
parts.push(`${PARAM_PREFIX.opt}${enc(attr)}=${enc(options.options[attr])}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (options.variant !== void 0) {
|
|
666
|
+
parts.push(`${PARAMS.variant}=${enc(options.variant)}`);
|
|
667
|
+
}
|
|
668
|
+
if (options.mockup !== void 0) {
|
|
669
|
+
parts.push(`${PARAMS.mockup}=${enc(options.mockup)}`);
|
|
670
|
+
}
|
|
671
|
+
if (options.width !== void 0) parts.push(`${PARAMS.width}=${options.width}`);
|
|
672
|
+
if (options.aspect !== void 0 && options.aspect !== "16:9") {
|
|
673
|
+
parts.push(`${PARAMS.aspect}=${enc(options.aspect)}`);
|
|
674
|
+
}
|
|
675
|
+
return `${base}${path}?${parts.join("&")}`;
|
|
676
|
+
}
|
|
677
|
+
function canonicalForSig(url) {
|
|
678
|
+
const hashIdx = url.indexOf("#");
|
|
679
|
+
const noHash = hashIdx === -1 ? url : url.slice(0, hashIdx);
|
|
680
|
+
const qIdx = noHash.indexOf("?");
|
|
681
|
+
const beforeQuery = qIdx === -1 ? noHash : noHash.slice(0, qIdx);
|
|
682
|
+
const rawQuery = qIdx === -1 ? "" : noHash.slice(qIdx + 1);
|
|
683
|
+
let pathname;
|
|
684
|
+
const schemeMatch = beforeQuery.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//);
|
|
685
|
+
if (schemeMatch) {
|
|
686
|
+
const afterScheme = beforeQuery.slice(schemeMatch[0].length);
|
|
687
|
+
const slashIdx = afterScheme.indexOf("/");
|
|
688
|
+
pathname = slashIdx === -1 ? "/" : afterScheme.slice(slashIdx);
|
|
689
|
+
} else {
|
|
690
|
+
pathname = beforeQuery;
|
|
691
|
+
}
|
|
692
|
+
const pairs = [];
|
|
693
|
+
if (rawQuery.length > 0) {
|
|
694
|
+
for (const segment of rawQuery.split("&")) {
|
|
695
|
+
if (segment.length === 0) continue;
|
|
696
|
+
const eq = segment.indexOf("=");
|
|
697
|
+
const rawKey = eq === -1 ? segment : segment.slice(0, eq);
|
|
698
|
+
const rawValue = eq === -1 ? "" : segment.slice(eq + 1);
|
|
699
|
+
const key = decodeURIComponent(rawKey);
|
|
700
|
+
if (key === "signature") continue;
|
|
701
|
+
pairs.push({ key, raw: rawValue });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
pairs.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
705
|
+
const canonicalQuery = pairs.map((p) => `${p.key}=${p.raw}`).join("&");
|
|
706
|
+
return `${pathname}?${canonicalQuery}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/mockup/hmac.ts
|
|
710
|
+
var K = new Uint32Array([
|
|
711
|
+
1116352408,
|
|
712
|
+
1899447441,
|
|
713
|
+
3049323471,
|
|
714
|
+
3921009573,
|
|
715
|
+
961987163,
|
|
716
|
+
1508970993,
|
|
717
|
+
2453635748,
|
|
718
|
+
2870763221,
|
|
719
|
+
3624381080,
|
|
720
|
+
310598401,
|
|
721
|
+
607225278,
|
|
722
|
+
1426881987,
|
|
723
|
+
1925078388,
|
|
724
|
+
2162078206,
|
|
725
|
+
2614888103,
|
|
726
|
+
3248222580,
|
|
727
|
+
3835390401,
|
|
728
|
+
4022224774,
|
|
729
|
+
264347078,
|
|
730
|
+
604807628,
|
|
731
|
+
770255983,
|
|
732
|
+
1249150122,
|
|
733
|
+
1555081692,
|
|
734
|
+
1996064986,
|
|
735
|
+
2554220882,
|
|
736
|
+
2821834349,
|
|
737
|
+
2952996808,
|
|
738
|
+
3210313671,
|
|
739
|
+
3336571891,
|
|
740
|
+
3584528711,
|
|
741
|
+
113926993,
|
|
742
|
+
338241895,
|
|
743
|
+
666307205,
|
|
744
|
+
773529912,
|
|
745
|
+
1294757372,
|
|
746
|
+
1396182291,
|
|
747
|
+
1695183700,
|
|
748
|
+
1986661051,
|
|
749
|
+
2177026350,
|
|
750
|
+
2456956037,
|
|
751
|
+
2730485921,
|
|
752
|
+
2820302411,
|
|
753
|
+
3259730800,
|
|
754
|
+
3345764771,
|
|
755
|
+
3516065817,
|
|
756
|
+
3600352804,
|
|
757
|
+
4094571909,
|
|
758
|
+
275423344,
|
|
759
|
+
430227734,
|
|
760
|
+
506948616,
|
|
761
|
+
659060556,
|
|
762
|
+
883997877,
|
|
763
|
+
958139571,
|
|
764
|
+
1322822218,
|
|
765
|
+
1537002063,
|
|
766
|
+
1747873779,
|
|
767
|
+
1955562222,
|
|
768
|
+
2024104815,
|
|
769
|
+
2227730452,
|
|
770
|
+
2361852424,
|
|
771
|
+
2428436474,
|
|
772
|
+
2756734187,
|
|
773
|
+
3204031479,
|
|
774
|
+
3329325298
|
|
775
|
+
]);
|
|
776
|
+
function rotr(x, n) {
|
|
777
|
+
return x >>> n | x << 32 - n;
|
|
778
|
+
}
|
|
779
|
+
function sha256(bytes) {
|
|
780
|
+
const h = new Uint32Array([
|
|
781
|
+
1779033703,
|
|
782
|
+
3144134277,
|
|
783
|
+
1013904242,
|
|
784
|
+
2773480762,
|
|
785
|
+
1359893119,
|
|
786
|
+
2600822924,
|
|
787
|
+
528734635,
|
|
788
|
+
1541459225
|
|
789
|
+
]);
|
|
790
|
+
const bitLen = bytes.length * 8;
|
|
791
|
+
const withOne = bytes.length + 1;
|
|
792
|
+
const total = withOne + (56 - withOne % 64 + 64) % 64 + 8;
|
|
793
|
+
const msg = new Uint8Array(total);
|
|
794
|
+
msg.set(bytes);
|
|
795
|
+
msg[bytes.length] = 128;
|
|
796
|
+
const hi = Math.floor(bitLen / 4294967296);
|
|
797
|
+
const lo = bitLen >>> 0;
|
|
798
|
+
msg[total - 8] = hi >>> 24 & 255;
|
|
799
|
+
msg[total - 7] = hi >>> 16 & 255;
|
|
800
|
+
msg[total - 6] = hi >>> 8 & 255;
|
|
801
|
+
msg[total - 5] = hi & 255;
|
|
802
|
+
msg[total - 4] = lo >>> 24 & 255;
|
|
803
|
+
msg[total - 3] = lo >>> 16 & 255;
|
|
804
|
+
msg[total - 2] = lo >>> 8 & 255;
|
|
805
|
+
msg[total - 1] = lo & 255;
|
|
806
|
+
const w = new Uint32Array(64);
|
|
807
|
+
for (let off = 0; off < total; off += 64) {
|
|
808
|
+
for (let i = 0; i < 16; i++) {
|
|
809
|
+
const j = off + i * 4;
|
|
810
|
+
w[i] = (msg[j] << 24 | msg[j + 1] << 16 | msg[j + 2] << 8 | msg[j + 3]) >>> 0;
|
|
811
|
+
}
|
|
812
|
+
for (let i = 16; i < 64; i++) {
|
|
813
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
814
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
815
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
816
|
+
}
|
|
817
|
+
let a = h[0];
|
|
818
|
+
let b = h[1];
|
|
819
|
+
let c = h[2];
|
|
820
|
+
let d = h[3];
|
|
821
|
+
let e = h[4];
|
|
822
|
+
let f = h[5];
|
|
823
|
+
let g = h[6];
|
|
824
|
+
let hh = h[7];
|
|
825
|
+
for (let i = 0; i < 64; i++) {
|
|
826
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
827
|
+
const ch = e & f ^ ~e & g;
|
|
828
|
+
const t1 = hh + S1 + ch + K[i] + w[i] >>> 0;
|
|
829
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
830
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
831
|
+
const t2 = S0 + maj >>> 0;
|
|
832
|
+
hh = g;
|
|
833
|
+
g = f;
|
|
834
|
+
f = e;
|
|
835
|
+
e = d + t1 >>> 0;
|
|
836
|
+
d = c;
|
|
837
|
+
c = b;
|
|
838
|
+
b = a;
|
|
839
|
+
a = t1 + t2 >>> 0;
|
|
840
|
+
}
|
|
841
|
+
h[0] = h[0] + a >>> 0;
|
|
842
|
+
h[1] = h[1] + b >>> 0;
|
|
843
|
+
h[2] = h[2] + c >>> 0;
|
|
844
|
+
h[3] = h[3] + d >>> 0;
|
|
845
|
+
h[4] = h[4] + e >>> 0;
|
|
846
|
+
h[5] = h[5] + f >>> 0;
|
|
847
|
+
h[6] = h[6] + g >>> 0;
|
|
848
|
+
h[7] = h[7] + hh >>> 0;
|
|
849
|
+
}
|
|
850
|
+
const out = new Uint8Array(32);
|
|
851
|
+
for (let i = 0; i < 8; i++) {
|
|
852
|
+
out[i * 4] = h[i] >>> 24 & 255;
|
|
853
|
+
out[i * 4 + 1] = h[i] >>> 16 & 255;
|
|
854
|
+
out[i * 4 + 2] = h[i] >>> 8 & 255;
|
|
855
|
+
out[i * 4 + 3] = h[i] & 255;
|
|
856
|
+
}
|
|
857
|
+
return out;
|
|
858
|
+
}
|
|
859
|
+
function utf8(str) {
|
|
860
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(str);
|
|
861
|
+
const bytes = [];
|
|
862
|
+
for (let i = 0; i < str.length; i++) {
|
|
863
|
+
let c = str.charCodeAt(i);
|
|
864
|
+
if (c < 128) bytes.push(c);
|
|
865
|
+
else if (c < 2048) {
|
|
866
|
+
bytes.push(192 | c >> 6, 128 | c & 63);
|
|
867
|
+
} else if (c >= 55296 && c <= 56319) {
|
|
868
|
+
const c2 = str.charCodeAt(++i);
|
|
869
|
+
c = 65536 + ((c & 1023) << 10) + (c2 & 1023);
|
|
870
|
+
bytes.push(
|
|
871
|
+
240 | c >> 18,
|
|
872
|
+
128 | c >> 12 & 63,
|
|
873
|
+
128 | c >> 6 & 63,
|
|
874
|
+
128 | c & 63
|
|
875
|
+
);
|
|
876
|
+
} else {
|
|
877
|
+
bytes.push(
|
|
878
|
+
224 | c >> 12,
|
|
879
|
+
128 | c >> 6 & 63,
|
|
880
|
+
128 | c & 63
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return new Uint8Array(bytes);
|
|
885
|
+
}
|
|
886
|
+
function toHex(bytes) {
|
|
887
|
+
let hex = "";
|
|
888
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
889
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
890
|
+
}
|
|
891
|
+
return hex;
|
|
892
|
+
}
|
|
893
|
+
function hmacSha256Hex(secret, message) {
|
|
894
|
+
const blockSize = 64;
|
|
895
|
+
let key = utf8(secret);
|
|
896
|
+
if (key.length > blockSize) key = sha256(key);
|
|
897
|
+
if (key.length < blockSize) {
|
|
898
|
+
const padded = new Uint8Array(blockSize);
|
|
899
|
+
padded.set(key);
|
|
900
|
+
key = padded;
|
|
901
|
+
}
|
|
902
|
+
const oKeyPad = new Uint8Array(blockSize);
|
|
903
|
+
const iKeyPad = new Uint8Array(blockSize);
|
|
904
|
+
for (let i = 0; i < blockSize; i++) {
|
|
905
|
+
oKeyPad[i] = key[i] ^ 92;
|
|
906
|
+
iKeyPad[i] = key[i] ^ 54;
|
|
907
|
+
}
|
|
908
|
+
const msg = utf8(message);
|
|
909
|
+
const inner = new Uint8Array(blockSize + msg.length);
|
|
910
|
+
inner.set(iKeyPad);
|
|
911
|
+
inner.set(msg, blockSize);
|
|
912
|
+
const innerHash = sha256(inner);
|
|
913
|
+
const outer = new Uint8Array(blockSize + innerHash.length);
|
|
914
|
+
outer.set(oKeyPad);
|
|
915
|
+
outer.set(innerHash, blockSize);
|
|
916
|
+
return toHex(sha256(outer));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/mockup/getMockupUrl.ts
|
|
920
|
+
var DEFAULT_MOCKUP_BASE = "https://img.snowcone.app";
|
|
921
|
+
function getMockupUrl(a, b, c) {
|
|
922
|
+
let productCode;
|
|
923
|
+
let opts;
|
|
924
|
+
let legacyAsset;
|
|
925
|
+
if (typeof b === "string") {
|
|
926
|
+
legacyAsset = a;
|
|
927
|
+
productCode = b;
|
|
928
|
+
opts = c ?? {};
|
|
929
|
+
} else {
|
|
930
|
+
productCode = a;
|
|
931
|
+
opts = b;
|
|
932
|
+
}
|
|
933
|
+
const base = (opts.base ?? DEFAULT_MOCKUP_BASE).replace(/\/+$/, "");
|
|
934
|
+
const assetUrl = opts.asset ?? legacyAsset;
|
|
935
|
+
const url = buildPublicMockupUrl(
|
|
936
|
+
{
|
|
937
|
+
productCode,
|
|
938
|
+
shop: opts.shop,
|
|
939
|
+
...opts.design ? { design: opts.design } : {},
|
|
940
|
+
...assetUrl !== void 0 ? { assetUrl } : {},
|
|
941
|
+
...opts.options ? { options: opts.options } : {},
|
|
942
|
+
...opts.width !== void 0 ? { width: opts.width } : {},
|
|
943
|
+
...opts.aspect !== void 0 ? { aspect: opts.aspect } : {},
|
|
944
|
+
...opts.mockup ?? opts.view ? { mockup: opts.mockup ?? opts.view } : {},
|
|
945
|
+
...opts.variant !== void 0 ? { variant: opts.variant } : {},
|
|
946
|
+
...opts.placement !== void 0 ? { placement: opts.placement } : {}
|
|
947
|
+
},
|
|
948
|
+
{ baseUrl: base }
|
|
949
|
+
);
|
|
950
|
+
if (!opts.secret) return url;
|
|
951
|
+
const signature = hmacSha256Hex(opts.secret, canonicalForSig(url)).slice(0, 16);
|
|
952
|
+
return `${url}&signature=${signature}`;
|
|
953
|
+
}
|
|
954
|
+
|
|
491
955
|
// src/state/optionSelection.ts
|
|
492
956
|
function resolveBestCombination(selection, attributes, combinations) {
|
|
493
957
|
const relevant = Object.fromEntries(
|
|
@@ -1158,7 +1622,7 @@ function resolveUrlFromDOM(relativeUrl) {
|
|
|
1158
1622
|
const filename = pathParts[pathParts.length - 1];
|
|
1159
1623
|
if (!filename) return null;
|
|
1160
1624
|
const filenameWithoutExt = filename.replace(/\.[^.]+$/, "");
|
|
1161
|
-
const images = document.querySelectorAll("img");
|
|
1625
|
+
const images = Array.from(document.querySelectorAll("img"));
|
|
1162
1626
|
for (const img of images) {
|
|
1163
1627
|
const src = img.getAttribute("src");
|
|
1164
1628
|
if (!src) continue;
|
|
@@ -1170,6 +1634,9 @@ function resolveUrlFromDOM(relativeUrl) {
|
|
|
1170
1634
|
}
|
|
1171
1635
|
function normalizeImageUrl(url) {
|
|
1172
1636
|
if (!url) return null;
|
|
1637
|
+
if (url.startsWith("blob:")) {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1173
1640
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1174
1641
|
return url;
|
|
1175
1642
|
}
|
|
@@ -1188,18 +1655,18 @@ function normalizeImageUrl(url) {
|
|
|
1188
1655
|
const absoluteUrl = new URL(url, window.location.origin).href;
|
|
1189
1656
|
if (absoluteUrl.includes("localhost") || absoluteUrl.includes("127.0.0.1")) {
|
|
1190
1657
|
console.warn(
|
|
1191
|
-
`[
|
|
1658
|
+
`[snowcone-sdk] Artwork URL "${absoluteUrl}" uses localhost. This will work in your browser but won't work for mockup generation. Deploy your app or use publicly accessible URLs for production mockups.`
|
|
1192
1659
|
);
|
|
1193
1660
|
}
|
|
1194
1661
|
return absoluteUrl;
|
|
1195
1662
|
} catch (e) {
|
|
1196
|
-
console.warn(`[
|
|
1663
|
+
console.warn(`[snowcone-sdk] Failed to normalize URL "${url}":`, e);
|
|
1197
1664
|
return url;
|
|
1198
1665
|
}
|
|
1199
1666
|
}
|
|
1200
1667
|
if (url.startsWith("/") || url.startsWith(".")) {
|
|
1201
1668
|
console.warn(
|
|
1202
|
-
`[
|
|
1669
|
+
`[snowcone-sdk] Relative URL "${url}" detected during SSR. URL will be resolved on the client. Consider using absolute URLs for SSR compatibility.`
|
|
1203
1670
|
);
|
|
1204
1671
|
}
|
|
1205
1672
|
return url;
|
|
@@ -1302,95 +1769,39 @@ function filterImagePlacements(placements) {
|
|
|
1302
1769
|
}
|
|
1303
1770
|
|
|
1304
1771
|
// src/mockup/urlGenerator.ts
|
|
1305
|
-
var
|
|
1306
|
-
function
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const envImageUrl = env?.MERCHIFY_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL;
|
|
1313
|
-
const envSignerUrl = env?.MERCHIFY_SIGNER_URL || env?.NEXT_PUBLIC_MERCH_SIGNER_URL;
|
|
1314
|
-
const envAccountId = env?.MERCHIFY_ACCOUNT_ID || env?.NEXT_PUBLIC_MERCH_ACCOUNT_ID;
|
|
1315
|
-
return {
|
|
1316
|
-
endpoint: winConfig.endpoint || envEndpoint || void 0,
|
|
1317
|
-
mockupUrl: winConfig.mockupUrl || envImageUrl || DEFAULT_MOCKUP_URL,
|
|
1318
|
-
signerUrl: winConfig.signerUrl || envSignerUrl || void 0,
|
|
1319
|
-
accountId: winConfig.accountId || envAccountId || DEFAULT_ACCOUNT_ID,
|
|
1320
|
-
mode: winConfig.mode || "mock"
|
|
1321
|
-
};
|
|
1772
|
+
var maxWidthCache = /* @__PURE__ */ new Map();
|
|
1773
|
+
function buildMockupUrl2(options, cfg) {
|
|
1774
|
+
const { ar, ...rest } = options;
|
|
1775
|
+
return buildMockupUrl(
|
|
1776
|
+
{ ...rest, aspect: ar },
|
|
1777
|
+
{ mockupBaseUrl: cfg.mockupBaseUrl, shop: cfg.shop }
|
|
1778
|
+
);
|
|
1322
1779
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
cachedMockupConfig = { ...base, ...overrides };
|
|
1328
|
-
} else if (!cachedMockupConfig) {
|
|
1329
|
-
cachedMockupConfig = resolveDefaultConfig();
|
|
1330
|
-
}
|
|
1331
|
-
const hostname = typeof window !== "undefined" ? window.location.hostname : "";
|
|
1332
|
-
const isLocalDev = hostname === "localhost" || hostname === "127.0.0.1" || hostname.includes("192.168.") || hostname.endsWith(".local") || hostname.endsWith(".lvh.me") || hostname === "lvh.me";
|
|
1333
|
-
if (!localhostWarningShown && typeof window !== "undefined" && !isLocalDev && cachedMockupConfig?.mockupUrl?.includes("localhost")) {
|
|
1334
|
-
console.error(
|
|
1335
|
-
`[@snowcone-app/sdk] WARNING: Using localhost mockup URL in production environment.
|
|
1336
|
-
mockupUrl: ${cachedMockupConfig.mockupUrl}
|
|
1337
|
-
This was likely caused by building with .env.local present.
|
|
1338
|
-
Redeploy with "pnpm run deploy" to fix.`
|
|
1339
|
-
);
|
|
1340
|
-
localhostWarningShown = true;
|
|
1341
|
-
}
|
|
1342
|
-
return cachedMockupConfig;
|
|
1780
|
+
function resolveMockupBaseUrl() {
|
|
1781
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1782
|
+
const winConfig = typeof window !== "undefined" && window.snowcone || {};
|
|
1783
|
+
return winConfig.mockupUrl || env?.SNOWCONE_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL || "https://cdn.snowcone.app";
|
|
1343
1784
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
const keys = Object.keys(element).sort();
|
|
1349
|
-
for (const key of keys) {
|
|
1350
|
-
if (key === "width" || key === "height" || key === "type") {
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
if (key === "alignment" && element[key] === "center") {
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
const value = element[key];
|
|
1357
|
-
if (value !== void 0 && value !== null) {
|
|
1358
|
-
normalized[key] = value;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return normalized;
|
|
1362
|
-
});
|
|
1785
|
+
function resolveShop() {
|
|
1786
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1787
|
+
const winConfig = typeof window !== "undefined" && window.snowcone || {};
|
|
1788
|
+
return winConfig.shop || env?.SNOWCONE_SHOP_ID || env?.NEXT_PUBLIC_SNOWCONE_SHOP_ID || "SHOP_NOT_CONFIGURED";
|
|
1363
1789
|
}
|
|
1364
1790
|
function mockupUrl(options) {
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
queryString += `&width=${options.width}`;
|
|
1376
|
-
queryString += `&accountId=${encodeURIComponent(accountId)}`;
|
|
1377
|
-
if (options.effects?.grain) {
|
|
1378
|
-
queryString += `&grain=${options.effects.grain}`;
|
|
1379
|
-
}
|
|
1380
|
-
if (options.ar && options.ar !== "16:9") {
|
|
1381
|
-
queryString += `&ar=${encodeURIComponent(options.ar)}`;
|
|
1382
|
-
}
|
|
1383
|
-
if (options.maskOverrides && options.maskOverrides.length > 0) {
|
|
1384
|
-
const maskOverridesBase64 = btoa(JSON.stringify(options.maskOverrides));
|
|
1385
|
-
queryString += `&maskOverrides=${encodeURIComponent(maskOverridesBase64)}`;
|
|
1386
|
-
}
|
|
1387
|
-
if (mockupConfig.mode === "mock") {
|
|
1388
|
-
const mockSignature = "bypass-sig-for-k6-load-test";
|
|
1389
|
-
queryString += `&sig=${encodeURIComponent(mockSignature)}`;
|
|
1791
|
+
const mockupBaseUrl = resolveMockupBaseUrl();
|
|
1792
|
+
const shop = resolveShop();
|
|
1793
|
+
const encodedDesign = encodeURIComponent(
|
|
1794
|
+
btoa(JSON.stringify(normalizeDesignElements(options.design)))
|
|
1795
|
+
);
|
|
1796
|
+
let width = normalizeWidth(options.width);
|
|
1797
|
+
const cacheKey = `${options.productId}:${options.mockupId}:${options.variantId}:${encodedDesign}`;
|
|
1798
|
+
const cachedWidth = maxWidthCache.get(cacheKey);
|
|
1799
|
+
if (cachedWidth !== void 0 && cachedWidth >= width) {
|
|
1800
|
+
width = cachedWidth;
|
|
1390
1801
|
} else {
|
|
1391
|
-
|
|
1802
|
+
maxWidthCache.set(cacheKey, width);
|
|
1392
1803
|
}
|
|
1393
|
-
return
|
|
1804
|
+
return buildMockupUrl2({ ...options, width }, { mockupBaseUrl, shop });
|
|
1394
1805
|
}
|
|
1395
1806
|
function getVariant(selection, product) {
|
|
1396
1807
|
const combination = findBestCombination(
|
|
@@ -1556,6 +1967,12 @@ function validateImageUrl(url) {
|
|
|
1556
1967
|
message: "Image URL is required",
|
|
1557
1968
|
value: url
|
|
1558
1969
|
});
|
|
1970
|
+
} else if (url.startsWith("blob:")) {
|
|
1971
|
+
errors.push({
|
|
1972
|
+
field: "imageUrl",
|
|
1973
|
+
message: "Blob URLs (blob:...) cannot be used for mockup generation \u2014 they are local to the browser and not accessible by the rendering server. Upload the image to a public URL first, or export the canvas to a data URL.",
|
|
1974
|
+
value: url
|
|
1975
|
+
});
|
|
1559
1976
|
} else if (
|
|
1560
1977
|
// Allow absolute URLs
|
|
1561
1978
|
!url.startsWith("http://") && !url.startsWith("https://") && // Allow data URLs
|
|
@@ -2554,11 +2971,11 @@ var UniversalContextProvider = class extends EventEmitter {
|
|
|
2554
2971
|
config;
|
|
2555
2972
|
consumers = /* @__PURE__ */ new Set();
|
|
2556
2973
|
initialized = false;
|
|
2557
|
-
constructor(
|
|
2974
|
+
constructor(config = {}) {
|
|
2558
2975
|
super();
|
|
2559
|
-
this.config =
|
|
2976
|
+
this.config = config;
|
|
2560
2977
|
this.contextManager = new ProductContextManager();
|
|
2561
|
-
this.loader = new ProductLoader(this.contextManager,
|
|
2978
|
+
this.loader = new ProductLoader(this.contextManager, config.fetcher);
|
|
2562
2979
|
this.setupSubscriptions();
|
|
2563
2980
|
}
|
|
2564
2981
|
/**
|
|
@@ -2676,7 +3093,7 @@ var ContextInjector = class {
|
|
|
2676
3093
|
static forVue(provider) {
|
|
2677
3094
|
return {
|
|
2678
3095
|
install(app) {
|
|
2679
|
-
app.provide("
|
|
3096
|
+
app.provide("snowconeContext", provider);
|
|
2680
3097
|
},
|
|
2681
3098
|
inject() {
|
|
2682
3099
|
return provider;
|
|
@@ -2704,7 +3121,7 @@ var ContextInjector = class {
|
|
|
2704
3121
|
// Attach to element
|
|
2705
3122
|
attach(element) {
|
|
2706
3123
|
element.__contextProvider = provider;
|
|
2707
|
-
element.addEventListener("
|
|
3124
|
+
element.addEventListener("snowcone:request-context", (event) => {
|
|
2708
3125
|
event.detail.context = provider.getContext();
|
|
2709
3126
|
});
|
|
2710
3127
|
},
|
|
@@ -2722,8 +3139,8 @@ var ContextInjector = class {
|
|
|
2722
3139
|
};
|
|
2723
3140
|
}
|
|
2724
3141
|
};
|
|
2725
|
-
function createUniversalProvider(
|
|
2726
|
-
return new UniversalContextProvider(
|
|
3142
|
+
function createUniversalProvider(config) {
|
|
3143
|
+
return new UniversalContextProvider(config);
|
|
2727
3144
|
}
|
|
2728
3145
|
function withContext(Base) {
|
|
2729
3146
|
return class extends Base {
|
|
@@ -3728,8 +4145,8 @@ var StandardComponents = {
|
|
|
3728
4145
|
Product: {
|
|
3729
4146
|
metadata: {
|
|
3730
4147
|
name: "Product",
|
|
3731
|
-
tagName: "
|
|
3732
|
-
displayName: "
|
|
4148
|
+
tagName: "snowcone-product",
|
|
4149
|
+
displayName: "SnowconeProduct",
|
|
3733
4150
|
description: "Product context provider component",
|
|
3734
4151
|
props: {
|
|
3735
4152
|
productId: { type: "string" },
|
|
@@ -3755,8 +4172,8 @@ var StandardComponents = {
|
|
|
3755
4172
|
ProductOptions: {
|
|
3756
4173
|
metadata: {
|
|
3757
4174
|
name: "ProductOptions",
|
|
3758
|
-
tagName: "
|
|
3759
|
-
displayName: "
|
|
4175
|
+
tagName: "snowcone-product-options",
|
|
4176
|
+
displayName: "SnowconeProductOptions",
|
|
3760
4177
|
description: "Product options selection component",
|
|
3761
4178
|
props: {
|
|
3762
4179
|
attributes: { type: "object" },
|
|
@@ -3776,8 +4193,8 @@ var StandardComponents = {
|
|
|
3776
4193
|
ProductPrice: {
|
|
3777
4194
|
metadata: {
|
|
3778
4195
|
name: "ProductPrice",
|
|
3779
|
-
tagName: "
|
|
3780
|
-
displayName: "
|
|
4196
|
+
tagName: "snowcone-product-price",
|
|
4197
|
+
displayName: "SnowconeProductPrice",
|
|
3781
4198
|
description: "Product price display component",
|
|
3782
4199
|
props: {
|
|
3783
4200
|
price: { type: "number" },
|
|
@@ -3793,8 +4210,8 @@ var StandardComponents = {
|
|
|
3793
4210
|
ProductImage: {
|
|
3794
4211
|
metadata: {
|
|
3795
4212
|
name: "ProductImage",
|
|
3796
|
-
tagName: "
|
|
3797
|
-
displayName: "
|
|
4213
|
+
tagName: "snowcone-product-image",
|
|
4214
|
+
displayName: "SnowconeProductImage",
|
|
3798
4215
|
description: "Product mockup image component",
|
|
3799
4216
|
props: {
|
|
3800
4217
|
productId: { type: "string" },
|
|
@@ -3813,8 +4230,8 @@ var StandardComponents = {
|
|
|
3813
4230
|
AddToCart: {
|
|
3814
4231
|
metadata: {
|
|
3815
4232
|
name: "AddToCart",
|
|
3816
|
-
tagName: "
|
|
3817
|
-
displayName: "
|
|
4233
|
+
tagName: "snowcone-add-to-cart",
|
|
4234
|
+
displayName: "SnowconeAddToCart",
|
|
3818
4235
|
description: "Add to cart button component",
|
|
3819
4236
|
props: {
|
|
3820
4237
|
text: { type: "string", default: "Add to Cart" },
|
|
@@ -4395,7 +4812,9 @@ var RealtimeMockupService = class {
|
|
|
4395
4812
|
logs = [];
|
|
4396
4813
|
lastError = null;
|
|
4397
4814
|
callbacks = {};
|
|
4815
|
+
lastBlobSentAt = 0;
|
|
4398
4816
|
canvasBlobs = /* @__PURE__ */ new Map();
|
|
4817
|
+
canvasStates = /* @__PURE__ */ new Map();
|
|
4399
4818
|
colors = /* @__PURE__ */ new Map();
|
|
4400
4819
|
lastSendTime = {};
|
|
4401
4820
|
throttleTimeouts = {};
|
|
@@ -4405,11 +4824,24 @@ var RealtimeMockupService = class {
|
|
|
4405
4824
|
lastSentVersion = 0;
|
|
4406
4825
|
// Track latest sent version per placement to detect stale responses
|
|
4407
4826
|
latestSentVersionByPlacement = {};
|
|
4827
|
+
// Track latest accepted (displayed) version per placement — only drop results
|
|
4828
|
+
// older than what we've already shown, not older than what we've sent.
|
|
4829
|
+
// This prevents the "version racing" problem during drag where sent versions
|
|
4830
|
+
// advance faster than the server can render.
|
|
4831
|
+
latestAcceptedVersionByPlacement = {};
|
|
4408
4832
|
// Feature flag: server now supports version in blob message
|
|
4409
4833
|
sendVersionInBlob = true;
|
|
4834
|
+
// Session-grant auth: when set, connect() fetches a short-lived token, opens
|
|
4835
|
+
// the WS with `?token=`, and renews ~15s before expiry via a `renew` message.
|
|
4836
|
+
tokenProvider;
|
|
4837
|
+
renewTimer = null;
|
|
4410
4838
|
setCallbacks(callbacks) {
|
|
4411
4839
|
this.callbacks = callbacks;
|
|
4412
4840
|
}
|
|
4841
|
+
/** Provide a grant fetcher to authorize the session (per-shop, renewable). */
|
|
4842
|
+
setTokenProvider(fn) {
|
|
4843
|
+
this.tokenProvider = fn;
|
|
4844
|
+
}
|
|
4413
4845
|
getState() {
|
|
4414
4846
|
return {
|
|
4415
4847
|
isConnected: this.ws?.readyState === WebSocket.OPEN,
|
|
@@ -4432,9 +4864,24 @@ var RealtimeMockupService = class {
|
|
|
4432
4864
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
4433
4865
|
return;
|
|
4434
4866
|
}
|
|
4867
|
+
if (this.tokenProvider) {
|
|
4868
|
+
this.tokenProvider().then((grant) => {
|
|
4869
|
+
this.openSocket(grant.token);
|
|
4870
|
+
this.scheduleRenew(grant.expiresAt);
|
|
4871
|
+
}).catch((err) => {
|
|
4872
|
+
this.addLog(`Failed to obtain realtime grant: ${err}`);
|
|
4873
|
+
this.status = "Disconnected";
|
|
4874
|
+
});
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4877
|
+
this.openSocket();
|
|
4878
|
+
}
|
|
4879
|
+
openSocket(token) {
|
|
4880
|
+
const url = token ? `${this.wsUrl}${this.wsUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(token)}` : this.wsUrl;
|
|
4435
4881
|
this.addLog(`Connecting to ${this.wsUrl}...`);
|
|
4436
|
-
this.ws = new WebSocket(
|
|
4882
|
+
this.ws = new WebSocket(url);
|
|
4437
4883
|
this.ws.onopen = () => {
|
|
4884
|
+
console.log(`[WS] connection OPENED to ${this.wsUrl}`);
|
|
4438
4885
|
this.addLog("WebSocket connection opened");
|
|
4439
4886
|
this.status = "Connected";
|
|
4440
4887
|
};
|
|
@@ -4448,11 +4895,13 @@ var RealtimeMockupService = class {
|
|
|
4448
4895
|
}
|
|
4449
4896
|
};
|
|
4450
4897
|
this.ws.onclose = (event) => {
|
|
4898
|
+
console.log(`[WS] connection CLOSED (code: ${event.code}, reason: "${event.reason}", wasClean: ${event.wasClean})`);
|
|
4451
4899
|
this.addLog(`WebSocket connection closed (code: ${event.code})`);
|
|
4452
4900
|
this.status = "Disconnected";
|
|
4453
4901
|
this.sessionId = null;
|
|
4454
4902
|
this.isConfigured = false;
|
|
4455
4903
|
this.configSent = false;
|
|
4904
|
+
this.clearRenew();
|
|
4456
4905
|
this.callbacks.onDisconnected?.();
|
|
4457
4906
|
};
|
|
4458
4907
|
this.ws.onerror = (error) => {
|
|
@@ -4460,6 +4909,29 @@ var RealtimeMockupService = class {
|
|
|
4460
4909
|
this.status = "Disconnected";
|
|
4461
4910
|
};
|
|
4462
4911
|
}
|
|
4912
|
+
scheduleRenew(expiresAt) {
|
|
4913
|
+
this.clearRenew();
|
|
4914
|
+
if (!this.tokenProvider) return;
|
|
4915
|
+
const ms = Math.max(1e3, expiresAt * 1e3 - Date.now() - 15e3);
|
|
4916
|
+
this.renewTimer = setTimeout(() => void this.renew(), ms);
|
|
4917
|
+
}
|
|
4918
|
+
clearRenew() {
|
|
4919
|
+
if (this.renewTimer) {
|
|
4920
|
+
clearTimeout(this.renewTimer);
|
|
4921
|
+
this.renewTimer = null;
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
async renew() {
|
|
4925
|
+
if (!this.tokenProvider || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
4926
|
+
try {
|
|
4927
|
+
const grant = await this.tokenProvider();
|
|
4928
|
+
this.ws.send(JSON.stringify({ type: "renew", token: grant.token }));
|
|
4929
|
+
this.addLog("\u{1F504} Renewed realtime session token", "sent");
|
|
4930
|
+
this.scheduleRenew(grant.expiresAt);
|
|
4931
|
+
} catch (err) {
|
|
4932
|
+
this.addLog(`Failed to renew realtime grant: ${err}`);
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4463
4935
|
handleMessage(data) {
|
|
4464
4936
|
switch (data.type) {
|
|
4465
4937
|
case "connected":
|
|
@@ -4511,15 +4983,18 @@ var RealtimeMockupService = class {
|
|
|
4511
4983
|
this.addLog("\u{1F3A8} Mockup rendering has started...");
|
|
4512
4984
|
break;
|
|
4513
4985
|
case "mockup_rendered":
|
|
4986
|
+
console.log(`[WS] mockup_rendered received: mockupId=${data.mockupId} hasImageUrl=${!!data.imageUrl} v=${data.requestVersion} placement="${data.placement}" renderMs=${data.renderMs}`);
|
|
4514
4987
|
if (data.imageUrl && data.mockupId) {
|
|
4515
4988
|
const responseVersion = data.requestVersion;
|
|
4516
4989
|
const responsePlacement = data.placement;
|
|
4517
4990
|
if (responseVersion !== void 0 && responsePlacement) {
|
|
4518
|
-
const
|
|
4519
|
-
if (
|
|
4520
|
-
|
|
4991
|
+
const lastAccepted = this.latestAcceptedVersionByPlacement[responsePlacement];
|
|
4992
|
+
if (lastAccepted !== void 0 && responseVersion < lastAccepted) {
|
|
4993
|
+
console.log(`[WS] STALE mockup dropped: v${responseVersion} for "${responsePlacement}" (already displayed: v${lastAccepted}, latest sent: v${this.latestSentVersionByPlacement[responsePlacement]})`);
|
|
4994
|
+
this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (displayed: v${lastAccepted})`);
|
|
4521
4995
|
break;
|
|
4522
4996
|
}
|
|
4997
|
+
this.latestAcceptedVersionByPlacement[responsePlacement] = responseVersion;
|
|
4523
4998
|
}
|
|
4524
4999
|
const mockupResult = {
|
|
4525
5000
|
mockupId: data.mockupId,
|
|
@@ -4527,7 +5002,10 @@ var RealtimeMockupService = class {
|
|
|
4527
5002
|
renderUrl: data.renderUrl || data.imageUrl,
|
|
4528
5003
|
imageSize: data.imageSize || 0,
|
|
4529
5004
|
requestVersion: responseVersion,
|
|
4530
|
-
placement: responsePlacement
|
|
5005
|
+
placement: responsePlacement,
|
|
5006
|
+
renderMs: data.renderMs,
|
|
5007
|
+
blobToRenderMs: data.blobToRenderMs,
|
|
5008
|
+
canvasRenderTiming: data.canvasRenderTiming
|
|
4531
5009
|
};
|
|
4532
5010
|
const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
|
|
4533
5011
|
if (existingIndex >= 0) {
|
|
@@ -4546,14 +5024,16 @@ var RealtimeMockupService = class {
|
|
|
4546
5024
|
}
|
|
4547
5025
|
break;
|
|
4548
5026
|
case "all_mockups_rendered":
|
|
5027
|
+
console.log(`[WS] all_mockups_rendered received: ${data.mockups?.length ?? 0} mockups`);
|
|
4549
5028
|
if (data.mockups) {
|
|
4550
5029
|
const freshMockups = data.mockups.filter((mockup) => {
|
|
4551
5030
|
if (mockup.requestVersion !== void 0 && mockup.placement) {
|
|
4552
|
-
const
|
|
4553
|
-
if (
|
|
4554
|
-
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (
|
|
5031
|
+
const lastAccepted = this.latestAcceptedVersionByPlacement[mockup.placement];
|
|
5032
|
+
if (lastAccepted !== void 0 && mockup.requestVersion < lastAccepted) {
|
|
5033
|
+
this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (displayed: v${lastAccepted})`);
|
|
4555
5034
|
return false;
|
|
4556
5035
|
}
|
|
5036
|
+
this.latestAcceptedVersionByPlacement[mockup.placement] = mockup.requestVersion;
|
|
4557
5037
|
}
|
|
4558
5038
|
return true;
|
|
4559
5039
|
});
|
|
@@ -4576,31 +5056,32 @@ var RealtimeMockupService = class {
|
|
|
4576
5056
|
}
|
|
4577
5057
|
}
|
|
4578
5058
|
disconnect() {
|
|
5059
|
+
this.clearRenew();
|
|
4579
5060
|
if (this.ws) {
|
|
4580
5061
|
this.ws.close();
|
|
4581
5062
|
this.ws = null;
|
|
4582
5063
|
}
|
|
4583
5064
|
}
|
|
4584
|
-
sendConfig(
|
|
5065
|
+
sendConfig(config) {
|
|
4585
5066
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
4586
5067
|
this.addLog("WebSocket not connected, caching config");
|
|
4587
|
-
this.config =
|
|
5068
|
+
this.config = config;
|
|
4588
5069
|
this.configSent = false;
|
|
4589
5070
|
return false;
|
|
4590
5071
|
}
|
|
4591
5072
|
if (!this.sessionId) {
|
|
4592
5073
|
this.addLog("WebSocket connected but no session yet, caching config");
|
|
4593
|
-
this.config =
|
|
5074
|
+
this.config = config;
|
|
4594
5075
|
this.configSent = false;
|
|
4595
5076
|
return false;
|
|
4596
5077
|
}
|
|
4597
|
-
const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(
|
|
5078
|
+
const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config);
|
|
4598
5079
|
if (this.configSent && !hasConfigChanged) {
|
|
4599
5080
|
this.addLog("Config already sent and unchanged, skipping duplicate");
|
|
4600
5081
|
return false;
|
|
4601
5082
|
}
|
|
4602
5083
|
if (this.configSent && hasConfigChanged) {
|
|
4603
|
-
const isOnlyMockupIdsChange = this.config && this.config.productId ===
|
|
5084
|
+
const isOnlyMockupIdsChange = this.config && this.config.productId === config.productId && this.config.variantId === config.variantId;
|
|
4604
5085
|
if (isOnlyMockupIdsChange) {
|
|
4605
5086
|
this.addLog("\u{1F504} MockupIds changed, keeping cached blobs (server will reuse)");
|
|
4606
5087
|
this.isConfigured = false;
|
|
@@ -4610,6 +5091,7 @@ var RealtimeMockupService = class {
|
|
|
4610
5091
|
this.isConfigured = false;
|
|
4611
5092
|
this.mockupResults = [];
|
|
4612
5093
|
this.canvasBlobs.clear();
|
|
5094
|
+
this.canvasStates.clear();
|
|
4613
5095
|
this.colors.clear();
|
|
4614
5096
|
this.lastSendTime = {};
|
|
4615
5097
|
Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
|
|
@@ -4617,14 +5099,15 @@ var RealtimeMockupService = class {
|
|
|
4617
5099
|
this.requestVersion = 0;
|
|
4618
5100
|
this.lastSentVersion = 0;
|
|
4619
5101
|
this.latestSentVersionByPlacement = {};
|
|
5102
|
+
this.latestAcceptedVersionByPlacement = {};
|
|
4620
5103
|
this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
|
|
4621
5104
|
}
|
|
4622
5105
|
}
|
|
4623
|
-
this.config =
|
|
5106
|
+
this.config = config;
|
|
4624
5107
|
this.configSent = true;
|
|
4625
5108
|
const message = {
|
|
4626
5109
|
type: "config",
|
|
4627
|
-
config
|
|
5110
|
+
config
|
|
4628
5111
|
};
|
|
4629
5112
|
const messageStr = JSON.stringify(message);
|
|
4630
5113
|
this.addLog(`\u{1F4E4} Sending config: ${JSON.stringify(message, null, 2)}`, "sent");
|
|
@@ -4652,6 +5135,30 @@ var RealtimeMockupService = class {
|
|
|
4652
5135
|
this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
|
|
4653
5136
|
return this.sendConfig(updatedConfig);
|
|
4654
5137
|
}
|
|
5138
|
+
/**
|
|
5139
|
+
* Update render width without changing other config.
|
|
5140
|
+
* Used for low-res preview during rapid edits (e.g., 600 while dragging, 1200 on release).
|
|
5141
|
+
* Preserves blobs since product/variant don't change.
|
|
5142
|
+
*/
|
|
5143
|
+
updateWidth(width) {
|
|
5144
|
+
if (!this.config) {
|
|
5145
|
+
this.addLog("Cannot update width: no config set");
|
|
5146
|
+
return false;
|
|
5147
|
+
}
|
|
5148
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5149
|
+
this.addLog("Cannot update width: WebSocket not connected");
|
|
5150
|
+
return false;
|
|
5151
|
+
}
|
|
5152
|
+
if (this.config.width === width) {
|
|
5153
|
+
return false;
|
|
5154
|
+
}
|
|
5155
|
+
const updatedConfig = {
|
|
5156
|
+
...this.config,
|
|
5157
|
+
width
|
|
5158
|
+
};
|
|
5159
|
+
this.addLog(`\u{1F4D0} Updating render width: ${this.config.width} \u2192 ${width}`);
|
|
5160
|
+
return this.sendConfig(updatedConfig);
|
|
5161
|
+
}
|
|
4655
5162
|
/**
|
|
4656
5163
|
* Update placementSettings without changing other config.
|
|
4657
5164
|
* Used to override scaleMode when canvas editor is active.
|
|
@@ -4674,6 +5181,8 @@ var RealtimeMockupService = class {
|
|
|
4674
5181
|
sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
|
|
4675
5182
|
this.canvasBlobs.set(placement, blob);
|
|
4676
5183
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
5184
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
5185
|
+
console.log(`[WS] sendCanvasBlob BLOCKED for "${placement}" (${blob.size}B): ${reason} (cached for later)`);
|
|
4677
5186
|
return false;
|
|
4678
5187
|
}
|
|
4679
5188
|
if (baseThrottleMs <= 0) {
|
|
@@ -4681,16 +5190,25 @@ var RealtimeMockupService = class {
|
|
|
4681
5190
|
this.lastSendTime[placement] = Date.now();
|
|
4682
5191
|
return true;
|
|
4683
5192
|
}
|
|
4684
|
-
const
|
|
4685
|
-
const
|
|
4686
|
-
const
|
|
4687
|
-
const
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
const
|
|
5193
|
+
const debounceMs = baseThrottleMs * mockupCount;
|
|
5194
|
+
const lastSendTime = this.lastSendTime[placement];
|
|
5195
|
+
const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
|
|
5196
|
+
const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
|
|
5197
|
+
if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
|
|
5198
|
+
if (this.throttleTimeouts[placement]) {
|
|
5199
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
5200
|
+
delete this.throttleTimeouts[placement];
|
|
5201
|
+
}
|
|
5202
|
+
const latestBlob = this.canvasBlobs.get(placement);
|
|
5203
|
+
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
5204
|
+
this.sendBlobImmediately(placement, latestBlob, notifyCallback);
|
|
5205
|
+
this.lastSendTime[placement] = Date.now();
|
|
5206
|
+
}
|
|
5207
|
+
} else {
|
|
5208
|
+
if (this.throttleTimeouts[placement]) {
|
|
5209
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
5210
|
+
}
|
|
5211
|
+
const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
|
|
4694
5212
|
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
4695
5213
|
const latestBlob = this.canvasBlobs.get(placement);
|
|
4696
5214
|
if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
@@ -4702,6 +5220,72 @@ var RealtimeMockupService = class {
|
|
|
4702
5220
|
}
|
|
4703
5221
|
return true;
|
|
4704
5222
|
}
|
|
5223
|
+
/**
|
|
5224
|
+
* Send canvas state JSON for server-side rendering.
|
|
5225
|
+
* Alternative to sendCanvasBlob — the server renders the PNG instead of the client.
|
|
5226
|
+
*/
|
|
5227
|
+
sendCanvasState(placement, state, mockupCount = 1, baseThrottleMs = 1e3) {
|
|
5228
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
|
|
5229
|
+
const reason = !this.ws ? "no WebSocket" : this.ws.readyState !== WebSocket.OPEN ? `ws.readyState=${this.ws.readyState}` : "not configured";
|
|
5230
|
+
console.log(`[WS] sendCanvasState BLOCKED for "${placement}": ${reason}`);
|
|
5231
|
+
return false;
|
|
5232
|
+
}
|
|
5233
|
+
this.canvasStates.set(placement, state);
|
|
5234
|
+
if (baseThrottleMs <= 0) {
|
|
5235
|
+
this.sendCanvasStateImmediately(placement, state);
|
|
5236
|
+
this.lastSendTime[placement] = Date.now();
|
|
5237
|
+
return true;
|
|
5238
|
+
}
|
|
5239
|
+
const debounceMs = baseThrottleMs * mockupCount;
|
|
5240
|
+
const lastSendTime = this.lastSendTime[placement];
|
|
5241
|
+
const timeSinceLastSend = lastSendTime ? Date.now() - lastSendTime : 0;
|
|
5242
|
+
const isActiveInteraction = lastSendTime && timeSinceLastSend < debounceMs * 3;
|
|
5243
|
+
if (isActiveInteraction && timeSinceLastSend >= debounceMs) {
|
|
5244
|
+
if (this.throttleTimeouts[placement]) {
|
|
5245
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
5246
|
+
delete this.throttleTimeouts[placement];
|
|
5247
|
+
}
|
|
5248
|
+
const latestState = this.canvasStates.get(placement);
|
|
5249
|
+
if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
5250
|
+
console.log(`[WS] sendCanvasState "${placement}": max-wait flush (${timeSinceLastSend}ms since last)`);
|
|
5251
|
+
this.sendCanvasStateImmediately(placement, latestState);
|
|
5252
|
+
this.lastSendTime[placement] = Date.now();
|
|
5253
|
+
}
|
|
5254
|
+
} else {
|
|
5255
|
+
if (this.throttleTimeouts[placement]) {
|
|
5256
|
+
clearTimeout(this.throttleTimeouts[placement]);
|
|
5257
|
+
}
|
|
5258
|
+
const delayTime = isActiveInteraction ? debounceMs - timeSinceLastSend : debounceMs;
|
|
5259
|
+
this.throttleTimeouts[placement] = setTimeout(() => {
|
|
5260
|
+
const latestState = this.canvasStates.get(placement);
|
|
5261
|
+
if (latestState && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
|
|
5262
|
+
console.log(`[WS] sendCanvasState "${placement}": debounce firing (${debounceMs}ms)`);
|
|
5263
|
+
this.sendCanvasStateImmediately(placement, latestState);
|
|
5264
|
+
this.lastSendTime[placement] = Date.now();
|
|
5265
|
+
}
|
|
5266
|
+
delete this.throttleTimeouts[placement];
|
|
5267
|
+
}, delayTime);
|
|
5268
|
+
}
|
|
5269
|
+
return true;
|
|
5270
|
+
}
|
|
5271
|
+
sendCanvasStateImmediately(placement, state) {
|
|
5272
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5273
|
+
console.log(`[WS] sendCanvasStateImmediately ABORTED: ws not open`);
|
|
5274
|
+
return;
|
|
5275
|
+
}
|
|
5276
|
+
this.lastSentVersion = ++this.requestVersion;
|
|
5277
|
+
this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
|
|
5278
|
+
const message = JSON.stringify({
|
|
5279
|
+
type: "canvas_state",
|
|
5280
|
+
placement,
|
|
5281
|
+
version: this.lastSentVersion,
|
|
5282
|
+
state
|
|
5283
|
+
});
|
|
5284
|
+
this.ws.send(message);
|
|
5285
|
+
this.lastBlobSentAt = Date.now();
|
|
5286
|
+
this.addLog(`Sent canvas state for "${placement}" (${(message.length / 1024).toFixed(1)}KB, v${this.lastSentVersion})`, "sent");
|
|
5287
|
+
this.callbacks.onBlobSent?.(placement);
|
|
5288
|
+
}
|
|
4705
5289
|
sendBlobImmediately(placement, blob, notifyCallback = true) {
|
|
4706
5290
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
4707
5291
|
this.lastSentVersion = ++this.requestVersion;
|
|
@@ -4727,6 +5311,7 @@ ${versionToSend}
|
|
|
4727
5311
|
combined.set(imageBytes, headerBytes.length);
|
|
4728
5312
|
}
|
|
4729
5313
|
this.ws.send(combined.buffer);
|
|
5314
|
+
this.lastBlobSentAt = Date.now();
|
|
4730
5315
|
this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
|
|
4731
5316
|
if (notifyCallback) {
|
|
4732
5317
|
this.callbacks.onBlobSent?.(placement);
|
|
@@ -4863,8 +5448,8 @@ ${versionToSend}
|
|
|
4863
5448
|
};
|
|
4864
5449
|
|
|
4865
5450
|
// src/index.ts
|
|
4866
|
-
function getFetcher(
|
|
4867
|
-
return
|
|
5451
|
+
function getFetcher(config) {
|
|
5452
|
+
return config?.fetcher || globalThis.fetch.bind(globalThis);
|
|
4868
5453
|
}
|
|
4869
5454
|
function validateProductLoose(product) {
|
|
4870
5455
|
try {
|
|
@@ -4873,11 +5458,11 @@ function validateProductLoose(product) {
|
|
|
4873
5458
|
return product;
|
|
4874
5459
|
}
|
|
4875
5460
|
}
|
|
4876
|
-
async function listProducts(
|
|
4877
|
-
const f = getFetcher(
|
|
4878
|
-
const meilisearchHost =
|
|
4879
|
-
const meilisearchApiKey =
|
|
4880
|
-
const meilisearchIndex =
|
|
5461
|
+
async function listProducts(config) {
|
|
5462
|
+
const f = getFetcher(config);
|
|
5463
|
+
const meilisearchHost = config?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://search.snowcone.app";
|
|
5464
|
+
const meilisearchApiKey = config?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
|
|
5465
|
+
const meilisearchIndex = config?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "snowcone";
|
|
4881
5466
|
const headers = {
|
|
4882
5467
|
"Content-Type": "application/json"
|
|
4883
5468
|
};
|
|
@@ -4890,7 +5475,7 @@ async function listProducts(config2) {
|
|
|
4890
5475
|
headers,
|
|
4891
5476
|
body: JSON.stringify({
|
|
4892
5477
|
q: "",
|
|
4893
|
-
limit:
|
|
5478
|
+
limit: config?.limit || 1e3,
|
|
4894
5479
|
filter: "mockups IS NOT EMPTY"
|
|
4895
5480
|
})
|
|
4896
5481
|
});
|
|
@@ -4899,11 +5484,11 @@ async function listProducts(config2) {
|
|
|
4899
5484
|
const items = (data.hits || []).map((p) => validateProductLoose(p));
|
|
4900
5485
|
return { items, total: data.estimatedTotalHits || items.length };
|
|
4901
5486
|
}
|
|
4902
|
-
async function getProduct(idOrSlug,
|
|
4903
|
-
const f = getFetcher(
|
|
4904
|
-
const meilisearchHost =
|
|
4905
|
-
const meilisearchApiKey =
|
|
4906
|
-
const meilisearchIndex =
|
|
5487
|
+
async function getProduct(idOrSlug, config) {
|
|
5488
|
+
const f = getFetcher(config);
|
|
5489
|
+
const meilisearchHost = config?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://search.snowcone.app";
|
|
5490
|
+
const meilisearchApiKey = config?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
|
|
5491
|
+
const meilisearchIndex = config?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "snowcone";
|
|
4907
5492
|
const headers = {
|
|
4908
5493
|
"Content-Type": "application/json"
|
|
4909
5494
|
};
|
|
@@ -4918,13 +5503,13 @@ async function getProduct(idOrSlug, config2) {
|
|
|
4918
5503
|
const raw = await res.json();
|
|
4919
5504
|
return validateProductLoose(raw);
|
|
4920
5505
|
}
|
|
4921
|
-
function createClient(
|
|
4922
|
-
const fetcher = getFetcher(
|
|
4923
|
-
const mockupService = new MockupServiceImpl(
|
|
5506
|
+
function createClient(config) {
|
|
5507
|
+
const fetcher = getFetcher(config);
|
|
5508
|
+
const mockupService = new MockupServiceImpl(config.mockup || {}, fetcher);
|
|
4924
5509
|
return {
|
|
4925
5510
|
catalog: {
|
|
4926
|
-
listProducts: (overrides) => listProducts({ ...
|
|
4927
|
-
getProduct: (idOrSlug, overrides) => getProduct(idOrSlug, { ...
|
|
5511
|
+
listProducts: (overrides) => listProducts({ ...config, ...overrides }),
|
|
5512
|
+
getProduct: (idOrSlug, overrides) => getProduct(idOrSlug, { ...config, ...overrides })
|
|
4928
5513
|
},
|
|
4929
5514
|
mockup: mockupService
|
|
4930
5515
|
};
|
|
@@ -4949,6 +5534,7 @@ function createClient(config2) {
|
|
|
4949
5534
|
DEFAULT_ARTWORK_URL,
|
|
4950
5535
|
DEFAULT_ASPECT_RATIO,
|
|
4951
5536
|
DEFAULT_COLOR,
|
|
5537
|
+
DEFAULT_MOCKUP_BASE,
|
|
4952
5538
|
DEFAULT_PLACEMENT_DIMENSIONS,
|
|
4953
5539
|
Elements,
|
|
4954
5540
|
ErrorManager,
|
|
@@ -4973,9 +5559,9 @@ function createClient(config2) {
|
|
|
4973
5559
|
VueAdapter,
|
|
4974
5560
|
adapterRegistry,
|
|
4975
5561
|
autoRegister,
|
|
5562
|
+
buildMockupUrl,
|
|
4976
5563
|
componentRegistry,
|
|
4977
5564
|
computeDisabledChoices,
|
|
4978
|
-
config,
|
|
4979
5565
|
createAddToCartEvent,
|
|
4980
5566
|
createAddToCartHandler,
|
|
4981
5567
|
createCartDetail,
|
|
@@ -5015,6 +5601,7 @@ function createClient(config2) {
|
|
|
5015
5601
|
getEffectiveAlignment,
|
|
5016
5602
|
getIncompleteSelectionMessage,
|
|
5017
5603
|
getMissingSelections,
|
|
5604
|
+
getMockupUrl,
|
|
5018
5605
|
getOptionRenderType,
|
|
5019
5606
|
getPricePreview,
|
|
5020
5607
|
getProduct,
|
|
@@ -5032,7 +5619,6 @@ function createClient(config2) {
|
|
|
5032
5619
|
prepareOptionRenderData,
|
|
5033
5620
|
registerStandardComponents,
|
|
5034
5621
|
resolveBestCombination,
|
|
5035
|
-
resolveMockupConfig,
|
|
5036
5622
|
resolveMockupId,
|
|
5037
5623
|
resolveVariantId,
|
|
5038
5624
|
retryOperation,
|