@wewear/virtual-try-on 1.3.7 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,117 +1,16 @@
1
- async function callVirtualTryOnApi(baseUrl, ww_access_token, ww_user_id, ww_product_image, ww_image) {
2
- const formData = new FormData();
3
- formData.append("ww_user_id", ww_user_id);
4
- formData.append("ww_product_image", ww_product_image);
5
- formData.append("ww_image", ww_image, "captured-image.jpg");
6
- formData.append("ww_access_token", ww_access_token);
7
- const response = await fetch(`${baseUrl}/api/virtual-try-on`, {
8
- method: "POST",
9
- body: formData,
10
- });
11
- if (!response.ok) {
12
- console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
13
- if (response.status === 500) {
14
- return { "imageUrl": null, "error": { message: `Service unavailable. Please try again later.`, type: 'error' } };
15
- }
16
- else if (response.status === 503) {
17
- return { "imageUrl": null, "error": { message: `Service overloaded. Please try again later.`, type: 'warning' } };
18
- }
19
- return { "imageUrl": null, "error": { message: `${response.statusText}`, type: 'warning' } };
20
- }
21
- const result = await response.json();
22
- console.log("[WeWear VTO] API response:", result);
23
- return result;
24
- }
25
-
26
1
  /** CSS class names for consistent styling */
27
2
  const CSS_CLASSES = {
28
3
  BUTTON_CONTAINER: "wewear-vto-button-container",
29
4
  BUTTON: "wewear-vto-button",
30
5
  MODAL: "wewear-vto-modal",
6
+ PREVIEW_BADGE: "ww-vto-preview-badge",
31
7
  };
32
8
  /** Z-index values for proper layering */
33
9
  const Z_INDEX = {
34
10
  BUTTON: 10,
35
11
  MODAL: 99999,
36
12
  };
37
- /** Camera constraints for optimal capture */
38
- const CAMERA_CONSTRAINTS = {
39
- video: {
40
- width: { ideal: 1280 },
41
- height: { ideal: 720 },
42
- facingMode: "user", // Front-facing camera
43
- },
44
- audio: false,
45
- };
46
- /** Image capture settings */
47
- const IMAGE_SETTINGS = {
48
- FORMAT: "image/jpeg",
49
- QUALITY: 0.8,
50
- };
51
-
52
- async function startCamera(video, loadingIndicator, captureButton) {
53
- console.log("[WeWear VTO] Starting camera...");
54
- try {
55
- console.log("[WeWear VTO] Requesting camera access...");
56
- const stream = await navigator.mediaDevices.getUserMedia(CAMERA_CONSTRAINTS);
57
- video.srcObject = stream;
58
- video.onloadedmetadata = () => {
59
- console.log("[WeWear VTO] Camera stream loaded successfully");
60
- loadingIndicator.style.display = "none";
61
- captureButton.style.display = "flex";
62
- };
63
- }
64
- catch (error) {
65
- console.error("[WeWear VTO] Camera access error:", error);
66
- loadingIndicator.innerHTML =
67
- "Camera access denied. Please allow camera permissions.";
68
- }
69
- }
70
- function stopCamera(video) {
71
- const stream = video.srcObject;
72
- if (stream) {
73
- const tracks = stream.getTracks();
74
- tracks.forEach((track) => {
75
- track.stop();
76
- });
77
- video.srcObject = null;
78
- }
79
- }
80
- async function captureImageFromVideo(video, canvas) {
81
- // Set canvas dimensions to match video
82
- canvas.width = video.videoWidth;
83
- canvas.height = video.videoHeight;
84
- // Draw video frame to canvas
85
- const ctx = canvas.getContext("2d");
86
- if (!ctx) {
87
- throw new Error("Could not get canvas context");
88
- }
89
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
90
- // Convert canvas to blob
91
- return new Promise((resolve, reject) => {
92
- canvas.toBlob((blob) => {
93
- if (blob) {
94
- resolve(blob);
95
- }
96
- else {
97
- reject(new Error("Failed to create blob from canvas"));
98
- }
99
- }, IMAGE_SETTINGS.FORMAT, IMAGE_SETTINGS.QUALITY);
100
- });
101
- }
102
13
 
103
- function getCookie(name) {
104
- var _a;
105
- if (typeof document === "undefined")
106
- return null;
107
- const value = `; ${document.cookie}`;
108
- const parts = value.split(`; ${name}=`);
109
- if (parts.length === 2) {
110
- const part = parts.pop();
111
- return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
112
- }
113
- return null;
114
- }
115
14
  function getPositionStyles(position) {
116
15
  switch (position) {
117
16
  case "bottom-left":
@@ -241,140 +140,18 @@ function createButtonContainer(buttonPosition, hasVirtualTryOn = false, onCamera
241
140
  }
242
141
  function createButton(buttonPosition, onClick, disabled = false) {
243
142
  const container = createButtonContainer(buttonPosition, false, onClick);
244
- const cameraButton = container.querySelector('.ww-camera-btn');
143
+ const cameraButton = container.querySelector(".ww-camera-btn");
245
144
  if (cameraButton) {
246
145
  cameraButton.disabled = disabled;
247
146
  if (disabled) {
248
- cameraButton.style.opacity = '0.5';
249
- cameraButton.style.cursor = 'not-allowed';
250
- cameraButton.title = 'Find your ideal size first';
147
+ cameraButton.style.opacity = "0.5";
148
+ cameraButton.style.cursor = "not-allowed";
149
+ cameraButton.title = "Find your ideal size first";
251
150
  }
252
151
  }
253
152
  return container;
254
153
  }
255
154
 
256
- function showCameraModal(callbacks) {
257
- console.log("[WeWear VTO] Opening camera modal...");
258
- // Remove any existing modals first
259
- removeElements(`.${CSS_CLASSES.MODAL}`);
260
- // Create modal container
261
- const modal = document.createElement("div");
262
- modal.className = CSS_CLASSES.MODAL;
263
- modal.style.cssText = `
264
- position: fixed;
265
- top: 0;
266
- left: 0;
267
- width: 100%;
268
- height: 100%;
269
- background-color: rgba(0, 0, 0, 0.95);
270
- display: flex;
271
- flex-direction: column;
272
- align-items: center;
273
- justify-content: center;
274
- z-index: ${Z_INDEX.MODAL};
275
- padding: 20px;
276
- box-sizing: border-box;
277
- `;
278
- // Create camera container
279
- const cameraContainer = document.createElement("div");
280
- cameraContainer.style.cssText = `
281
- position: relative;
282
- width: 100%;
283
- max-width: 500px;
284
- height: 70vh;
285
- background-color: #000;
286
- border-radius: 12px;
287
- overflow: hidden;
288
- display: flex;
289
- flex-direction: column;
290
- align-items: center;
291
- justify-content: center;
292
- `;
293
- // Create video element
294
- const video = document.createElement("video");
295
- video.autoplay = true;
296
- video.playsInline = true;
297
- video.muted = true;
298
- video.style.cssText = `
299
- width: 100%;
300
- height: 100%;
301
- object-fit: cover;
302
- border-radius: 12px;
303
- `;
304
- // Create canvas for capturing
305
- const canvas = document.createElement("canvas");
306
- canvas.style.display = "none";
307
- // Create capture button
308
- const captureButton = document.createElement("button");
309
- captureButton.innerHTML = "";
310
- captureButton.style.cssText = `
311
- position: absolute;
312
- bottom: 20px;
313
- left: 50%;
314
- transform: translateX(-50%);
315
- width: 60px;
316
- height: 60px;
317
- border: 3px solid white;
318
- background-color: rgba(0, 0, 0, 0.7);
319
- border-radius: 50%;
320
- cursor: pointer;
321
- display: none;
322
- align-items: center;
323
- justify-content: center;
324
- transition: all 0.2s ease;
325
- `;
326
- // Create close button
327
- const closeButton = document.createElement("button");
328
- closeButton.innerHTML = "×";
329
- closeButton.style.cssText = `
330
- position: absolute;
331
- top: 15px;
332
- right: 15px;
333
- width: 40px;
334
- height: 40px;
335
- border: none;
336
- background-color: rgba(0, 0, 0, 0.7);
337
- color: white;
338
- font-size: 24px;
339
- font-weight: bold;
340
- cursor: pointer;
341
- border-radius: 50%;
342
- display: flex;
343
- align-items: center;
344
- justify-content: center;
345
- `;
346
- // Create loading indicator
347
- const loadingIndicator = document.createElement("div");
348
- loadingIndicator.innerHTML = "Initializing camera...";
349
- loadingIndicator.style.cssText = `
350
- color: white;
351
- font-size: 16px;
352
- position: absolute;
353
- top: 50%;
354
- left: 50%;
355
- transform: translate(-50%, -50%);
356
- `;
357
- // Add event listeners
358
- closeButton.onclick = () => {
359
- stopCamera(video);
360
- modal.remove();
361
- };
362
- captureButton.onclick = async () => {
363
- await callbacks.onCapture(video, canvas);
364
- };
365
- // Assemble the camera modal
366
- cameraContainer.appendChild(video);
367
- cameraContainer.appendChild(canvas);
368
- cameraContainer.appendChild(captureButton);
369
- cameraContainer.appendChild(closeButton);
370
- cameraContainer.appendChild(loadingIndicator);
371
- modal.appendChild(cameraContainer);
372
- document.body.appendChild(modal);
373
- console.log("[WeWear VTO] Camera modal added to DOM");
374
- // Start camera
375
- startCamera(video, loadingIndicator, captureButton);
376
- }
377
-
378
155
  /**
379
156
  * Creates a loading overlay that can be shown in different containers
380
157
  */
@@ -427,24 +204,6 @@ function createLoadingOverlay(text = "Processing...") {
427
204
  `;
428
205
  return overlay;
429
206
  }
430
- /**
431
- * Shows a loading overlay in a modal container
432
- */
433
- function showModalLoading(container) {
434
- // Remove any existing loading overlays
435
- removeModalLoading();
436
- // Show loading in the product area
437
- showProductLoading(container);
438
- }
439
- /**
440
- * Removes loading overlay from modal
441
- */
442
- function removeModalLoading() {
443
- const existingOverlay = document.body.querySelector(".ww-loading-overlay");
444
- if (existingOverlay) {
445
- existingOverlay.remove();
446
- }
447
- }
448
207
  /**
449
208
  * Shows a loading overlay in the product gallery container
450
209
  */
@@ -469,202 +228,115 @@ function removeProductLoading(container) {
469
228
  }
470
229
  }
471
230
 
472
- function showReviewModal(imageBlob, callbacks) {
473
- console.log("[WeWear VTO] Opening review modal...");
474
- // Remove any existing modals first
475
- removeElements(`.${CSS_CLASSES.MODAL}`);
476
- // Create image URL for preview
477
- const imageUrl = URL.createObjectURL(imageBlob);
478
- // Create modal container
479
- const modal = document.createElement("div");
480
- modal.className = CSS_CLASSES.MODAL;
481
- modal.style.cssText = `
482
- position: fixed;
483
- top: 0;
484
- left: 0;
485
- width: 100%;
486
- height: 100%;
487
- background-color: rgba(0, 0, 0, 0.95);
488
- display: flex;
489
- flex-direction: column;
490
- align-items: center;
491
- justify-content: center;
492
- z-index: ${Z_INDEX.MODAL};
493
- padding: 20px;
494
- box-sizing: border-box;
495
- gap: 20px;
496
- `;
497
- // Create review container
498
- const reviewContainer = document.createElement("div");
499
- reviewContainer.style.cssText = `
500
- position: relative;
501
- width: 100%;
502
- max-width: 500px;
503
- height: 63vh;
504
- background-color: #000;
505
- border-radius: 12px;
506
- overflow: hidden;
507
- display: flex;
508
- flex-direction: column;
509
- align-items: center;
510
- justify-content: center;
511
- `;
512
- // Create image element
513
- const image = document.createElement("img");
514
- image.src = imageUrl;
515
- image.alt = "Review your photo";
516
- image.style.cssText = `
517
- width: 100%;
518
- height: 100%;
519
- object-fit: cover;
520
- border-radius: 12px;
521
- `;
522
- // Create close button
523
- const closeButton = document.createElement("button");
524
- closeButton.innerHTML = "×";
525
- closeButton.style.cssText = `
526
- position: absolute;
527
- top: 15px;
528
- right: 15px;
529
- width: 40px;
530
- height: 40px;
531
- border: none;
532
- background-color: rgba(0, 0, 0, 0.7);
533
- color: white;
534
- font-size: 24px;
535
- font-weight: bold;
536
- cursor: pointer;
537
- border-radius: 50%;
538
- display: flex;
539
- align-items: center;
540
- justify-content: center;
541
- `;
542
- // Create button container
543
- const buttonContainer = document.createElement("div");
544
- buttonContainer.style.cssText = `
545
- display: flex;
546
- gap: 15px;
547
- width: 100%;
548
- max-width: 500px;
549
- `;
550
- // Create retake button
551
- const retakeButton = document.createElement("button");
552
- retakeButton.textContent = "Retake";
553
- retakeButton.style.cssText = `
554
- flex: 1;
555
- padding: 12px 24px;
556
- background-color: rgba(255, 255, 255, 0.9);
557
- color: black;
558
- border-radius: 8px;
559
- border: none;
560
- font-size: 16px;
561
- font-weight: normal;
562
- cursor: pointer;
563
- `;
564
- // Create use photo button
565
- const usePhotoButton = document.createElement("button");
566
- usePhotoButton.textContent = "Use Photo";
567
- usePhotoButton.style.cssText = `
568
- flex: 1;
569
- padding: 12px 24px;
570
- background-color: rgba(0, 0, 0, 0.7);
571
- color: white;
572
- border-radius: 8px;
573
- border: none;
574
- font-size: 16px;
575
- font-weight: normal;
576
- cursor: pointer;
577
- `;
578
- // Add event listeners
579
- closeButton.onclick = () => {
580
- URL.revokeObjectURL(imageUrl); // Clean up
581
- modal.remove();
582
- };
583
- retakeButton.onclick = () => {
584
- URL.revokeObjectURL(imageUrl); // Clean up
585
- modal.remove();
586
- callbacks.onRetake();
587
- };
588
- usePhotoButton.onclick = async () => {
589
- URL.revokeObjectURL(imageUrl); // Clean up
590
- modal.remove();
591
- await callbacks.onAccept(imageBlob);
592
- };
593
- // Assemble the review modal
594
- buttonContainer.appendChild(retakeButton);
595
- buttonContainer.appendChild(usePhotoButton);
596
- reviewContainer.appendChild(image);
597
- reviewContainer.appendChild(closeButton);
598
- modal.appendChild(reviewContainer);
599
- modal.appendChild(buttonContainer);
600
- document.body.appendChild(modal);
601
- console.log("[WeWear VTO] Review modal added to DOM");
231
+ function createPreviewBadge() {
232
+ const badge = document.createElement("div");
233
+ badge.className = CSS_CLASSES.PREVIEW_BADGE;
234
+ badge.innerText = "PREVIEW";
235
+ const styleId = "ww-vto-badge-styles";
236
+ if (!document.getElementById(styleId)) {
237
+ const styles = document.createElement("style");
238
+ styles.id = styleId;
239
+ styles.innerHTML = `
240
+ .${CSS_CLASSES.PREVIEW_BADGE} {
241
+ position: absolute;
242
+ top: 16px;
243
+ right: 16px;
244
+ background-color: rgba(0, 0, 0, 0.6);
245
+ color: white;
246
+ padding: 4px 8px;
247
+ border-radius: 4px;
248
+ font-size: 12px;
249
+ font-weight: 600;
250
+ z-index: 10;
251
+ animation: ww-vto-pulse 2s infinite;
252
+ pointer-events: none;
253
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
254
+ }
255
+
256
+ @keyframes ww-vto-pulse {
257
+ 0% {
258
+ transform: scale(1);
259
+ opacity: 0.9;
260
+ }
261
+ 50% {
262
+ transform: scale(1.05);
263
+ opacity: 1;
264
+ }
265
+ 100% {
266
+ transform: scale(1);
267
+ opacity: 0.9;
268
+ }
269
+ }
270
+ `;
271
+ document.head.appendChild(styles);
272
+ }
273
+ return badge;
602
274
  }
603
275
 
604
276
  function showAlert(container, message, type) {
605
277
  var _a;
606
278
  removeAlert(container); // Remove any existing alert
607
- const alertDiv = document.createElement('div');
279
+ const alertDiv = document.createElement("div");
608
280
  alertDiv.className = `ww-alert ww-alert-${type}`;
609
- alertDiv.setAttribute('role', type === 'error' ? 'alert' : 'status');
610
- alertDiv.setAttribute('aria-live', 'assertive');
611
- alertDiv.style.cssText = `
612
- position: absolute;
613
- top: 16px;
614
- left: 50%;
615
- transform: translateX(-50%) translateY(-10px);
616
- background: ${type === 'error' ? '#FEE2E2' : '#FEF9C3'};
617
- color: ${type === 'error' ? '#B91C1C' : '#92400E'};
618
- border: 1px solid ${type === 'error' ? '#F87171' : '#FBBF24'};
619
- border-radius: 8px;
620
- padding: 12px 16px;
621
- z-index: 9999;
622
- font-size: 15px;
623
- font-weight: 500;
624
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
625
- display: flex;
626
- align-items: center;
627
- min-width: 220px;
628
- max-width: 90%;
629
- opacity: 0;
630
- transition: opacity 0.3s ease, transform 0.3s ease;
281
+ alertDiv.setAttribute("role", "alert" );
282
+ alertDiv.setAttribute("aria-live", "assertive");
283
+ alertDiv.style.cssText = `
284
+ position: absolute;
285
+ top: 16px;
286
+ left: 50%;
287
+ transform: translateX(-50%) translateY(-10px);
288
+ background: ${"#FEE2E2" };
289
+ color: ${"#B91C1C" };
290
+ border: 1px solid ${"#F87171" };
291
+ border-radius: 8px;
292
+ padding: 12px 16px;
293
+ z-index: 9999;
294
+ font-size: 15px;
295
+ font-weight: 500;
296
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
297
+ display: flex;
298
+ align-items: center;
299
+ min-width: 220px;
300
+ max-width: 90%;
301
+ opacity: 0;
302
+ transition: opacity 0.3s ease, transform 0.3s ease;
631
303
  `;
632
- alertDiv.innerHTML = `
633
- <span style="flex: 1; line-height: 1.4;">${message}</span>
634
- <button type="button" aria-label="Close alert"
635
- style="
636
- background: none;
637
- border: none;
638
- font-size: 20px;
639
- color: inherit;
640
- cursor: pointer;
641
- margin-left: 12px;
642
- padding: 0;
643
- line-height: 1;
644
- ">
645
- &times;
646
- </button>
304
+ alertDiv.innerHTML = `
305
+ <span style="flex: 1; line-height: 1.4;">${message}</span>
306
+ <button type="button" aria-label="Close alert"
307
+ style="
308
+ background: none;
309
+ border: none;
310
+ font-size: 20px;
311
+ color: inherit;
312
+ cursor: pointer;
313
+ margin-left: 12px;
314
+ padding: 0;
315
+ line-height: 1;
316
+ ">
317
+ &times;
318
+ </button>
647
319
  `;
648
320
  // Close on button click
649
- (_a = alertDiv.querySelector('button')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => {
321
+ (_a = alertDiv.querySelector("button")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
650
322
  fadeOutAndRemove(alertDiv);
651
323
  });
652
324
  container.appendChild(alertDiv);
653
325
  // Animate in
654
326
  requestAnimationFrame(() => {
655
- alertDiv.style.opacity = '1';
656
- alertDiv.style.transform = 'translateX(-50%) translateY(0)';
327
+ alertDiv.style.opacity = "1";
328
+ alertDiv.style.transform = "translateX(-50%) translateY(0)";
657
329
  });
658
330
  }
659
331
  function fadeOutAndRemove(element) {
660
- element.style.opacity = '0';
661
- element.style.transform = 'translateX(-50%) translateY(-10px)';
332
+ element.style.opacity = "0";
333
+ element.style.transform = "translateX(-50%) translateY(-10px)";
662
334
  setTimeout(() => {
663
335
  element.remove();
664
336
  }, 300);
665
337
  }
666
338
  function removeAlert(container) {
667
- const alert = container.querySelector('.ww-alert');
339
+ const alert = container.querySelector(".ww-alert");
668
340
  if (alert)
669
341
  fadeOutAndRemove(alert);
670
342
  }
@@ -672,10 +344,13 @@ function removeAlert(container) {
672
344
  class VirtualTryOnWidget {
673
345
  constructor(config) {
674
346
  this.virtualTryOnImageUrl = null;
347
+ this.originalProductImageUrl = null;
675
348
  this.isShowingVirtualTryOn = false;
676
- this.lastApiParams = null;
349
+ this.lastModelImage = null;
677
350
  this.cookieCheckInterval = null;
678
351
  this.cameraButton = null;
352
+ this.iframeMessageListener = null;
353
+ this.previewBadge = null;
679
354
  this.config = {
680
355
  baseUrl: config.baseUrl,
681
356
  productPageSelector: config.productPageSelector,
@@ -704,40 +379,18 @@ class VirtualTryOnWidget {
704
379
  if (getComputedStyle(container).position === "static") {
705
380
  container.style.position = "relative";
706
381
  }
707
- // Check required cookies
708
- const ww_access_token = getCookie("ww_access_token");
709
- const ww_user_id = getCookie("ww_user_id");
710
- const cookiesPresent = !!ww_access_token && !!ww_user_id;
711
382
  // Create and add the virtual try-on button
712
383
  const button = createButton(this.config.buttonPosition, async () => {
713
384
  if (this.cameraButton && !this.cameraButton.disabled) {
714
385
  await this.handleTryOnClick();
715
386
  }
716
- }, !cookiesPresent);
387
+ });
717
388
  // Store reference to camera button for dynamic enable/disable
718
- this.cameraButton = button.querySelector('.ww-camera-btn');
389
+ this.cameraButton = button.querySelector(".ww-camera-btn");
719
390
  container.appendChild(button);
720
391
  console.log("[WeWear VTO] Widget initialized successfully");
721
- // Periodically check cookies and update button state
722
- this.cookieCheckInterval = window.setInterval(() => {
723
- const accessToken = getCookie("ww_access_token");
724
- const userId = getCookie("ww_user_id");
725
- const present = !!accessToken && !!userId;
726
- if (this.cameraButton) {
727
- if (present && this.cameraButton.disabled) {
728
- this.cameraButton.disabled = false;
729
- this.cameraButton.style.opacity = '1';
730
- this.cameraButton.style.cursor = 'pointer';
731
- this.cameraButton.title = '';
732
- }
733
- else if (!present && !this.cameraButton.disabled) {
734
- this.cameraButton.disabled = true;
735
- this.cameraButton.style.opacity = '0.5';
736
- this.cameraButton.style.cursor = 'not-allowed';
737
- this.cameraButton.title = 'Required cookies missing';
738
- }
739
- }
740
- }, 2000); // check every 2 seconds
392
+ // Listen for messages from the photo upload page
393
+ this.setupIframeListener();
741
394
  }
742
395
  catch (error) {
743
396
  console.error("[WeWear VTO] Initialization failed:", error);
@@ -751,152 +404,214 @@ class VirtualTryOnWidget {
751
404
  console.log("[WeWear VTO] Button clicked, starting try-on process...");
752
405
  try {
753
406
  // Get required data
754
- const ww_access_token = getCookie("ww_access_token");
755
- const ww_user_id = getCookie("ww_user_id");
756
- const productImageElement = document.querySelector(this.config.productImageSelector);
757
- const ww_product_image = (productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.src) || (productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.getAttribute('data-src')) || '';
407
+ if (!this.originalProductImageUrl) {
408
+ const productImageElement = document.querySelector(this.config.productImageSelector);
409
+ this.originalProductImageUrl =
410
+ (productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.src) ||
411
+ (productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.getAttribute("data-src")) ||
412
+ "";
413
+ }
414
+ const ww_product_image = this.originalProductImageUrl;
758
415
  console.log("[WeWear VTO] Retrieved data:", {
759
- ww_user_id,
760
416
  ww_product_image,
761
- ww_access_token,
762
417
  });
763
- // Validate required data
764
- if (!ww_user_id) {
765
- console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
766
- return;
767
- }
768
418
  if (!ww_product_image) {
769
419
  console.warn("[WeWear VTO] Product image not found:", this.config.productImageSelector);
770
420
  return;
771
421
  }
772
- if (!ww_access_token) {
773
- console.warn("[WeWear VTO] Missing required cookie: ww_access_token");
774
- return;
775
- }
776
- // Show camera capture modal
777
- this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image);
422
+ // Open the photo upload page in a modal
423
+ const photoUploadUrl = new URL(`${this.config.baseUrl}`);
424
+ photoUploadUrl.searchParams.append("ww_product_image", ww_product_image);
425
+ this.showPhotoUploadModal(photoUploadUrl.toString());
778
426
  }
779
427
  catch (error) {
780
428
  console.error("[WeWear VTO] Try-on request failed:", error);
781
429
  }
782
430
  }
783
431
  /**
784
- * Shows camera modal with appropriate callbacks
785
- * @private
786
- */
787
- showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image) {
788
- const callbacks = {
789
- onCapture: async (video, canvas) => {
790
- try {
791
- // Capture image from video
792
- const imageBlob = await captureImageFromVideo(video, canvas);
793
- // Stop camera and show review modal
794
- stopCamera(video);
795
- // Show review modal instead of immediately processing
796
- this.showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_image);
797
- }
798
- catch (error) {
799
- console.error("[WeWear VTO] Image capture error:", error);
800
- }
801
- },
802
- };
803
- showCameraModal(callbacks);
804
- }
805
- /**
806
- * Shows review modal with appropriate callbacks
432
+ * Shows a modal with an iframe for the photo upload page
807
433
  * @private
808
434
  */
809
- showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_image) {
810
- const callbacks = {
811
- onRetake: () => {
812
- // Reopen camera modal
813
- this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image);
814
- },
815
- onAccept: async (acceptedImageBlob) => {
816
- await this.processAcceptedImage(acceptedImageBlob, ww_access_token, ww_user_id, ww_product_image, document.querySelector(this.config.gallerySelector));
817
- },
435
+ showPhotoUploadModal(url) {
436
+ // Remove existing modals
437
+ removeElements(`.${CSS_CLASSES.MODAL}`);
438
+ const modal = document.createElement("div");
439
+ modal.className = CSS_CLASSES.MODAL;
440
+ modal.style.cssText = `
441
+ position: fixed;
442
+ top: 0;
443
+ left: 0;
444
+ width: 100%;
445
+ height: 100%;
446
+ background-color: rgba(0, 0, 0, 0.5);
447
+ display: flex;
448
+ justify-content: center;
449
+ align-items: center;
450
+ z-index: 1000;
451
+ `;
452
+ const iframe = document.createElement("iframe");
453
+ iframe.src = url;
454
+ iframe.style.cssText = `
455
+ width: 90%;
456
+ height: 90%;
457
+ max-width: 800px;
458
+ max-height: 600px;
459
+ border: none;
460
+ border-radius: 8px;
461
+ `;
462
+ const closeButton = document.createElement("button");
463
+ closeButton.innerText = "X";
464
+ closeButton.style.cssText = `
465
+ position: absolute;
466
+ top: 10px;
467
+ right: 10px;
468
+ background: white;
469
+ border: none;
470
+ border-radius: 50%;
471
+ width: 30px;
472
+ height: 30px;
473
+ cursor: pointer;
474
+ `;
475
+ closeButton.onclick = () => {
476
+ modal.remove();
818
477
  };
819
- showReviewModal(imageBlob, callbacks);
478
+ modal.appendChild(iframe);
479
+ modal.appendChild(closeButton);
480
+ document.body.appendChild(modal);
820
481
  }
821
482
  /**
822
- * Processes the accepted image by calling the API
483
+ * Sets up listener for messages from the iframe
823
484
  * @private
824
485
  */
825
- async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_image, container) {
826
- try {
827
- console.log("[WeWear VTO] Processing accepted image...");
828
- // Show loading in the review modal first
829
- showModalLoading(container);
830
- // Store the API parameters for potential refresh
831
- this.lastApiParams = {
832
- imageBlob,
833
- ww_access_token,
834
- ww_user_id,
835
- ww_product_image,
836
- };
837
- // Call the API with the accepted image
838
- const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_image, imageBlob);
839
- // Remove modal loading and close modal
840
- removeModalLoading();
841
- removeElements(`.${CSS_CLASSES.MODAL}`);
842
- if (result === null || result === void 0 ? void 0 : result.imageUrl) {
843
- this.replaceProductImage(result.imageUrl);
486
+ setupIframeListener() {
487
+ // Remove existing listener if any
488
+ if (this.iframeMessageListener) {
489
+ window.removeEventListener("message", this.iframeMessageListener);
490
+ }
491
+ this.iframeMessageListener = (event) => {
492
+ if (event.origin !== new URL(this.config.baseUrl).origin) {
493
+ return;
844
494
  }
845
- else if (result === null || result === void 0 ? void 0 : result.error) {
846
- showAlert(container, result.error.message, result.error.type);
495
+ switch (event.data.type) {
496
+ case "VTO_IMAGE_SELECTED":
497
+ if (event.data.image) {
498
+ this.lastModelImage = event.data.image;
499
+ this.startVirtualTryOn();
500
+ }
501
+ break;
502
+ case "CLOSE_MODAL":
503
+ removeElements(`.${CSS_CLASSES.MODAL}`);
504
+ break;
847
505
  }
848
- }
849
- catch (error) {
850
- console.error("[WeWear VTO] Error processing accepted image:", error);
851
- // Remove loading on error
852
- removeModalLoading();
853
- }
506
+ };
507
+ window.addEventListener("message", this.iframeMessageListener);
854
508
  }
855
- /**
856
- * Recalls the virtual try-on API with the same parameters
857
- * @private
858
- */
859
- async refreshVirtualTryOn() {
860
- if (!this.lastApiParams) {
861
- console.warn("[WeWear VTO] No previous API parameters available for refresh");
509
+ async startVirtualTryOn(isRefresh = false) {
510
+ if (isRefresh) {
511
+ console.log("[WeWear VTO] Refreshing virtual try-on with previous parameters...");
512
+ }
513
+ if (!this.lastModelImage || !this.originalProductImageUrl) {
514
+ console.warn("[WeWear VTO] Missing required data to start virtual try-on.");
862
515
  return;
863
516
  }
517
+ // Hide the modal and show loading indicator
518
+ const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
519
+ if (modal && modal instanceof HTMLElement) {
520
+ modal.style.display = "none";
521
+ }
522
+ const container = document.querySelector(this.config.gallerySelector);
523
+ if (container instanceof HTMLElement) {
524
+ showProductLoading(container, "Generating your virtual try-on...");
525
+ }
864
526
  try {
865
- console.log("[WeWear VTO] Refreshing virtual try-on with previous parameters...");
866
- // Show loading in the product area
867
- const container = document.querySelector(this.config.gallerySelector);
868
- if (container instanceof HTMLElement) {
869
- showProductLoading(container, "Generating new virtual try-on...");
527
+ const submitResponse = await this.callVtoApi(this.lastModelImage, this.originalProductImageUrl);
528
+ let status = await this.fetchJobStatus(submitResponse.job_id);
529
+ const previews = [];
530
+ while (status.status !== "COMPLETED" && status.status !== "FAILED") {
531
+ await new Promise((r) => setTimeout(r, 3000));
532
+ status = await this.fetchJobStatus(submitResponse.job_id);
533
+ for (let i = previews.length; i < status.previews_available; i++) {
534
+ try {
535
+ const previewUrl = await this.fetchJobImage(submitResponse.job_id, i);
536
+ previews.push(previewUrl);
537
+ this.replaceProductImage(previewUrl);
538
+ if (!this.previewBadge && container instanceof HTMLElement) {
539
+ this.previewBadge = createPreviewBadge();
540
+ container.appendChild(this.previewBadge);
541
+ }
542
+ }
543
+ catch (e) {
544
+ if (!(e instanceof Error && e.message === "202_PROCESSING"))
545
+ throw e;
546
+ }
547
+ }
870
548
  }
871
- // Call the API with the stored parameters
872
- const result = await callVirtualTryOnApi(this.config.baseUrl, this.lastApiParams.ww_access_token, this.lastApiParams.ww_user_id, this.lastApiParams.ww_product_image, this.lastApiParams.imageBlob);
873
- if (result === null || result === void 0 ? void 0 : result.imageUrl) {
874
- this.virtualTryOnImageUrl = result.imageUrl;
875
- // Find the gallery container and update the image
876
- const container = document.querySelector(this.config.gallerySelector);
877
- if (container instanceof HTMLElement) {
878
- removeProductLoading(container);
879
- this.showVirtualTryOnImage(container);
880
- this.updateButtonContainer(container);
549
+ if (status.status === "COMPLETED") {
550
+ const finalImage = await this.fetchJobImage(submitResponse.job_id);
551
+ if (finalImage) {
552
+ this.replaceProductImage(finalImage);
553
+ }
554
+ if (this.previewBadge) {
555
+ this.previewBadge.remove();
556
+ this.previewBadge = null;
881
557
  }
882
558
  }
883
- else if (result === null || result === void 0 ? void 0 : result.error) {
884
- const container = document.querySelector(this.config.gallerySelector);
559
+ if (status.status === "FAILED") {
560
+ console.error("[WeWear VTO] VTO process failed:", status.message);
885
561
  if (container instanceof HTMLElement) {
886
- removeProductLoading(container);
887
- showAlert(container, result.error.message, result.error.type);
562
+ showAlert(container, status.message, "error");
888
563
  }
889
564
  }
890
565
  }
891
566
  catch (error) {
892
- console.error("[WeWear VTO] Error refreshing virtual try-on:", error);
893
- // Remove loading on error
894
- const container = document.querySelector(this.config.gallerySelector);
567
+ console.error("[WeWear VTO] Error during virtual try-on process:", error);
568
+ if (container instanceof HTMLElement) {
569
+ showAlert(container, "An unexpected error occurred.", "error");
570
+ }
571
+ }
572
+ finally {
895
573
  if (container instanceof HTMLElement) {
896
574
  removeProductLoading(container);
897
575
  }
576
+ removeElements(`.${CSS_CLASSES.MODAL}`);
898
577
  }
899
578
  }
579
+ async callVtoApi(modelImage, productImage) {
580
+ const formData = new FormData();
581
+ formData.append("model_image", modelImage, "model_image.png");
582
+ formData.append("ww_product_image", productImage);
583
+ const res = await fetch(`${this.config.baseUrl}/api/vto`, {
584
+ method: "POST",
585
+ body: formData,
586
+ });
587
+ if (!res.ok) {
588
+ const errorText = await res.text();
589
+ console.error("[WeWear VTO] API submission failed:", res.status, errorText);
590
+ throw new Error(`API submission failed: ${res.status}`);
591
+ }
592
+ return res.json();
593
+ }
594
+ async fetchJobStatus(jobId) {
595
+ const res = await fetch(`${this.config.baseUrl}/api/vto/status?job_id=${jobId}`);
596
+ if (!res.ok)
597
+ throw new Error(`Status check failed: ${res.status}`);
598
+ return res.json();
599
+ }
600
+ async fetchJobImage(jobId, previewIndex) {
601
+ let url = `${this.config.baseUrl}/api/vto/image?job_id=${jobId}`;
602
+ if (previewIndex !== undefined)
603
+ url += `&preview_index=${previewIndex}`;
604
+ const res = await fetch(url);
605
+ if (res.status === 202)
606
+ throw new Error("202_PROCESSING");
607
+ if (!res.ok)
608
+ throw new Error(`Image fetch failed: ${res.status}`);
609
+ const blob = await res.blob();
610
+ return URL.createObjectURL(blob);
611
+ }
612
+ async refreshVirtualTryOn() {
613
+ await this.startVirtualTryOn(true);
614
+ }
900
615
  /**
901
616
  * Replaces the product image in the gallery with the virtual try-on result
902
617
  * @private
@@ -916,8 +631,9 @@ class VirtualTryOnWidget {
916
631
  container.setAttribute("data-ww-original-content", container.innerHTML);
917
632
  }
918
633
  // Capture original image dimensions before replacement to prevent layout shift
919
- const originalImg = container.querySelector('img');
920
- if (originalImg && !container.hasAttribute("data-ww-original-dimensions")) {
634
+ const originalImg = container.querySelector("img");
635
+ if (originalImg &&
636
+ !container.hasAttribute("data-ww-original-dimensions")) {
921
637
  const computedStyle = window.getComputedStyle(originalImg);
922
638
  const rect = originalImg.getBoundingClientRect();
923
639
  container.setAttribute("data-ww-original-width", computedStyle.width);
@@ -958,6 +674,7 @@ class VirtualTryOnWidget {
958
674
  else {
959
675
  this.showVirtualTryOnImage(container);
960
676
  }
677
+ removeProductLoading(container);
961
678
  this.updateButtonContainer(container);
962
679
  }, this.isShowingVirtualTryOn);
963
680
  container.appendChild(buttonContainer);
@@ -971,44 +688,46 @@ class VirtualTryOnWidget {
971
688
  console.warn("[WeWear VTO] No virtual try-on image URL available");
972
689
  return;
973
690
  }
974
- // Clear existing content except buttons
975
- const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
976
- container.innerHTML = "";
691
+ // Remove all direct children except for the button container
692
+ Array.from(container.children).forEach((child) => {
693
+ if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
694
+ child.remove();
695
+ }
696
+ });
977
697
  // Get stored original dimensions to prevent layout shift
978
- const originalWidth = container.getAttribute("data-ww-original-width") || '';
979
- const originalHeight = container.getAttribute("data-ww-original-height") || '';
980
- const originalRectWidth = container.getAttribute("data-ww-original-rect-width") || '';
981
- const originalRectHeight = container.getAttribute("data-ww-original-rect-height") || '';
698
+ const originalWidth = container.getAttribute("data-ww-original-width") || "";
699
+ const originalHeight = container.getAttribute("data-ww-original-height") || "";
700
+ const originalRectWidth = container.getAttribute("data-ww-original-rect-width") || "";
701
+ const originalRectHeight = container.getAttribute("data-ww-original-rect-height") || "";
982
702
  const image = document.createElement("img");
983
703
  image.src = this.virtualTryOnImageUrl;
984
704
  image.alt = "Virtual Try-On Result";
985
705
  // Use original dimensions to prevent layout shift
986
- let widthStyle = '100%';
987
- let heightStyle = '100%';
706
+ let widthStyle = "100%";
707
+ let heightStyle = "100%";
988
708
  // Prefer computed style dimensions, fallback to bounding rect, then container fill
989
- if (originalWidth && originalWidth !== 'auto' && originalWidth !== '0px') {
709
+ if (originalWidth && originalWidth !== "auto" && originalWidth !== "0px") {
990
710
  widthStyle = originalWidth;
991
711
  }
992
- else if (originalRectWidth && originalRectWidth !== '0') {
712
+ else if (originalRectWidth && originalRectWidth !== "0") {
993
713
  widthStyle = `${originalRectWidth}px`;
994
714
  }
995
- if (originalHeight && originalHeight !== 'auto' && originalHeight !== '0px') {
715
+ if (originalHeight &&
716
+ originalHeight !== "auto" &&
717
+ originalHeight !== "0px") {
996
718
  heightStyle = originalHeight;
997
719
  }
998
- else if (originalRectHeight && originalRectHeight !== '0') {
720
+ else if (originalRectHeight && originalRectHeight !== "0") {
999
721
  heightStyle = `${originalRectHeight}px`;
1000
722
  }
1001
- image.style.cssText = `
1002
- width: ${widthStyle};
1003
- height: ${heightStyle};
1004
- object-fit: cover;
1005
- border-radius: 8px;
723
+ image.style.cssText = `
724
+ width: ${widthStyle};
725
+ height: ${heightStyle};
726
+ object-fit: cover;
727
+ border-radius: 8px;
1006
728
  `;
1007
- container.appendChild(image);
1008
- // Re-add buttons
1009
- existingButtons.forEach((btn) => {
1010
- container.appendChild(btn);
1011
- });
729
+ // Prepend the image to ensure buttons are rendered on top
730
+ container.prepend(image);
1012
731
  this.isShowingVirtualTryOn = true;
1013
732
  }
1014
733
  /**
@@ -1016,16 +735,18 @@ class VirtualTryOnWidget {
1016
735
  * @private
1017
736
  */
1018
737
  showOriginalImage(container) {
1019
- const originalContent = container.getAttribute("data-ww-original-content");
1020
- if (originalContent) {
1021
- // Store existing buttons
1022
- const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
1023
- // Restore original content
1024
- container.innerHTML = originalContent;
1025
- // Re-add buttons
1026
- existingButtons.forEach((btn) => {
1027
- container.appendChild(btn);
738
+ const originalContentHTML = container.getAttribute("data-ww-original-content");
739
+ if (originalContentHTML) {
740
+ // Remove all direct children except for the button container
741
+ Array.from(container.children).forEach((child) => {
742
+ if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
743
+ child.remove();
744
+ }
1028
745
  });
746
+ // Parse the original content and prepend it
747
+ const tempDiv = document.createElement("div");
748
+ tempDiv.innerHTML = originalContentHTML;
749
+ container.prepend(...Array.from(tempDiv.children));
1029
750
  }
1030
751
  this.isShowingVirtualTryOn = false;
1031
752
  }
@@ -1044,10 +765,20 @@ class VirtualTryOnWidget {
1044
765
  this.cookieCheckInterval = null;
1045
766
  }
1046
767
  this.cameraButton = null;
768
+ // Remove message listener
769
+ if (this.iframeMessageListener) {
770
+ window.removeEventListener("message", this.iframeMessageListener);
771
+ this.iframeMessageListener = null;
772
+ }
1047
773
  // Reset state
1048
774
  this.virtualTryOnImageUrl = null;
1049
775
  this.isShowingVirtualTryOn = false;
1050
- this.lastApiParams = null;
776
+ this.lastModelImage = null;
777
+ this.originalProductImageUrl = null;
778
+ if (this.previewBadge) {
779
+ this.previewBadge.remove();
780
+ this.previewBadge = null;
781
+ }
1051
782
  console.log("[WeWear VTO] Widget destroyed successfully");
1052
783
  }
1053
784
  catch (error) {
@@ -1089,18 +820,6 @@ function initVirtualTryOn(config) {
1089
820
  console.error("[WeWear VTO] Initialization error:", error);
1090
821
  }
1091
822
  }
1092
- function destroyVirtualTryOn() {
1093
- try {
1094
- if (widgetInstance) {
1095
- widgetInstance.destroy();
1096
- widgetInstance = null;
1097
- console.log("[WeWear VTO] Widget instance destroyed");
1098
- }
1099
- }
1100
- catch (error) {
1101
- console.error("[WeWear VTO] Error destroying widget:", error);
1102
- }
1103
- }
1104
823
  function getWidgetInstance() {
1105
824
  return widgetInstance;
1106
825
  }
@@ -1108,5 +827,5 @@ if (typeof window !== "undefined") {
1108
827
  initVirtualTryOn();
1109
828
  }
1110
829
 
1111
- export { VirtualTryOnWidget, destroyVirtualTryOn, getWidgetInstance, initVirtualTryOn };
830
+ export { VirtualTryOnWidget, getWidgetInstance, initVirtualTryOn };
1112
831
  //# sourceMappingURL=index.esm.js.map