@wewear/virtual-try-on 1.4.0 → 1.4.2

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