@wewear/virtual-try-on 1.4.0 → 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
13
 
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
-
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,174 +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 face overlay
308
- const faceOverlay = document.createElement("div");
309
- faceOverlay.style.cssText = `
310
- position: absolute;
311
- top: 0;
312
- left: 0;
313
- width: 100%;
314
- height: 100%;
315
- pointer-events: none;
316
- display: flex;
317
- align-items: center;
318
- justify-content: center;
319
- z-index: 2;
320
- `;
321
- // SVG face outline (simple oval)
322
- faceOverlay.innerHTML = `
323
- <svg width="380" height="520" viewBox="0 0 380 520" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity:0.7;">
324
- <ellipse cx="190" cy="240" rx="160" ry="220" stroke="rgba(0, 0, 0, 0.7)" stroke-width="6" fill="none" />
325
- </svg>
326
- `;
327
- // Create capture button
328
- const captureButton = document.createElement("button");
329
- captureButton.innerHTML = "";
330
- captureButton.style.cssText = `
331
- position: absolute;
332
- bottom: 20px;
333
- left: 50%;
334
- transform: translateX(-50%);
335
- width: 60px;
336
- height: 60px;
337
- border: 3px solid white;
338
- background-color: rgba(0, 0, 0, 0.7);
339
- border-radius: 50%;
340
- cursor: pointer;
341
- display: flex;
342
- align-items: center;
343
- justify-content: center;
344
- transition: all 0.2s ease;
345
- `;
346
- // Create close button
347
- const closeButton = document.createElement("button");
348
- closeButton.innerHTML = "×";
349
- closeButton.style.cssText = `
350
- position: absolute;
351
- top: 15px;
352
- right: 15px;
353
- width: 40px;
354
- height: 40px;
355
- border: none;
356
- background-color: rgba(0, 0, 0, 0.7);
357
- color: white;
358
- font-size: 24px;
359
- font-weight: bold;
360
- cursor: pointer;
361
- border-radius: 50%;
362
- display: flex;
363
- align-items: center;
364
- justify-content: center;
365
- `;
366
- // Create loading indicator
367
- const loadingIndicator = document.createElement("div");
368
- loadingIndicator.innerHTML = "Initializing camera...";
369
- loadingIndicator.style.cssText = `
370
- color: white;
371
- font-size: 16px;
372
- position: absolute;
373
- top: 50%;
374
- left: 50%;
375
- transform: translate(-50%, -50%);
376
- `;
377
- // Add event listeners
378
- closeButton.onclick = () => {
379
- stopCamera(video);
380
- modal.remove();
381
- };
382
- captureButton.onclick = async () => {
383
- await callbacks.onCapture(video, canvas);
384
- };
385
- // Assemble the camera modal
386
- cameraContainer.appendChild(video);
387
- cameraContainer.appendChild(faceOverlay);
388
- cameraContainer.appendChild(canvas);
389
- cameraContainer.appendChild(captureButton);
390
- cameraContainer.appendChild(closeButton);
391
- modal.appendChild(cameraContainer);
392
- // Add instruction text outside the camera box
393
- const instructionText = document.createElement("div");
394
- instructionText.style.cssText = `
395
- width: 100%;
396
- max-width: 500px;
397
- margin: 18px auto 0 auto;
398
- text-align: center;
399
- color: #fff;
400
- font-size: 18px;
401
- font-weight: bold;
402
- text-shadow: 0 2px 8px #000;
403
- `;
404
- instructionText.innerText = "Please align your face inside the outline for better results.";
405
- modal.appendChild(instructionText);
406
- document.body.appendChild(modal);
407
- console.log("[WeWear VTO] Camera modal added to DOM");
408
- // Start camera
409
- startCamera(video, loadingIndicator, captureButton);
410
- }
411
-
412
155
  /**
413
156
  * Creates a loading overlay that can be shown in different containers
414
157
  */
@@ -461,24 +204,6 @@ function createLoadingOverlay(text = "Processing...") {
461
204
  `;
462
205
  return overlay;
463
206
  }
464
- /**
465
- * Shows a loading overlay in a modal container
466
- */
467
- function showModalLoading(container) {
468
- // Remove any existing loading overlays
469
- removeModalLoading();
470
- // Show loading in the product area
471
- showProductLoading(container);
472
- }
473
- /**
474
- * Removes loading overlay from modal
475
- */
476
- function removeModalLoading() {
477
- const existingOverlay = document.body.querySelector(".ww-loading-overlay");
478
- if (existingOverlay) {
479
- existingOverlay.remove();
480
- }
481
- }
482
207
  /**
483
208
  * Shows a loading overlay in the product gallery container
484
209
  */
@@ -503,202 +228,115 @@ function removeProductLoading(container) {
503
228
  }
504
229
  }
505
230
 
506
- function showReviewModal(imageBlob, callbacks) {
507
- console.log("[WeWear VTO] Opening review modal...");
508
- // Remove any existing modals first
509
- removeElements(`.${CSS_CLASSES.MODAL}`);
510
- // Create image URL for preview
511
- const imageUrl = URL.createObjectURL(imageBlob);
512
- // Create modal container
513
- const modal = document.createElement("div");
514
- modal.className = CSS_CLASSES.MODAL;
515
- modal.style.cssText = `
516
- position: fixed;
517
- top: 0;
518
- left: 0;
519
- width: 100%;
520
- height: 100%;
521
- background-color: rgba(0, 0, 0, 0.95);
522
- display: flex;
523
- flex-direction: column;
524
- align-items: center;
525
- justify-content: center;
526
- z-index: ${Z_INDEX.MODAL};
527
- padding: 20px;
528
- box-sizing: border-box;
529
- gap: 20px;
530
- `;
531
- // Create review container
532
- const reviewContainer = document.createElement("div");
533
- reviewContainer.style.cssText = `
534
- position: relative;
535
- width: 100%;
536
- max-width: 500px;
537
- height: 63vh;
538
- background-color: #000;
539
- border-radius: 12px;
540
- overflow: hidden;
541
- display: flex;
542
- flex-direction: column;
543
- align-items: center;
544
- justify-content: center;
545
- `;
546
- // Create image element
547
- const image = document.createElement("img");
548
- image.src = imageUrl;
549
- image.alt = "Review your photo";
550
- image.style.cssText = `
551
- width: 100%;
552
- height: 100%;
553
- object-fit: cover;
554
- border-radius: 12px;
555
- `;
556
- // Create close button
557
- const closeButton = document.createElement("button");
558
- closeButton.innerHTML = "×";
559
- closeButton.style.cssText = `
560
- position: absolute;
561
- top: 15px;
562
- right: 15px;
563
- width: 40px;
564
- height: 40px;
565
- border: none;
566
- background-color: rgba(0, 0, 0, 0.7);
567
- color: white;
568
- font-size: 24px;
569
- font-weight: bold;
570
- cursor: pointer;
571
- border-radius: 50%;
572
- display: flex;
573
- align-items: center;
574
- justify-content: center;
575
- `;
576
- // Create button container
577
- const buttonContainer = document.createElement("div");
578
- buttonContainer.style.cssText = `
579
- display: flex;
580
- gap: 15px;
581
- width: 100%;
582
- max-width: 500px;
583
- `;
584
- // Create retake button
585
- const retakeButton = document.createElement("button");
586
- retakeButton.textContent = "Retake";
587
- retakeButton.style.cssText = `
588
- flex: 1;
589
- padding: 12px 24px;
590
- background-color: rgba(255, 255, 255, 0.9);
591
- color: black;
592
- border-radius: 8px;
593
- border: none;
594
- font-size: 16px;
595
- font-weight: normal;
596
- cursor: pointer;
597
- `;
598
- // Create use photo button
599
- const usePhotoButton = document.createElement("button");
600
- usePhotoButton.textContent = "Use Photo";
601
- usePhotoButton.style.cssText = `
602
- flex: 1;
603
- padding: 12px 24px;
604
- background-color: rgba(0, 0, 0, 0.7);
605
- color: white;
606
- border-radius: 8px;
607
- border: none;
608
- font-size: 16px;
609
- font-weight: normal;
610
- cursor: pointer;
611
- `;
612
- // Add event listeners
613
- closeButton.onclick = () => {
614
- URL.revokeObjectURL(imageUrl); // Clean up
615
- modal.remove();
616
- };
617
- retakeButton.onclick = () => {
618
- URL.revokeObjectURL(imageUrl); // Clean up
619
- modal.remove();
620
- callbacks.onRetake();
621
- };
622
- usePhotoButton.onclick = async () => {
623
- URL.revokeObjectURL(imageUrl); // Clean up
624
- modal.remove();
625
- await callbacks.onAccept(imageBlob);
626
- };
627
- // Assemble the review modal
628
- buttonContainer.appendChild(retakeButton);
629
- buttonContainer.appendChild(usePhotoButton);
630
- reviewContainer.appendChild(image);
631
- reviewContainer.appendChild(closeButton);
632
- modal.appendChild(reviewContainer);
633
- modal.appendChild(buttonContainer);
634
- document.body.appendChild(modal);
635
- 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;
636
274
  }
637
275
 
638
276
  function showAlert(container, message, type) {
639
277
  var _a;
640
278
  removeAlert(container); // Remove any existing alert
641
- const alertDiv = document.createElement('div');
279
+ const alertDiv = document.createElement("div");
642
280
  alertDiv.className = `ww-alert ww-alert-${type}`;
643
- alertDiv.setAttribute('role', type === 'error' ? 'alert' : 'status');
644
- alertDiv.setAttribute('aria-live', 'assertive');
645
- alertDiv.style.cssText = `
646
- position: absolute;
647
- top: 16px;
648
- left: 50%;
649
- transform: translateX(-50%) translateY(-10px);
650
- background: ${type === 'error' ? '#FEE2E2' : '#FEF9C3'};
651
- color: ${type === 'error' ? '#B91C1C' : '#92400E'};
652
- border: 1px solid ${type === 'error' ? '#F87171' : '#FBBF24'};
653
- border-radius: 8px;
654
- padding: 12px 16px;
655
- z-index: 9999;
656
- font-size: 15px;
657
- font-weight: 500;
658
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
659
- display: flex;
660
- align-items: center;
661
- min-width: 220px;
662
- max-width: 90%;
663
- opacity: 0;
664
- 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;
665
303
  `;
666
- alertDiv.innerHTML = `
667
- <span style="flex: 1; line-height: 1.4;">${message}</span>
668
- <button type="button" aria-label="Close alert"
669
- style="
670
- background: none;
671
- border: none;
672
- font-size: 20px;
673
- color: inherit;
674
- cursor: pointer;
675
- margin-left: 12px;
676
- padding: 0;
677
- line-height: 1;
678
- ">
679
- &times;
680
- </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>
681
319
  `;
682
320
  // Close on button click
683
- (_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", () => {
684
322
  fadeOutAndRemove(alertDiv);
685
323
  });
686
324
  container.appendChild(alertDiv);
687
325
  // Animate in
688
326
  requestAnimationFrame(() => {
689
- alertDiv.style.opacity = '1';
690
- alertDiv.style.transform = 'translateX(-50%) translateY(0)';
327
+ alertDiv.style.opacity = "1";
328
+ alertDiv.style.transform = "translateX(-50%) translateY(0)";
691
329
  });
692
330
  }
693
331
  function fadeOutAndRemove(element) {
694
- element.style.opacity = '0';
695
- element.style.transform = 'translateX(-50%) translateY(-10px)';
332
+ element.style.opacity = "0";
333
+ element.style.transform = "translateX(-50%) translateY(-10px)";
696
334
  setTimeout(() => {
697
335
  element.remove();
698
336
  }, 300);
699
337
  }
700
338
  function removeAlert(container) {
701
- const alert = container.querySelector('.ww-alert');
339
+ const alert = container.querySelector(".ww-alert");
702
340
  if (alert)
703
341
  fadeOutAndRemove(alert);
704
342
  }
@@ -706,10 +344,13 @@ function removeAlert(container) {
706
344
  class VirtualTryOnWidget {
707
345
  constructor(config) {
708
346
  this.virtualTryOnImageUrl = null;
347
+ this.originalProductImageUrl = null;
709
348
  this.isShowingVirtualTryOn = false;
710
- this.lastApiParams = null;
349
+ this.lastModelImage = null;
711
350
  this.cookieCheckInterval = null;
712
351
  this.cameraButton = null;
352
+ this.iframeMessageListener = null;
353
+ this.previewBadge = null;
713
354
  this.config = {
714
355
  baseUrl: config.baseUrl,
715
356
  productPageSelector: config.productPageSelector,
@@ -738,40 +379,18 @@ class VirtualTryOnWidget {
738
379
  if (getComputedStyle(container).position === "static") {
739
380
  container.style.position = "relative";
740
381
  }
741
- // Check required cookies
742
- const ww_access_token = getCookie("ww_access_token");
743
- const ww_user_id = getCookie("ww_user_id");
744
- const cookiesPresent = !!ww_access_token && !!ww_user_id;
745
382
  // Create and add the virtual try-on button
746
383
  const button = createButton(this.config.buttonPosition, async () => {
747
384
  if (this.cameraButton && !this.cameraButton.disabled) {
748
385
  await this.handleTryOnClick();
749
386
  }
750
- }, !cookiesPresent);
387
+ });
751
388
  // Store reference to camera button for dynamic enable/disable
752
- this.cameraButton = button.querySelector('.ww-camera-btn');
389
+ this.cameraButton = button.querySelector(".ww-camera-btn");
753
390
  container.appendChild(button);
754
391
  console.log("[WeWear VTO] Widget initialized successfully");
755
- // Periodically check cookies and update button state
756
- this.cookieCheckInterval = window.setInterval(() => {
757
- const accessToken = getCookie("ww_access_token");
758
- const userId = getCookie("ww_user_id");
759
- const present = !!accessToken && !!userId;
760
- if (this.cameraButton) {
761
- if (present && this.cameraButton.disabled) {
762
- this.cameraButton.disabled = false;
763
- this.cameraButton.style.opacity = '1';
764
- this.cameraButton.style.cursor = 'pointer';
765
- this.cameraButton.title = '';
766
- }
767
- else if (!present && !this.cameraButton.disabled) {
768
- this.cameraButton.disabled = true;
769
- this.cameraButton.style.opacity = '0.5';
770
- this.cameraButton.style.cursor = 'not-allowed';
771
- this.cameraButton.title = 'Required cookies missing';
772
- }
773
- }
774
- }, 2000); // check every 2 seconds
392
+ // Listen for messages from the photo upload page
393
+ this.setupIframeListener();
775
394
  }
776
395
  catch (error) {
777
396
  console.error("[WeWear VTO] Initialization failed:", error);
@@ -785,152 +404,214 @@ class VirtualTryOnWidget {
785
404
  console.log("[WeWear VTO] Button clicked, starting try-on process...");
786
405
  try {
787
406
  // Get required data
788
- const ww_access_token = getCookie("ww_access_token");
789
- const ww_user_id = getCookie("ww_user_id");
790
- const productImageElement = document.querySelector(this.config.productImageSelector);
791
- 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;
792
415
  console.log("[WeWear VTO] Retrieved data:", {
793
- ww_user_id,
794
416
  ww_product_image,
795
- ww_access_token,
796
417
  });
797
- // Validate required data
798
- if (!ww_user_id) {
799
- console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
800
- return;
801
- }
802
418
  if (!ww_product_image) {
803
419
  console.warn("[WeWear VTO] Product image not found:", this.config.productImageSelector);
804
420
  return;
805
421
  }
806
- if (!ww_access_token) {
807
- console.warn("[WeWear VTO] Missing required cookie: ww_access_token");
808
- return;
809
- }
810
- // Show camera capture modal
811
- 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());
812
426
  }
813
427
  catch (error) {
814
428
  console.error("[WeWear VTO] Try-on request failed:", error);
815
429
  }
816
430
  }
817
431
  /**
818
- * Shows camera modal with appropriate callbacks
432
+ * Shows a modal with an iframe for the photo upload page
819
433
  * @private
820
434
  */
821
- showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image) {
822
- const callbacks = {
823
- onCapture: async (video, canvas) => {
824
- try {
825
- // Capture image from video
826
- const imageBlob = await captureImageFromVideo(video, canvas);
827
- // Stop camera and show review modal
828
- stopCamera(video);
829
- // Show review modal instead of immediately processing
830
- this.showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_image);
831
- }
832
- catch (error) {
833
- console.error("[WeWear VTO] Image capture error:", error);
834
- }
835
- },
836
- };
837
- showCameraModal(callbacks);
838
- }
839
- /**
840
- * Shows review modal with appropriate callbacks
841
- * @private
842
- */
843
- showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_image) {
844
- const callbacks = {
845
- onRetake: () => {
846
- // Reopen camera modal
847
- this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image);
848
- },
849
- onAccept: async (acceptedImageBlob) => {
850
- await this.processAcceptedImage(acceptedImageBlob, ww_access_token, ww_user_id, ww_product_image, document.querySelector(this.config.gallerySelector));
851
- },
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();
852
477
  };
853
- showReviewModal(imageBlob, callbacks);
478
+ modal.appendChild(iframe);
479
+ modal.appendChild(closeButton);
480
+ document.body.appendChild(modal);
854
481
  }
855
482
  /**
856
- * Processes the accepted image by calling the API
483
+ * Sets up listener for messages from the iframe
857
484
  * @private
858
485
  */
859
- async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_image, container) {
860
- try {
861
- console.log("[WeWear VTO] Processing accepted image...");
862
- // Show loading in the review modal first
863
- showModalLoading(container);
864
- // Store the API parameters for potential refresh
865
- this.lastApiParams = {
866
- imageBlob,
867
- ww_access_token,
868
- ww_user_id,
869
- ww_product_image,
870
- };
871
- // Call the API with the accepted image
872
- const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_image, imageBlob);
873
- // Remove modal loading and close modal
874
- removeModalLoading();
875
- removeElements(`.${CSS_CLASSES.MODAL}`);
876
- if (result === null || result === void 0 ? void 0 : result.imageUrl) {
877
- 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;
878
494
  }
879
- else if (result === null || result === void 0 ? void 0 : result.error) {
880
- 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;
881
505
  }
882
- }
883
- catch (error) {
884
- console.error("[WeWear VTO] Error processing accepted image:", error);
885
- // Remove loading on error
886
- removeModalLoading();
887
- }
506
+ };
507
+ window.addEventListener("message", this.iframeMessageListener);
888
508
  }
889
- /**
890
- * Recalls the virtual try-on API with the same parameters
891
- * @private
892
- */
893
- async refreshVirtualTryOn() {
894
- if (!this.lastApiParams) {
895
- 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.");
896
515
  return;
897
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
+ }
898
526
  try {
899
- console.log("[WeWear VTO] Refreshing virtual try-on with previous parameters...");
900
- // Show loading in the product area
901
- const container = document.querySelector(this.config.gallerySelector);
902
- if (container instanceof HTMLElement) {
903
- 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
+ }
904
548
  }
905
- // Call the API with the stored parameters
906
- 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);
907
- if (result === null || result === void 0 ? void 0 : result.imageUrl) {
908
- this.virtualTryOnImageUrl = result.imageUrl;
909
- // Find the gallery container and update the image
910
- const container = document.querySelector(this.config.gallerySelector);
911
- if (container instanceof HTMLElement) {
912
- removeProductLoading(container);
913
- this.showVirtualTryOnImage(container);
914
- 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;
915
557
  }
916
558
  }
917
- else if (result === null || result === void 0 ? void 0 : result.error) {
918
- const container = document.querySelector(this.config.gallerySelector);
559
+ if (status.status === "FAILED") {
560
+ console.error("[WeWear VTO] VTO process failed:", status.message);
919
561
  if (container instanceof HTMLElement) {
920
- removeProductLoading(container);
921
- showAlert(container, result.error.message, result.error.type);
562
+ showAlert(container, status.message, "error");
922
563
  }
923
564
  }
924
565
  }
925
566
  catch (error) {
926
- console.error("[WeWear VTO] Error refreshing virtual try-on:", error);
927
- // Remove loading on error
928
- 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 {
929
573
  if (container instanceof HTMLElement) {
930
574
  removeProductLoading(container);
931
575
  }
576
+ removeElements(`.${CSS_CLASSES.MODAL}`);
932
577
  }
933
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
+ }
934
615
  /**
935
616
  * Replaces the product image in the gallery with the virtual try-on result
936
617
  * @private
@@ -950,8 +631,9 @@ class VirtualTryOnWidget {
950
631
  container.setAttribute("data-ww-original-content", container.innerHTML);
951
632
  }
952
633
  // Capture original image dimensions before replacement to prevent layout shift
953
- const originalImg = container.querySelector('img');
954
- if (originalImg && !container.hasAttribute("data-ww-original-dimensions")) {
634
+ const originalImg = container.querySelector("img");
635
+ if (originalImg &&
636
+ !container.hasAttribute("data-ww-original-dimensions")) {
955
637
  const computedStyle = window.getComputedStyle(originalImg);
956
638
  const rect = originalImg.getBoundingClientRect();
957
639
  container.setAttribute("data-ww-original-width", computedStyle.width);
@@ -992,6 +674,7 @@ class VirtualTryOnWidget {
992
674
  else {
993
675
  this.showVirtualTryOnImage(container);
994
676
  }
677
+ removeProductLoading(container);
995
678
  this.updateButtonContainer(container);
996
679
  }, this.isShowingVirtualTryOn);
997
680
  container.appendChild(buttonContainer);
@@ -1005,44 +688,46 @@ class VirtualTryOnWidget {
1005
688
  console.warn("[WeWear VTO] No virtual try-on image URL available");
1006
689
  return;
1007
690
  }
1008
- // Clear existing content except buttons
1009
- const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
1010
- 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
+ });
1011
697
  // Get stored original dimensions to prevent layout shift
1012
- const originalWidth = container.getAttribute("data-ww-original-width") || '';
1013
- const originalHeight = container.getAttribute("data-ww-original-height") || '';
1014
- const originalRectWidth = container.getAttribute("data-ww-original-rect-width") || '';
1015
- 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") || "";
1016
702
  const image = document.createElement("img");
1017
703
  image.src = this.virtualTryOnImageUrl;
1018
704
  image.alt = "Virtual Try-On Result";
1019
705
  // Use original dimensions to prevent layout shift
1020
- let widthStyle = '100%';
1021
- let heightStyle = '100%';
706
+ let widthStyle = "100%";
707
+ let heightStyle = "100%";
1022
708
  // Prefer computed style dimensions, fallback to bounding rect, then container fill
1023
- if (originalWidth && originalWidth !== 'auto' && originalWidth !== '0px') {
709
+ if (originalWidth && originalWidth !== "auto" && originalWidth !== "0px") {
1024
710
  widthStyle = originalWidth;
1025
711
  }
1026
- else if (originalRectWidth && originalRectWidth !== '0') {
712
+ else if (originalRectWidth && originalRectWidth !== "0") {
1027
713
  widthStyle = `${originalRectWidth}px`;
1028
714
  }
1029
- if (originalHeight && originalHeight !== 'auto' && originalHeight !== '0px') {
715
+ if (originalHeight &&
716
+ originalHeight !== "auto" &&
717
+ originalHeight !== "0px") {
1030
718
  heightStyle = originalHeight;
1031
719
  }
1032
- else if (originalRectHeight && originalRectHeight !== '0') {
720
+ else if (originalRectHeight && originalRectHeight !== "0") {
1033
721
  heightStyle = `${originalRectHeight}px`;
1034
722
  }
1035
- image.style.cssText = `
1036
- width: ${widthStyle};
1037
- height: ${heightStyle};
1038
- object-fit: cover;
1039
- border-radius: 8px;
723
+ image.style.cssText = `
724
+ width: ${widthStyle};
725
+ height: ${heightStyle};
726
+ object-fit: cover;
727
+ border-radius: 8px;
1040
728
  `;
1041
- container.appendChild(image);
1042
- // Re-add buttons
1043
- existingButtons.forEach((btn) => {
1044
- container.appendChild(btn);
1045
- });
729
+ // Prepend the image to ensure buttons are rendered on top
730
+ container.prepend(image);
1046
731
  this.isShowingVirtualTryOn = true;
1047
732
  }
1048
733
  /**
@@ -1050,16 +735,18 @@ class VirtualTryOnWidget {
1050
735
  * @private
1051
736
  */
1052
737
  showOriginalImage(container) {
1053
- const originalContent = container.getAttribute("data-ww-original-content");
1054
- if (originalContent) {
1055
- // Store existing buttons
1056
- const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
1057
- // Restore original content
1058
- container.innerHTML = originalContent;
1059
- // Re-add buttons
1060
- existingButtons.forEach((btn) => {
1061
- 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
+ }
1062
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));
1063
750
  }
1064
751
  this.isShowingVirtualTryOn = false;
1065
752
  }
@@ -1078,10 +765,20 @@ class VirtualTryOnWidget {
1078
765
  this.cookieCheckInterval = null;
1079
766
  }
1080
767
  this.cameraButton = null;
768
+ // Remove message listener
769
+ if (this.iframeMessageListener) {
770
+ window.removeEventListener("message", this.iframeMessageListener);
771
+ this.iframeMessageListener = null;
772
+ }
1081
773
  // Reset state
1082
774
  this.virtualTryOnImageUrl = null;
1083
775
  this.isShowingVirtualTryOn = false;
1084
- this.lastApiParams = null;
776
+ this.lastModelImage = null;
777
+ this.originalProductImageUrl = null;
778
+ if (this.previewBadge) {
779
+ this.previewBadge.remove();
780
+ this.previewBadge = null;
781
+ }
1085
782
  console.log("[WeWear VTO] Widget destroyed successfully");
1086
783
  }
1087
784
  catch (error) {
@@ -1123,18 +820,6 @@ function initVirtualTryOn(config) {
1123
820
  console.error("[WeWear VTO] Initialization error:", error);
1124
821
  }
1125
822
  }
1126
- function destroyVirtualTryOn() {
1127
- try {
1128
- if (widgetInstance) {
1129
- widgetInstance.destroy();
1130
- widgetInstance = null;
1131
- console.log("[WeWear VTO] Widget instance destroyed");
1132
- }
1133
- }
1134
- catch (error) {
1135
- console.error("[WeWear VTO] Error destroying widget:", error);
1136
- }
1137
- }
1138
823
  function getWidgetInstance() {
1139
824
  return widgetInstance;
1140
825
  }
@@ -1142,5 +827,5 @@ if (typeof window !== "undefined") {
1142
827
  initVirtualTryOn();
1143
828
  }
1144
829
 
1145
- export { VirtualTryOnWidget, destroyVirtualTryOn, getWidgetInstance, initVirtualTryOn };
830
+ export { VirtualTryOnWidget, getWidgetInstance, initVirtualTryOn };
1146
831
  //# sourceMappingURL=index.esm.js.map