@wewear/virtual-try-on 1.1.0 → 1.3.0

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,12 +4,31 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WeWearVirtualTryOn = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- /**
8
- * WeWear Virtual Try-On Widget Implementation
9
- *
10
- * Core widget class that handles virtual try-on functionality integration.
11
- * Manages button creation, API communication, and modal display.
12
- */
7
+ async function callVirtualTryOnApi(baseUrl, ww_access_token, ww_user_id, ww_product_id, ww_image) {
8
+ try {
9
+ const formData = new FormData();
10
+ formData.append("ww_user_id", ww_user_id);
11
+ formData.append("ww_product_id", ww_product_id);
12
+ formData.append("ww_image", ww_image, "captured-image.jpg");
13
+ formData.append("ww_access_token", ww_access_token);
14
+ const response = await fetch(`${baseUrl}/api/virtual-try-on`, {
15
+ method: "POST",
16
+ body: formData,
17
+ });
18
+ if (!response.ok) {
19
+ console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
20
+ return null;
21
+ }
22
+ const result = await response.json();
23
+ console.log("[WeWear VTO] API response:", result);
24
+ return result;
25
+ }
26
+ catch (error) {
27
+ console.error("[WeWear VTO] API call failed:", error);
28
+ return null;
29
+ }
30
+ }
31
+
13
32
  /** Default configuration constants */
14
33
  const DEFAULT_CONFIG = {
15
34
  BASE_URL: "https://virtual-try-on-widget.vercel.app",
@@ -29,335 +48,583 @@
29
48
  BUTTON: 10,
30
49
  MODAL: 99999,
31
50
  };
32
- class VirtualTryOnWidget {
33
- constructor(config = {}) {
34
- this.config = {
35
- baseUrl: config.baseUrl || DEFAULT_CONFIG.BASE_URL,
36
- productPageSelector: config.productPageSelector || DEFAULT_CONFIG.PRODUCT_PAGE_SELECTOR,
37
- gallerySelector: config.gallerySelector || DEFAULT_CONFIG.GALLERY_SELECTOR,
38
- skuSelector: config.skuSelector || DEFAULT_CONFIG.SKU_SELECTOR,
39
- buttonPosition: config.buttonPosition || DEFAULT_CONFIG.BUTTON_POSITION,
51
+ /** Camera constraints for optimal capture */
52
+ const CAMERA_CONSTRAINTS = {
53
+ video: {
54
+ width: { ideal: 1280 },
55
+ height: { ideal: 720 },
56
+ facingMode: "user", // Front-facing camera
57
+ },
58
+ audio: false,
59
+ };
60
+ /** Image capture settings */
61
+ const IMAGE_SETTINGS = {
62
+ FORMAT: "image/jpeg",
63
+ QUALITY: 0.8,
64
+ };
65
+
66
+ async function startCamera(video, loadingIndicator, captureButton) {
67
+ console.log("[WeWear VTO] Starting camera...");
68
+ try {
69
+ console.log("[WeWear VTO] Requesting camera access...");
70
+ const stream = await navigator.mediaDevices.getUserMedia(CAMERA_CONSTRAINTS);
71
+ video.srcObject = stream;
72
+ video.onloadedmetadata = () => {
73
+ console.log("[WeWear VTO] Camera stream loaded successfully");
74
+ loadingIndicator.style.display = "none";
75
+ captureButton.style.display = "flex";
40
76
  };
41
77
  }
42
- /**
43
- * Retrieves a cookie value by name
44
- * @param name - Cookie name to retrieve
45
- * @returns Cookie value or null if not found
46
- */
47
- getCookie(name) {
48
- var _a;
49
- if (typeof document === "undefined")
50
- return null;
51
- const value = `; ${document.cookie}`;
52
- const parts = value.split(`; ${name}=`);
53
- if (parts.length === 2) {
54
- const part = parts.pop();
55
- return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
56
- }
57
- return null;
78
+ catch (error) {
79
+ console.error("[WeWear VTO] Camera access error:", error);
80
+ loadingIndicator.innerHTML =
81
+ "Camera access denied. Please allow camera permissions.";
58
82
  }
59
- /**
60
- * Makes API call to virtual try-on service
61
- * @param ww_access_token - Optional authentication token
62
- * @param ww_user_id - User identifier
63
- /**
64
- * Creates the virtual try-on button element
65
- * @param onClick - Click handler function
66
- * @returns Button container element
67
- */
68
- createButton(onClick) {
69
- const container = document.createElement("div");
70
- container.className = CSS_CLASSES.BUTTON_CONTAINER;
71
- const positionStyles = this.getPositionStyles();
72
- container.style.cssText = `
73
- position: absolute;
74
- ${positionStyles}
75
- display: flex;
76
- border-radius: 50%;
77
- background: #000000;
78
- padding: 4px;
79
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
80
- z-index: ${Z_INDEX.BUTTON};
81
- transition: transform 0.2s ease, box-shadow 0.2s ease;
82
- `;
83
- const button = document.createElement("button");
84
- button.type = "button";
85
- button.className = CSS_CLASSES.BUTTON;
86
- button.setAttribute("aria-label", "Virtual Try-On");
87
- button.setAttribute("title", "Try this product virtually");
88
- button.style.cssText = `
89
- display: flex;
90
- width: 36px;
91
- height: 36px;
92
- cursor: pointer;
93
- align-items: center;
94
- justify-content: center;
95
- border-radius: 50%;
96
- padding: 10px;
97
- border: none;
98
- background: transparent;
99
- color: #ffffff;
100
- `;
101
- // Add hover effects
102
- container.addEventListener("mouseenter", () => {
103
- container.style.transform = "scale(1.05)";
104
- container.style.boxShadow = "0 30px 60px -12px rgba(0, 0, 0, 0.35)";
105
- });
106
- container.addEventListener("mouseleave", () => {
107
- container.style.transform = "scale(1)";
108
- container.style.boxShadow = "0 25px 50px -12px rgba(0, 0, 0, 0.25)";
83
+ }
84
+ function stopCamera(video) {
85
+ const stream = video.srcObject;
86
+ if (stream) {
87
+ const tracks = stream.getTracks();
88
+ tracks.forEach((track) => {
89
+ track.stop();
109
90
  });
110
- button.innerHTML = `
111
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
112
- <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
113
- <circle cx="12" cy="13" r="3"></circle>
114
- </svg>
115
- `;
116
- button.onclick = onClick;
117
- container.appendChild(button);
118
- return container;
91
+ video.srcObject = null;
119
92
  }
120
- /**
121
- * Gets CSS position styles based on button position configuration
122
- * @returns CSS position string
123
- */
124
- getPositionStyles() {
125
- switch (this.config.buttonPosition) {
126
- case "bottom-left":
127
- return "left: 20px; bottom: 20px;";
128
- case "top-right":
129
- return "right: 20px; top: 20px;";
130
- case "top-left":
131
- return "left: 20px; top: 20px;";
132
- default:
133
- return "right: 20px; bottom: 20px;";
134
- }
93
+ }
94
+ async function captureImageFromVideo(video, canvas) {
95
+ // Set canvas dimensions to match video
96
+ canvas.width = video.videoWidth;
97
+ canvas.height = video.videoHeight;
98
+ // Draw video frame to canvas
99
+ const ctx = canvas.getContext("2d");
100
+ if (!ctx) {
101
+ throw new Error("Could not get canvas context");
135
102
  }
136
- /**
137
- * Displays the virtual try-on result in a modal
138
- * @param imageUrl - URL of the virtual try-on image
139
- */
140
- showImageModal(imageUrl) {
141
- // Remove any existing modals first
142
- const existingModals = document.querySelectorAll(`.${CSS_CLASSES.MODAL}`);
143
- existingModals.forEach((modal) => {
144
- modal.remove();
145
- });
146
- // Create modal container
147
- const modal = document.createElement("div");
148
- modal.className = CSS_CLASSES.MODAL;
149
- modal.style.cssText = `
150
- position: fixed;
151
- top: 0;
152
- left: 0;
153
- width: 100%;
154
- height: 100%;
155
- background-color: rgba(0, 0, 0, 0.9);
156
- display: flex;
157
- align-items: center;
158
- justify-content: center;
159
- z-index: ${Z_INDEX.MODAL};
160
- padding: 20px;
161
- box-sizing: border-box;
162
- `;
163
- // Create image container
164
- const imageContainer = document.createElement("div");
165
- imageContainer.style.cssText = `
166
- position: relative;
167
- max-width: 90%;
168
- max-height: 90%;
169
- background-color: white;
170
- border-radius: 8px;
171
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
172
- overflow: hidden;
173
- `;
174
- // Create image element
175
- const image = document.createElement("img");
176
- image.src = imageUrl;
177
- image.alt = "Virtual Try-On Result";
178
- image.style.cssText = `
179
- width: 100%;
180
- height: 100%;
181
- object-fit: contain;
182
- display: block;
183
- `;
184
- // Create close button
185
- const closeButton = document.createElement("button");
186
- closeButton.innerHTML = "×";
187
- closeButton.style.cssText = `
188
- position: absolute;
189
- top: 10px;
190
- right: 10px;
191
- width: 30px;
192
- height: 30px;
193
- border: none;
194
- background-color: rgba(0, 0, 0, 0.7);
195
- color: white;
196
- font-size: 20px;
197
- font-weight: bold;
198
- cursor: pointer;
199
- border-radius: 50%;
200
- display: flex;
201
- align-items: center;
202
- justify-content: center;
203
- `;
204
- closeButton.onclick = () => {
205
- modal.remove();
206
- };
207
- // Close on backdrop click
208
- modal.onclick = (e) => {
209
- if (e.target === modal) {
210
- modal.remove();
103
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
104
+ // Convert canvas to blob
105
+ return new Promise((resolve, reject) => {
106
+ canvas.toBlob((blob) => {
107
+ if (blob) {
108
+ resolve(blob);
211
109
  }
212
- };
213
- // Close on Escape key
214
- const handleEscape = (e) => {
215
- if (e.key === "Escape") {
216
- modal.remove();
217
- document.removeEventListener("keydown", handleEscape);
110
+ else {
111
+ reject(new Error("Failed to create blob from canvas"));
218
112
  }
219
- };
220
- document.addEventListener("keydown", handleEscape);
221
- imageContainer.appendChild(image);
222
- imageContainer.appendChild(closeButton);
223
- modal.appendChild(imageContainer);
224
- document.body.appendChild(modal);
113
+ }, IMAGE_SETTINGS.FORMAT, IMAGE_SETTINGS.QUALITY);
114
+ });
115
+ }
116
+
117
+ function getCookie(name) {
118
+ var _a;
119
+ if (typeof document === "undefined")
120
+ return null;
121
+ const value = `; ${document.cookie}`;
122
+ const parts = value.split(`; ${name}=`);
123
+ if (parts.length === 2) {
124
+ const part = parts.pop();
125
+ return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
225
126
  }
226
- /**
227
- * Shows the camera capture modal
228
- * @param ww_access_token - Access token for API
229
- * @param ww_user_id - User identifier
230
- * @param ww_product_id - Product identifier
231
- */
232
- showCameraModal(ww_access_token, ww_user_id, ww_product_id) {
233
- console.log("[WeWear VTO] Opening camera modal...");
234
- // Remove any existing modals first
235
- const existingModals = document.querySelectorAll(`.${CSS_CLASSES.MODAL}`);
236
- existingModals.forEach((modal) => {
237
- modal.remove();
238
- });
239
- // Create modal container
240
- const modal = document.createElement("div");
241
- modal.className = CSS_CLASSES.MODAL;
242
- modal.style.cssText = `
243
- position: fixed;
244
- top: 0;
245
- left: 0;
246
- width: 100%;
247
- height: 100%;
248
- background-color: rgba(0, 0, 0, 0.95);
249
- display: flex;
250
- flex-direction: column;
251
- align-items: center;
252
- justify-content: center;
253
- z-index: ${Z_INDEX.MODAL};
254
- padding: 20px;
255
- box-sizing: border-box;
256
- `;
257
- // Create camera container
258
- const cameraContainer = document.createElement("div");
259
- cameraContainer.style.cssText = `
260
- position: relative;
261
- width: 100%;
262
- max-width: 500px;
263
- height: 70vh;
264
- background-color: #000;
265
- border-radius: 12px;
266
- overflow: hidden;
267
- display: flex;
268
- flex-direction: column;
269
- align-items: center;
270
- justify-content: center;
271
- `;
272
- // Create video element
273
- const video = document.createElement("video");
274
- video.autoplay = true;
275
- video.playsInline = true;
276
- video.muted = true;
277
- video.style.cssText = `
278
- width: 100%;
279
- height: 100%;
280
- object-fit: cover;
281
- border-radius: 12px;
282
- `;
283
- // Create canvas for capturing
284
- const canvas = document.createElement("canvas");
285
- canvas.style.display = "none";
286
- // Create capture button
287
- const captureButton = document.createElement("button");
288
- captureButton.innerHTML = `
289
- <svg width="24" height="24" viewBox="0 0 24 24" fill="white">
290
- <circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" fill="none"/>
291
- <circle cx="12" cy="12" r="6" fill="white"/>
127
+ return null;
128
+ }
129
+ function getPositionStyles(position) {
130
+ switch (position) {
131
+ case "bottom-left":
132
+ return "left: 20px; bottom: 20px;";
133
+ case "top-right":
134
+ return "right: 20px; top: 20px;";
135
+ case "top-left":
136
+ return "left: 20px; top: 20px;";
137
+ default:
138
+ return "right: 20px; bottom: 20px;";
139
+ }
140
+ }
141
+ function removeElements(selector) {
142
+ const elements = document.querySelectorAll(selector);
143
+ elements.forEach((element) => {
144
+ element.remove();
145
+ });
146
+ }
147
+
148
+ function createButtonContainer(buttonPosition, hasVirtualTryOn = false, onCameraClick, onRefreshClick, onToggleClick, isShowingVirtualTryOn = false) {
149
+ const container = document.createElement("div");
150
+ container.className = `${CSS_CLASSES.BUTTON_CONTAINER} ww-button-group`;
151
+ const positionStyles = getPositionStyles(buttonPosition);
152
+ container.style.cssText = `
153
+ position: absolute;
154
+ ${positionStyles}
155
+ display: flex;
156
+ border-radius: 9999px;
157
+ background: white;
158
+ padding: 4px;
159
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
160
+ z-index: ${Z_INDEX.BUTTON};
161
+ `;
162
+ // Camera button
163
+ const cameraButton = document.createElement("button");
164
+ cameraButton.type = "button";
165
+ cameraButton.className = `${CSS_CLASSES.BUTTON} ww-camera-btn`;
166
+ cameraButton.setAttribute("aria-label", "Virtual Try-On");
167
+ cameraButton.style.cssText = `
168
+ display: flex;
169
+ width: 36px;
170
+ height: 36px;
171
+ cursor: pointer;
172
+ align-items: center;
173
+ justify-content: center;
174
+ border-radius: 9999px;
175
+ padding: 8px;
176
+ border: none;
177
+ background: transparent;
178
+ color: currentColor;
179
+ `;
180
+ cameraButton.innerHTML = `
181
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-camera">
182
+ <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
183
+ <circle cx="12" cy="13" r="3"></circle>
184
+ </svg>
185
+ `;
186
+ cameraButton.onclick = onCameraClick;
187
+ container.appendChild(cameraButton);
188
+ // Add refresh and toggle buttons if we have a virtual try-on
189
+ if (hasVirtualTryOn) {
190
+ // Refresh button - calls API again
191
+ if (onRefreshClick) {
192
+ const refreshButton = document.createElement("button");
193
+ refreshButton.type = "button";
194
+ refreshButton.className = "ww-refresh-btn";
195
+ refreshButton.setAttribute("aria-label", "Refresh virtual try-on");
196
+ refreshButton.style.cssText = `
197
+ display: flex;
198
+ width: 36px;
199
+ height: 36px;
200
+ cursor: pointer;
201
+ align-items: center;
202
+ justify-content: center;
203
+ border-radius: 9999px;
204
+ padding: 8px;
205
+ border: none;
206
+ background: transparent;
207
+ color: currentColor;
208
+ `;
209
+ refreshButton.innerHTML = `
210
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-ccw">
211
+ <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
212
+ <path d="M3 3v5h5"></path>
213
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
214
+ <path d="M16 16h5v5"></path>
215
+ </svg>
216
+ `;
217
+ refreshButton.onclick = onRefreshClick;
218
+ container.appendChild(refreshButton);
219
+ }
220
+ // Toggle button (scan-face) - switches between original and virtual try-on
221
+ if (onToggleClick) {
222
+ const toggleButton = document.createElement("button");
223
+ toggleButton.type = "button";
224
+ toggleButton.className = "ww-toggle-btn";
225
+ toggleButton.setAttribute("aria-label", isShowingVirtualTryOn ? "Show Original Image" : "Show Virtual Try-On");
226
+ toggleButton.style.cssText = `
227
+ display: flex;
228
+ width: 36px;
229
+ height: 36px;
230
+ cursor: pointer;
231
+ align-items: center;
232
+ justify-content: center;
233
+ border-radius: 9999px;
234
+ padding: 8px;
235
+ border: none;
236
+ background: ${isShowingVirtualTryOn ? "rgba(0, 0, 0)" : "none"};
237
+ color: ${isShowingVirtualTryOn ? "white" : "currentColor"};
238
+ `;
239
+ toggleButton.innerHTML = `
240
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-face">
241
+ <path d="M3 7V5a2 2 0 0 1 2-2h2"></path>
242
+ <path d="M17 3h2a2 2 0 0 1 2 2v2"></path>
243
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2"></path>
244
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2"></path>
245
+ <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
246
+ <path d="M9 9h.01"></path>
247
+ <path d="M15 9h.01"></path>
248
+ </svg>
249
+ `;
250
+ toggleButton.onclick = onToggleClick;
251
+ container.appendChild(toggleButton);
252
+ }
253
+ }
254
+ return container;
255
+ }
256
+ function createButton(buttonPosition, onClick) {
257
+ return createButtonContainer(buttonPosition, false, onClick);
258
+ }
259
+
260
+ function showCameraModal(callbacks) {
261
+ console.log("[WeWear VTO] Opening camera modal...");
262
+ // Remove any existing modals first
263
+ removeElements(`.${CSS_CLASSES.MODAL}`);
264
+ // Create modal container
265
+ const modal = document.createElement("div");
266
+ modal.className = CSS_CLASSES.MODAL;
267
+ modal.style.cssText = `
268
+ position: fixed;
269
+ top: 0;
270
+ left: 0;
271
+ width: 100%;
272
+ height: 100%;
273
+ background-color: rgba(0, 0, 0, 0.95);
274
+ display: flex;
275
+ flex-direction: column;
276
+ align-items: center;
277
+ justify-content: center;
278
+ z-index: ${Z_INDEX.MODAL};
279
+ padding: 20px;
280
+ box-sizing: border-box;
281
+ `;
282
+ // Create camera container
283
+ const cameraContainer = document.createElement("div");
284
+ cameraContainer.style.cssText = `
285
+ position: relative;
286
+ width: 100%;
287
+ max-width: 500px;
288
+ height: 70vh;
289
+ background-color: #000;
290
+ border-radius: 12px;
291
+ overflow: hidden;
292
+ display: flex;
293
+ flex-direction: column;
294
+ align-items: center;
295
+ justify-content: center;
296
+ `;
297
+ // Create video element
298
+ const video = document.createElement("video");
299
+ video.autoplay = true;
300
+ video.playsInline = true;
301
+ video.muted = true;
302
+ video.style.cssText = `
303
+ width: 100%;
304
+ height: 100%;
305
+ object-fit: cover;
306
+ border-radius: 12px;
307
+ `;
308
+ // Create canvas for capturing
309
+ const canvas = document.createElement("canvas");
310
+ canvas.style.display = "none";
311
+ // Create capture button
312
+ const captureButton = document.createElement("button");
313
+ captureButton.innerHTML = "";
314
+ captureButton.style.cssText = `
315
+ position: absolute;
316
+ bottom: 20px;
317
+ left: 50%;
318
+ transform: translateX(-50%);
319
+ width: 60px;
320
+ height: 60px;
321
+ border: 3px solid white;
322
+ background-color: rgba(0, 0, 0, 0.7);
323
+ border-radius: 50%;
324
+ cursor: pointer;
325
+ display: none;
326
+ align-items: center;
327
+ justify-content: center;
328
+ transition: all 0.2s ease;
329
+ `;
330
+ // Create close button
331
+ const closeButton = document.createElement("button");
332
+ closeButton.innerHTML = "×";
333
+ closeButton.style.cssText = `
334
+ position: absolute;
335
+ top: 15px;
336
+ right: 15px;
337
+ width: 40px;
338
+ height: 40px;
339
+ border: none;
340
+ background-color: rgba(0, 0, 0, 0.7);
341
+ color: white;
342
+ font-size: 24px;
343
+ font-weight: bold;
344
+ cursor: pointer;
345
+ border-radius: 50%;
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: center;
349
+ `;
350
+ // Create loading indicator
351
+ const loadingIndicator = document.createElement("div");
352
+ loadingIndicator.innerHTML = "Initializing camera...";
353
+ loadingIndicator.style.cssText = `
354
+ color: white;
355
+ font-size: 16px;
356
+ position: absolute;
357
+ top: 50%;
358
+ left: 50%;
359
+ transform: translate(-50%, -50%);
360
+ `;
361
+ // Add event listeners
362
+ closeButton.onclick = () => {
363
+ stopCamera(video);
364
+ modal.remove();
365
+ };
366
+ captureButton.onclick = async () => {
367
+ await callbacks.onCapture(video, canvas);
368
+ };
369
+ // Assemble the camera modal
370
+ cameraContainer.appendChild(video);
371
+ cameraContainer.appendChild(canvas);
372
+ cameraContainer.appendChild(captureButton);
373
+ cameraContainer.appendChild(closeButton);
374
+ cameraContainer.appendChild(loadingIndicator);
375
+ modal.appendChild(cameraContainer);
376
+ document.body.appendChild(modal);
377
+ console.log("[WeWear VTO] Camera modal added to DOM");
378
+ // Start camera
379
+ startCamera(video, loadingIndicator, captureButton);
380
+ }
381
+
382
+ /**
383
+ * Creates a loading overlay that can be shown in different containers
384
+ */
385
+ function createLoadingOverlay(text = "Processing...") {
386
+ const overlay = document.createElement("div");
387
+ overlay.className = "ww-loading-overlay";
388
+ overlay.style.cssText = `
389
+ position: absolute;
390
+ top: 0;
391
+ left: 0;
392
+ width: 100%;
393
+ height: 100%;
394
+ background-color: rgba(0, 0, 0, 0.8);
395
+ display: flex;
396
+ flex-direction: column;
397
+ align-items: center;
398
+ justify-content: center;
399
+ z-index: ${Z_INDEX.MODAL + 1};
400
+ border-radius: inherit;
401
+ `;
402
+ // Add CSS animation to the document if not already added
403
+ if (!document.getElementById("ww-loading-animation")) {
404
+ const style = document.createElement("style");
405
+ style.id = "ww-loading-animation";
406
+ style.textContent = `
407
+ @keyframes ww-pulse {
408
+ 0%, 100% { opacity: 0.7; transform: scale(1); }
409
+ 50% { opacity: 1; transform: scale(1.05); }
410
+ }
411
+ .ww-loading-overlay .ww-logo {
412
+ animation: ww-pulse 2s ease-in-out infinite;
413
+ }
414
+ `;
415
+ document.head.appendChild(style);
416
+ }
417
+ overlay.innerHTML = `
418
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 24px; color: white;">
419
+ <svg class="ww-logo" width="80" height="50" viewBox="0 0 214 135" fill="none" xmlns="http://www.w3.org/2000/svg">
420
+ <g clip-path="url(#clip0_3624_12755)">
421
+ <path d="M102.906 74.8679C102.906 77.9717 101.574 80.7453 98.9104 83.1887C96.6871 85.1918 93.9025 86.6997 90.5566 87.7123C87.695 88.5708 84.6462 89 81.4104 89C73.8821 89 68.0047 87.0189 63.7783 83.0566C59.5519 87.0189 53.6855 89 46.1792 89C42.9434 89 39.9057 88.5708 37.066 87.7123C33.7201 86.6997 30.9245 85.1918 28.6792 83.1887C26.0157 80.7453 24.684 77.9717 24.684 74.8679V41.6509H32.3774V74.8679C32.3774 76.2547 33.489 77.5645 35.7123 78.7972C37.3632 79.7217 39.0692 80.3711 40.8302 80.7453C42.5252 81.1195 44.3082 81.3066 46.1792 81.3066C48.0063 81.3066 49.7673 81.1195 51.4623 80.7453C53.2453 80.3711 54.9623 79.7217 56.6132 78.7972C58.8585 77.5645 59.9811 76.2547 59.9811 74.8679V41.6509H67.6085V74.8679C67.6085 76.2547 68.7311 77.5645 70.9764 78.7972C72.6274 79.7217 74.3443 80.3711 76.1274 80.7453C77.8223 81.1195 79.5833 81.3066 81.4104 81.3066C83.2814 81.3066 85.0755 81.1195 86.7925 80.7453C88.5314 80.3711 90.2264 79.7217 91.8774 78.7972C94.1006 77.5645 95.2123 76.2547 95.2123 74.8679V41.6509H102.906V74.8679ZM189.283 74.8679C189.283 77.9717 187.951 80.7453 185.288 83.1887C183.064 85.1918 180.28 86.6997 176.934 87.7123C174.072 88.5708 171.024 89 167.788 89C160.259 89 154.382 87.0189 150.156 83.0566C145.929 87.0189 140.063 89 132.557 89C129.321 89 126.283 88.5708 123.443 87.7123C120.097 86.6997 117.302 85.1918 115.057 83.1887C112.393 80.7453 111.061 77.9717 111.061 74.8679V41.6509H118.755V74.8679C118.755 76.2547 119.866 77.5645 122.09 78.7972C123.741 79.7217 125.447 80.3711 127.208 80.7453C128.903 81.1195 130.686 81.3066 132.557 81.3066C134.384 81.3066 136.145 81.1195 137.84 80.7453C139.623 80.3711 141.34 79.7217 142.991 78.7972C145.236 77.5645 146.358 76.2547 146.358 74.8679V41.6509H153.986V74.8679C153.986 76.2547 155.108 77.5645 157.354 78.7972C159.005 79.7217 160.722 80.3711 162.505 80.7453C164.2 81.1195 165.961 81.3066 167.788 81.3066C169.659 81.3066 171.453 81.1195 173.17 80.7453C174.909 80.3711 176.604 79.7217 178.255 78.7972C180.478 77.5645 181.59 76.2547 181.59 74.8679V41.6509H189.283V74.8679Z" fill="white"/>
422
+ </g>
423
+ <defs>
424
+ <clipPath id="clip0_3624_12755">
425
+ <rect width="214" height="135" fill="white"/>
426
+ </clipPath>
427
+ </defs>
292
428
  </svg>
293
- `;
294
- captureButton.style.cssText = `
295
- position: absolute;
296
- bottom: 20px;
297
- left: 50%;
298
- transform: translateX(-50%);
299
- width: 60px;
300
- height: 60px;
301
- border: 3px solid white;
302
- background-color: transparent;
303
- border-radius: 50%;
304
- cursor: pointer;
305
- display: none;
306
- align-items: center;
307
- justify-content: center;
308
- transition: all 0.2s ease;
309
- `;
310
- // Create close button
311
- const closeButton = document.createElement("button");
312
- closeButton.innerHTML = "×";
313
- closeButton.style.cssText = `
314
- position: absolute;
315
- top: 15px;
316
- right: 15px;
317
- width: 40px;
318
- height: 40px;
319
- border: none;
320
- background-color: rgba(0, 0, 0, 0.7);
321
- color: white;
322
- font-size: 24px;
323
- font-weight: bold;
324
- cursor: pointer;
325
- border-radius: 50%;
326
- display: flex;
327
- align-items: center;
328
- justify-content: center;
329
- `;
330
- // Create loading indicator
331
- const loadingIndicator = document.createElement("div");
332
- loadingIndicator.innerHTML = "Initializing camera...";
333
- loadingIndicator.style.cssText = `
334
- color: white;
335
- font-size: 16px;
336
- position: absolute;
337
- top: 50%;
338
- left: 50%;
339
- transform: translate(-50%, -50%);
340
- `;
341
- // Add event listeners
342
- closeButton.onclick = () => {
343
- this.stopCamera(video);
344
- modal.remove();
345
- };
346
- captureButton.onclick = () => {
347
- this.captureImage(video, canvas, ww_access_token, ww_user_id, ww_product_id, modal);
429
+ <div style="font-size: 16px; font-weight: 500; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">${text}</div>
430
+ </div>
431
+ `;
432
+ return overlay;
433
+ }
434
+ /**
435
+ * Shows a loading overlay in a modal container
436
+ */
437
+ function showModalLoading(text = "Processing...") {
438
+ const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
439
+ if (!modal)
440
+ return;
441
+ // Remove any existing loading overlays
442
+ removeModalLoading();
443
+ const loadingOverlay = createLoadingOverlay(text);
444
+ modal.appendChild(loadingOverlay);
445
+ }
446
+ /**
447
+ * Removes loading overlay from modal
448
+ */
449
+ function removeModalLoading() {
450
+ const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
451
+ if (!modal)
452
+ return;
453
+ const existingOverlay = modal.querySelector(".ww-loading-overlay");
454
+ if (existingOverlay) {
455
+ existingOverlay.remove();
456
+ }
457
+ }
458
+ /**
459
+ * Shows a loading overlay in the product gallery container
460
+ */
461
+ function showProductLoading(container, text = "Generating virtual try-on...") {
462
+ // Remove any existing loading overlays
463
+ removeProductLoading(container);
464
+ // Make container relative if it's not already positioned
465
+ const computedStyle = window.getComputedStyle(container);
466
+ if (computedStyle.position === "static") {
467
+ container.style.position = "relative";
468
+ }
469
+ const loadingOverlay = createLoadingOverlay(text);
470
+ container.appendChild(loadingOverlay);
471
+ }
472
+ /**
473
+ * Removes loading overlay from product container
474
+ */
475
+ function removeProductLoading(container) {
476
+ const existingOverlay = container.querySelector(".ww-loading-overlay");
477
+ if (existingOverlay) {
478
+ existingOverlay.remove();
479
+ }
480
+ }
481
+
482
+ function showReviewModal(imageBlob, callbacks) {
483
+ console.log("[WeWear VTO] Opening review modal...");
484
+ // Remove any existing modals first
485
+ removeElements(`.${CSS_CLASSES.MODAL}`);
486
+ // Create image URL for preview
487
+ const imageUrl = URL.createObjectURL(imageBlob);
488
+ // Create modal container
489
+ const modal = document.createElement("div");
490
+ modal.className = CSS_CLASSES.MODAL;
491
+ modal.style.cssText = `
492
+ position: fixed;
493
+ top: 0;
494
+ left: 0;
495
+ width: 100%;
496
+ height: 100%;
497
+ background-color: rgba(0, 0, 0, 0.95);
498
+ display: flex;
499
+ flex-direction: column;
500
+ align-items: center;
501
+ justify-content: center;
502
+ z-index: ${Z_INDEX.MODAL};
503
+ padding: 20px;
504
+ box-sizing: border-box;
505
+ gap: 20px;
506
+ `;
507
+ // Create review container
508
+ const reviewContainer = document.createElement("div");
509
+ reviewContainer.style.cssText = `
510
+ position: relative;
511
+ width: 100%;
512
+ max-width: 500px;
513
+ height: 63vh;
514
+ background-color: #000;
515
+ border-radius: 12px;
516
+ overflow: hidden;
517
+ display: flex;
518
+ flex-direction: column;
519
+ align-items: center;
520
+ justify-content: center;
521
+ `;
522
+ // Create image element
523
+ const image = document.createElement("img");
524
+ image.src = imageUrl;
525
+ image.alt = "Review your photo";
526
+ image.style.cssText = `
527
+ width: 100%;
528
+ height: 100%;
529
+ object-fit: cover;
530
+ border-radius: 12px;
531
+ `;
532
+ // Create close button
533
+ const closeButton = document.createElement("button");
534
+ closeButton.innerHTML = "×";
535
+ closeButton.style.cssText = `
536
+ position: absolute;
537
+ top: 15px;
538
+ right: 15px;
539
+ width: 40px;
540
+ height: 40px;
541
+ border: none;
542
+ background-color: rgba(0, 0, 0, 0.7);
543
+ color: white;
544
+ font-size: 24px;
545
+ font-weight: bold;
546
+ cursor: pointer;
547
+ border-radius: 50%;
548
+ display: flex;
549
+ align-items: center;
550
+ justify-content: center;
551
+ `;
552
+ // Create button container
553
+ const buttonContainer = document.createElement("div");
554
+ buttonContainer.style.cssText = `
555
+ display: flex;
556
+ gap: 15px;
557
+ width: 100%;
558
+ max-width: 500px;
559
+ `;
560
+ // Create retake button
561
+ const retakeButton = document.createElement("button");
562
+ retakeButton.textContent = "Retake";
563
+ retakeButton.style.cssText = `
564
+ flex: 1;
565
+ padding: 12px 24px;
566
+ background-color: rgba(255, 255, 255, 0.9);
567
+ color: black;
568
+ border-radius: 8px;
569
+ border: none;
570
+ font-size: 16px;
571
+ font-weight: normal;
572
+ cursor: pointer;
573
+ `;
574
+ // Create use photo button
575
+ const usePhotoButton = document.createElement("button");
576
+ usePhotoButton.textContent = "Use Photo";
577
+ usePhotoButton.style.cssText = `
578
+ flex: 1;
579
+ padding: 12px 24px;
580
+ background-color: rgba(0, 0, 0, 0.7);
581
+ color: white;
582
+ border-radius: 8px;
583
+ border: none;
584
+ font-size: 16px;
585
+ font-weight: normal;
586
+ cursor: pointer;
587
+ `;
588
+ // Add event listeners
589
+ closeButton.onclick = () => {
590
+ URL.revokeObjectURL(imageUrl); // Clean up
591
+ modal.remove();
592
+ };
593
+ retakeButton.onclick = () => {
594
+ URL.revokeObjectURL(imageUrl); // Clean up
595
+ modal.remove();
596
+ callbacks.onRetake();
597
+ };
598
+ usePhotoButton.onclick = async () => {
599
+ URL.revokeObjectURL(imageUrl); // Clean up
600
+ modal.remove();
601
+ await callbacks.onAccept(imageBlob);
602
+ };
603
+ // Assemble the review modal
604
+ buttonContainer.appendChild(retakeButton);
605
+ buttonContainer.appendChild(usePhotoButton);
606
+ reviewContainer.appendChild(image);
607
+ reviewContainer.appendChild(closeButton);
608
+ modal.appendChild(reviewContainer);
609
+ modal.appendChild(buttonContainer);
610
+ document.body.appendChild(modal);
611
+ console.log("[WeWear VTO] Review modal added to DOM");
612
+ }
613
+
614
+ class VirtualTryOnWidget {
615
+ constructor(config = {}) {
616
+ this.virtualTryOnImageUrl = null;
617
+ this.isShowingVirtualTryOn = false;
618
+ this.lastApiParams = null;
619
+ this.config = {
620
+ baseUrl: config.baseUrl || DEFAULT_CONFIG.BASE_URL,
621
+ productPageSelector: config.productPageSelector || DEFAULT_CONFIG.PRODUCT_PAGE_SELECTOR,
622
+ gallerySelector: config.gallerySelector || DEFAULT_CONFIG.GALLERY_SELECTOR,
623
+ skuSelector: config.skuSelector || DEFAULT_CONFIG.SKU_SELECTOR,
624
+ buttonPosition: config.buttonPosition || DEFAULT_CONFIG.BUTTON_POSITION,
348
625
  };
349
- // Assemble the camera modal
350
- cameraContainer.appendChild(video);
351
- cameraContainer.appendChild(canvas);
352
- cameraContainer.appendChild(captureButton);
353
- cameraContainer.appendChild(closeButton);
354
- cameraContainer.appendChild(loadingIndicator);
355
- modal.appendChild(cameraContainer);
356
- document.body.appendChild(modal);
357
- console.log("[WeWear VTO] Camera modal added to DOM");
358
- // Start camera
359
- this.startCamera(video, loadingIndicator, captureButton);
360
- } /**
626
+ }
627
+ /**
361
628
  * Initializes the virtual try-on widget on the current page
362
629
  * @returns Promise that resolves when initialization is complete
363
630
  */
@@ -378,7 +645,7 @@
378
645
  container.style.position = "relative";
379
646
  }
380
647
  // Create and add the virtual try-on button
381
- const button = this.createButton(async () => {
648
+ const button = createButton(this.config.buttonPosition, async () => {
382
649
  await this.handleTryOnClick();
383
650
  });
384
651
  container.appendChild(button);
@@ -397,352 +664,252 @@
397
664
  console.log("[WeWear VTO] Button clicked, starting try-on process...");
398
665
  try {
399
666
  // Get required data
400
- const ww_access_token = this.getCookie("ww_access_token");
401
- const ww_user_id = this.getCookie("ww_user_id");
667
+ const ww_access_token = getCookie("ww_access_token");
668
+ const ww_user_id = getCookie("ww_user_id");
402
669
  const skuElement = document.querySelector(this.config.skuSelector);
403
670
  const ww_product_id = (_a = skuElement === null || skuElement === void 0 ? void 0 : skuElement.textContent) === null || _a === void 0 ? void 0 : _a.trim();
404
671
  console.log("[WeWear VTO] Retrieved data:", {
405
672
  ww_user_id,
406
673
  ww_product_id,
407
- ww_access_token: !!ww_access_token,
674
+ ww_access_token,
408
675
  });
409
676
  // Validate required data
410
677
  if (!ww_user_id) {
411
678
  console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
412
- this.showError("Please sign in to use virtual try-on");
413
679
  return;
414
680
  }
415
681
  if (!ww_product_id) {
416
682
  console.warn("[WeWear VTO] Product SKU not found:", this.config.skuSelector);
417
- this.showError("Product information not available");
683
+ return;
684
+ }
685
+ if (!ww_access_token) {
686
+ console.warn("[WeWear VTO] Missing required cookie: ww_access_token");
418
687
  return;
419
688
  }
420
689
  // Show camera capture modal
421
- this.showCameraModal(ww_access_token, ww_user_id, ww_product_id);
690
+ this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id);
422
691
  }
423
692
  catch (error) {
424
693
  console.error("[WeWear VTO] Try-on request failed:", error);
425
- this.showError("An unexpected error occurred. Please try again.");
426
694
  }
427
695
  }
428
696
  /**
429
- * Shows an error message to the user
430
- * @param message - Error message to display
697
+ * Shows camera modal with appropriate callbacks
431
698
  * @private
432
699
  */
433
- showError(message) {
434
- // Simple alert for now - could be enhanced with a custom modal
435
- alert(`WeWear Virtual Try-On: ${message}`);
700
+ showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id) {
701
+ const callbacks = {
702
+ onCapture: async (video, canvas) => {
703
+ try {
704
+ // Capture image from video
705
+ const imageBlob = await captureImageFromVideo(video, canvas);
706
+ // Stop camera and show review modal
707
+ stopCamera(video);
708
+ // Show review modal instead of immediately processing
709
+ this.showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_id);
710
+ }
711
+ catch (error) {
712
+ console.error("[WeWear VTO] Image capture error:", error);
713
+ }
714
+ },
715
+ };
716
+ showCameraModal(callbacks);
436
717
  }
437
718
  /**
438
- * Starts the camera stream
439
- * @param video - Video element to display camera stream
440
- * @param loadingIndicator - Loading indicator element
441
- * @param captureButton - Capture button element
719
+ * Shows review modal with appropriate callbacks
720
+ * @private
442
721
  */
443
- async startCamera(video, loadingIndicator, captureButton) {
444
- console.log("[WeWear VTO] Starting camera...");
722
+ showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
723
+ const callbacks = {
724
+ onRetake: () => {
725
+ // Reopen camera modal
726
+ this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id);
727
+ },
728
+ onAccept: async (acceptedImageBlob) => {
729
+ await this.processAcceptedImage(acceptedImageBlob, ww_access_token, ww_user_id, ww_product_id);
730
+ },
731
+ };
732
+ showReviewModal(imageBlob, callbacks);
733
+ }
734
+ /**
735
+ * Processes the accepted image by calling the API
736
+ * @private
737
+ */
738
+ async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
445
739
  try {
446
- const constraints = {
447
- video: {
448
- width: { ideal: 1280 },
449
- height: { ideal: 720 },
450
- facingMode: "user", // Front-facing camera
451
- },
452
- audio: false,
453
- };
454
- console.log("[WeWear VTO] Requesting camera access...");
455
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
456
- video.srcObject = stream;
457
- video.onloadedmetadata = () => {
458
- console.log("[WeWear VTO] Camera stream loaded successfully");
459
- loadingIndicator.style.display = "none";
460
- captureButton.style.display = "flex";
740
+ console.log("[WeWear VTO] Processing accepted image...");
741
+ // Show loading in the review modal first
742
+ showModalLoading("Processing image...");
743
+ // Store the API parameters for potential refresh
744
+ this.lastApiParams = {
745
+ imageBlob,
746
+ ww_access_token,
747
+ ww_user_id,
748
+ ww_product_id,
461
749
  };
750
+ // Call the API with the accepted image
751
+ const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_id, imageBlob);
752
+ // Remove modal loading and close modal
753
+ removeModalLoading();
754
+ removeElements(`.${CSS_CLASSES.MODAL}`);
755
+ if (result === null || result === void 0 ? void 0 : result.imageUrl) {
756
+ this.replaceProductImage(result.imageUrl);
757
+ }
758
+ else {
759
+ console.error("[WeWear VTO] Invalid API response:", result);
760
+ }
462
761
  }
463
762
  catch (error) {
464
- console.error("[WeWear VTO] Camera access error:", error);
465
- loadingIndicator.innerHTML =
466
- "Camera access denied. Please allow camera permissions.";
763
+ console.error("[WeWear VTO] Error processing accepted image:", error);
764
+ // Remove loading on error
765
+ removeModalLoading();
467
766
  }
468
767
  }
469
768
  /**
470
- * Stops the camera stream
471
- * @param video - Video element with camera stream
769
+ * Recalls the virtual try-on API with the same parameters
770
+ * @private
472
771
  */
473
- stopCamera(video) {
474
- const stream = video.srcObject;
475
- if (stream) {
476
- const tracks = stream.getTracks();
477
- tracks.forEach((track) => {
478
- track.stop();
479
- });
480
- video.srcObject = null;
772
+ async refreshVirtualTryOn() {
773
+ if (!this.lastApiParams) {
774
+ console.warn("[WeWear VTO] No previous API parameters available for refresh");
775
+ return;
481
776
  }
482
- }
483
- /**
484
- * Captures an image from the video stream
485
- * @param video - Video element with camera stream
486
- * @param canvas - Canvas element for capturing
487
- * @param ww_access_token - Access token for API
488
- * @param ww_user_id - User identifier
489
- * @param ww_product_id - Product identifier
490
- * @param modal - Modal element to close after processing
491
- */
492
- async captureImage(video, canvas, ww_access_token, ww_user_id, ww_product_id, modal) {
493
777
  try {
494
- // Set canvas dimensions to match video
495
- canvas.width = video.videoWidth;
496
- canvas.height = video.videoHeight;
497
- // Draw video frame to canvas
498
- const ctx = canvas.getContext("2d");
499
- if (!ctx) {
500
- throw new Error("Could not get canvas context");
778
+ console.log("[WeWear VTO] Refreshing virtual try-on with previous parameters...");
779
+ // Show loading in the product area
780
+ const container = document.querySelector(this.config.gallerySelector);
781
+ if (container instanceof HTMLElement) {
782
+ showProductLoading(container, "Generating new virtual try-on...");
783
+ }
784
+ // Call the API with the stored parameters
785
+ const result = await callVirtualTryOnApi(this.config.baseUrl, this.lastApiParams.ww_access_token, this.lastApiParams.ww_user_id, this.lastApiParams.ww_product_id, this.lastApiParams.imageBlob);
786
+ if (result === null || result === void 0 ? void 0 : result.imageUrl) {
787
+ this.virtualTryOnImageUrl = result.imageUrl;
788
+ // Find the gallery container and update the image
789
+ const container = document.querySelector(this.config.gallerySelector);
790
+ if (container instanceof HTMLElement) {
791
+ removeProductLoading(container);
792
+ this.showVirtualTryOnImage(container);
793
+ this.updateButtonContainer(container);
794
+ }
795
+ }
796
+ else {
797
+ console.error("[WeWear VTO] Invalid API response on refresh:", result);
798
+ // Remove loading on error
799
+ const container = document.querySelector(this.config.gallerySelector);
800
+ if (container instanceof HTMLElement) {
801
+ removeProductLoading(container);
802
+ }
501
803
  }
502
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
503
- // Convert canvas to blob
504
- const blob = await new Promise((resolve, reject) => {
505
- canvas.toBlob((blob) => {
506
- if (blob) {
507
- resolve(blob);
508
- }
509
- else {
510
- reject(new Error("Failed to create blob from canvas"));
511
- }
512
- }, "image/jpeg", 0.8);
513
- });
514
- // Stop camera and close camera modal
515
- this.stopCamera(video);
516
- modal.remove();
517
- // Show review modal instead of immediately processing
518
- this.showReviewModal(blob, ww_access_token, ww_user_id, ww_product_id);
519
804
  }
520
805
  catch (error) {
521
- console.error("[WeWear VTO] Image capture error:", error);
522
- this.showError("Failed to capture image. Please try again.");
806
+ console.error("[WeWear VTO] Error refreshing virtual try-on:", error);
807
+ // Remove loading on error
808
+ const container = document.querySelector(this.config.gallerySelector);
809
+ if (container instanceof HTMLElement) {
810
+ removeProductLoading(container);
811
+ }
523
812
  }
524
813
  }
525
814
  /**
526
- * Shows the review modal for captured image
527
- * @param imageBlob - Captured image blob
528
- * @param ww_access_token - Access token for API
529
- * @param ww_user_id - User identifier
530
- * @param ww_product_id - Product identifier
531
- */
532
- showReviewModal(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
533
- console.log("[WeWear VTO] Opening review modal...");
534
- // Remove any existing modals first
535
- const existingModals = document.querySelectorAll(`.${CSS_CLASSES.MODAL}`);
536
- existingModals.forEach((modal) => {
537
- modal.remove();
538
- });
539
- // Create image URL for preview
540
- const imageUrl = URL.createObjectURL(imageBlob);
541
- // Create modal container (same as camera modal)
542
- const modal = document.createElement("div");
543
- modal.className = CSS_CLASSES.MODAL;
544
- modal.style.cssText = `
545
- position: fixed;
546
- top: 0;
547
- left: 0;
548
- width: 100%;
549
- height: 100%;
550
- background-color: rgba(0, 0, 0, 0.95);
551
- display: flex;
552
- flex-direction: column;
553
- align-items: center;
554
- justify-content: center;
555
- z-index: ${Z_INDEX.MODAL};
556
- padding: 20px;
557
- box-sizing: border-box;
558
- `;
559
- // Create review container (similar to camera container)
560
- const reviewContainer = document.createElement("div");
561
- reviewContainer.style.cssText = `
562
- position: relative;
563
- width: 100%;
564
- max-width: 500px;
565
- height: 70vh;
566
- background-color: #000;
567
- border-radius: 12px;
568
- overflow: hidden;
569
- display: flex;
570
- flex-direction: column;
571
- align-items: center;
572
- justify-content: center;
573
- `;
574
- // Create image element (replaces video element)
575
- const image = document.createElement("img");
576
- image.src = imageUrl;
577
- image.alt = "Review your photo";
578
- image.style.cssText = `
579
- width: 100%;
580
- height: 100%;
581
- object-fit: cover;
582
- border-radius: 12px;
583
- `;
584
- // Create close button (same as camera modal)
585
- const closeButton = document.createElement("button");
586
- closeButton.innerHTML = "×";
587
- closeButton.style.cssText = `
588
- position: absolute;
589
- top: 15px;
590
- right: 15px;
591
- width: 40px;
592
- height: 40px;
593
- border: none;
594
- background-color: rgba(0, 0, 0, 0.7);
595
- color: white;
596
- font-size: 24px;
597
- font-weight: bold;
598
- cursor: pointer;
599
- border-radius: 50%;
600
- display: flex;
601
- align-items: center;
602
- justify-content: center;
603
- `;
604
- // Create button container positioned at bottom (similar to capture button position)
605
- const buttonContainer = document.createElement("div");
606
- buttonContainer.style.cssText = `
607
- position: absolute;
608
- bottom: 20px;
609
- left: 50%;
610
- transform: translateX(-50%);
611
- display: flex;
612
- gap: 15px;
613
- width: calc(100% - 40px);
614
- max-width: 300px;
615
- `;
616
- // Create retake button
617
- const retakeButton = document.createElement("button");
618
- retakeButton.textContent = "Retake";
619
- retakeButton.style.cssText = `
620
- flex: 1;
621
- padding: 12px 24px;
622
- background-color: transparent;
623
- color: white;
624
- border: 2px solid white;
625
- border-radius: 8px;
626
- font-size: 16px;
627
- font-weight: 600;
628
- cursor: pointer;
629
- transition: all 0.2s ease;
630
- `;
631
- // Create use photo button
632
- const usePhotoButton = document.createElement("button");
633
- usePhotoButton.textContent = "Use Photo";
634
- usePhotoButton.style.cssText = `
635
- flex: 1;
636
- padding: 12px 24px;
637
- background-color: white;
638
- color: black;
639
- border: 2px solid white;
640
- border-radius: 8px;
641
- font-size: 16px;
642
- font-weight: 600;
643
- cursor: pointer;
644
- transition: all 0.2s ease;
645
- `;
646
- // Add hover effects
647
- retakeButton.addEventListener("mouseenter", () => {
648
- retakeButton.style.backgroundColor = "white";
649
- retakeButton.style.color = "black";
650
- });
651
- retakeButton.addEventListener("mouseleave", () => {
652
- retakeButton.style.backgroundColor = "transparent";
653
- retakeButton.style.color = "white";
654
- });
655
- usePhotoButton.addEventListener("mouseenter", () => {
656
- usePhotoButton.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
657
- });
658
- usePhotoButton.addEventListener("mouseleave", () => {
659
- usePhotoButton.style.backgroundColor = "white";
660
- });
661
- // Add event listeners
662
- closeButton.onclick = () => {
663
- URL.revokeObjectURL(imageUrl); // Clean up
664
- modal.remove();
665
- };
666
- retakeButton.onclick = () => {
667
- URL.revokeObjectURL(imageUrl); // Clean up
668
- modal.remove();
669
- // Reopen camera modal
670
- this.showCameraModal(ww_access_token, ww_user_id, ww_product_id);
671
- };
672
- usePhotoButton.onclick = async () => {
673
- URL.revokeObjectURL(imageUrl); // Clean up
674
- modal.remove();
675
- // Process the image
676
- await this.processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id);
677
- };
678
- // Assemble the review modal (similar to camera modal)
679
- buttonContainer.appendChild(retakeButton);
680
- buttonContainer.appendChild(usePhotoButton);
681
- reviewContainer.appendChild(image);
682
- reviewContainer.appendChild(closeButton);
683
- reviewContainer.appendChild(buttonContainer);
684
- modal.appendChild(reviewContainer);
685
- document.body.appendChild(modal);
686
- console.log("[WeWear VTO] Review modal added to DOM");
687
- }
688
- /**
689
- * Processes the accepted image by calling the API
690
- * @param imageBlob - Accepted image blob
691
- * @param ww_access_token - Access token for API
692
- * @param ww_user_id - User identifier
693
- * @param ww_product_id - Product identifier
815
+ * Replaces the product image in the gallery with the virtual try-on result
816
+ * @private
694
817
  */
695
- async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
818
+ replaceProductImage(imageUrl) {
696
819
  try {
697
- console.log("[WeWear VTO] Processing accepted image...");
698
- // Call the API with the accepted image
699
- const result = await this.callVirtualTryOnApiWithImage(ww_access_token, ww_user_id, ww_product_id, imageBlob);
700
- if (result === null || result === void 0 ? void 0 : result.imageUrl) {
701
- this.showImageModal(result.imageUrl);
820
+ // Store the virtual try-on image URL
821
+ this.virtualTryOnImageUrl = imageUrl;
822
+ // Find the gallery container
823
+ const container = document.querySelector(this.config.gallerySelector);
824
+ if (!container || !(container instanceof HTMLElement)) {
825
+ console.warn("[WeWear VTO] Gallery container not found for image replacement:", this.config.gallerySelector);
826
+ return;
702
827
  }
703
- else {
704
- console.error("[WeWear VTO] Invalid API response:", result);
828
+ // Store the original content if not already stored
829
+ if (!container.hasAttribute("data-ww-original-content")) {
830
+ container.setAttribute("data-ww-original-content", container.innerHTML);
705
831
  }
832
+ // Show the virtual try-on image initially
833
+ this.showVirtualTryOnImage(container);
834
+ // Replace the button container with the new multi-button version
835
+ this.updateButtonContainer(container);
836
+ console.log("[WeWear VTO] Product image replaced with virtual try-on result");
706
837
  }
707
838
  catch (error) {
708
- console.error("[WeWear VTO] Error processing accepted image:", error);
709
- this.showError("Failed to process image. Please try again.");
839
+ console.error("[WeWear VTO] Error replacing product image:", error);
710
840
  }
711
841
  }
712
842
  /**
713
- * Makes API call to virtual try-on service with captured image
714
- * @param ww_access_token - Optional authentication token
715
- * @param ww_user_id - User identifier
716
- * @param ww_product_id - Product identifier
717
- * @param imageBlob - Captured image blob
718
- * @returns Promise with virtual try-on result or null if failed
843
+ * Updates the button container to show all available buttons
844
+ * @private
719
845
  */
720
- async callVirtualTryOnApiWithImage(ww_access_token, ww_user_id, ww_product_id, imageBlob) {
721
- try {
722
- // Create form data for multipart upload
723
- const formData = new FormData();
724
- formData.append("ww_user_id", ww_user_id);
725
- formData.append("ww_product_id", ww_product_id);
726
- formData.append("image", imageBlob, "captured-image.jpg");
727
- if (ww_access_token) {
728
- formData.append("ww_access_token", ww_access_token);
846
+ updateButtonContainer(container) {
847
+ // Remove existing button containers
848
+ const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
849
+ existingButtons.forEach((btn) => {
850
+ btn.remove();
851
+ });
852
+ // Create new button container with all buttons
853
+ const buttonContainer = createButtonContainer(this.config.buttonPosition, this.virtualTryOnImageUrl !== null, async () => {
854
+ await this.handleTryOnClick();
855
+ }, async () => {
856
+ await this.refreshVirtualTryOn();
857
+ }, () => {
858
+ if (this.isShowingVirtualTryOn) {
859
+ this.showOriginalImage(container);
729
860
  }
730
- const response = await fetch(`${this.config.baseUrl}/api/virtual-try-on`, {
731
- method: "POST",
732
- body: formData,
733
- });
734
- if (!response.ok) {
735
- console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
736
- return null;
861
+ else {
862
+ this.showVirtualTryOnImage(container);
737
863
  }
738
- const result = await response.json();
739
- console.log("[WeWear VTO] API response:", result);
740
- return result;
864
+ this.updateButtonContainer(container);
865
+ }, this.isShowingVirtualTryOn);
866
+ container.appendChild(buttonContainer);
867
+ }
868
+ /**
869
+ * Shows the virtual try-on image in the container
870
+ * @private
871
+ */
872
+ showVirtualTryOnImage(container) {
873
+ if (!this.virtualTryOnImageUrl) {
874
+ console.warn("[WeWear VTO] No virtual try-on image URL available");
875
+ return;
741
876
  }
742
- catch (error) {
743
- console.error("[WeWear VTO] API call failed:", error);
744
- return null;
877
+ // Clear existing content except buttons
878
+ const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
879
+ container.innerHTML = "";
880
+ const image = document.createElement("img");
881
+ image.src = this.virtualTryOnImageUrl;
882
+ image.alt = "Virtual Try-On Result";
883
+ image.style.cssText = `
884
+ width: 100%;
885
+ height: 100%;
886
+ object-fit: cover;
887
+ border-radius: 8px;
888
+ `;
889
+ container.appendChild(image);
890
+ // Re-add buttons
891
+ existingButtons.forEach((btn) => {
892
+ container.appendChild(btn);
893
+ });
894
+ this.isShowingVirtualTryOn = true;
895
+ }
896
+ /**
897
+ * Shows the original product image in the container
898
+ * @private
899
+ */
900
+ showOriginalImage(container) {
901
+ const originalContent = container.getAttribute("data-ww-original-content");
902
+ if (originalContent) {
903
+ // Store existing buttons
904
+ const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
905
+ // Restore original content
906
+ container.innerHTML = originalContent;
907
+ // Re-add buttons
908
+ existingButtons.forEach((btn) => {
909
+ container.appendChild(btn);
910
+ });
745
911
  }
912
+ this.isShowingVirtualTryOn = false;
746
913
  }
747
914
  /**
748
915
  * Destroys the widget and cleans up resources
@@ -750,15 +917,13 @@
750
917
  destroy() {
751
918
  try {
752
919
  // Remove all buttons
753
- const buttons = document.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
754
- buttons.forEach((button) => {
755
- button.remove();
756
- });
920
+ removeElements(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
757
921
  // Remove all modals
758
- const modals = document.querySelectorAll(`.${CSS_CLASSES.MODAL}`);
759
- modals.forEach((modal) => {
760
- modal.remove();
761
- });
922
+ removeElements(`.${CSS_CLASSES.MODAL}`);
923
+ // Reset state
924
+ this.virtualTryOnImageUrl = null;
925
+ this.isShowingVirtualTryOn = false;
926
+ this.lastApiParams = null;
762
927
  console.log("[WeWear VTO] Widget destroyed successfully");
763
928
  }
764
929
  catch (error) {
@@ -767,17 +932,7 @@
767
932
  }
768
933
  }
769
934
 
770
- /**
771
- * WeWear Virtual Try-On Widget Auto-Installer
772
- *
773
- * Provides automatic initialization and management of the virtual try-on widget.
774
- * Handles DOM ready states and widget lifecycle management.
775
- */
776
935
  let widgetInstance = null;
777
- /**
778
- * Initializes the virtual try-on widget with optional configuration
779
- * @param config - Widget configuration options
780
- */
781
936
  function initVirtualTryOn(config) {
782
937
  try {
783
938
  // Clean up any existing instance
@@ -805,9 +960,6 @@
805
960
  console.error("[WeWear VTO] Initialization error:", error);
806
961
  }
807
962
  }
808
- /**
809
- * Destroys the current widget instance and cleans up resources
810
- */
811
963
  function destroyVirtualTryOn() {
812
964
  try {
813
965
  if (widgetInstance) {
@@ -820,13 +972,16 @@
820
972
  console.error("[WeWear VTO] Error destroying widget:", error);
821
973
  }
822
974
  }
823
- // Auto-initialize if window object exists (browser environment)
975
+ function getWidgetInstance() {
976
+ return widgetInstance;
977
+ }
824
978
  if (typeof window !== "undefined") {
825
979
  initVirtualTryOn();
826
980
  }
827
981
 
828
982
  exports.VirtualTryOnWidget = VirtualTryOnWidget;
829
983
  exports.destroyVirtualTryOn = destroyVirtualTryOn;
984
+ exports.getWidgetInstance = getWidgetInstance;
830
985
  exports.initVirtualTryOn = initVirtualTryOn;
831
986
 
832
987
  }));