@akinon/pz-virtual-try-on 2.0.34-beta.0 → 2.0.34

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.
@@ -72,7 +72,7 @@ export interface VirtualTryOnAsyncRequest {
72
72
  }
73
73
 
74
74
  export interface VirtualTryOnAsyncResponse {
75
- reference_url: string;
75
+ process_id: string;
76
76
  status: 'pending' | 'processing';
77
77
  message?: string;
78
78
  }
@@ -85,7 +85,7 @@ export type VirtualTryOnJobStatus =
85
85
  | 'failed';
86
86
 
87
87
  export interface VirtualTryOnJobStatusResponse {
88
- reference_url: string;
88
+ process_id?: string;
89
89
  status: VirtualTryOnJobStatus;
90
90
  created_at?: string;
91
91
  started_at?: string;
@@ -93,24 +93,27 @@ export interface VirtualTryOnJobStatusResponse {
93
93
 
94
94
  // NEW API FORMAT: result object
95
95
  result?: {
96
- job_type: 'single' | 'multiple';
96
+ job_type: 'single_try_on' | 'multiple_try_on' | 'single' | 'multiple';
97
97
  single?: {
98
- reference: string;
99
98
  generated: string;
100
- selected: string[];
99
+ selected?: string[];
100
+ reference?: string;
101
101
  };
102
102
  multiple?: {
103
- reference: string;
103
+ reference?: string;
104
104
  products: VirtualTryOnMultipleResult[];
105
105
  };
106
106
  error?: VirtualTryOnCompatibilityError;
107
107
  };
108
108
 
109
+ tryon_limit?: number;
110
+ used_count?: number;
111
+
109
112
  // LEGACY: Direct fields (backward compatibility)
110
113
  generated?: string;
111
114
  selected?: string[];
112
115
  error?: VirtualTryOnCompatibilityError | string; // Can be string from backend
113
- job_type?: 'single' | 'multiple';
116
+ job_type?: 'single_try_on' | 'multiple_try_on' | 'single' | 'multiple';
114
117
  multiple?: VirtualTryOnMultipleResult[];
115
118
  }
116
119
 
@@ -119,11 +122,13 @@ export interface VirtualTryOnMultipleResult {
119
122
  reference?: string;
120
123
  generated: string;
121
124
  selected?: string[];
125
+ used_count?: number;
122
126
  error?: VirtualTryOnCompatibilityError | null;
123
127
  }
124
128
 
125
129
  export interface VirtualTryOnCompatibilityError {
126
130
  code:
131
+ | 'INCOMPATIBLE'
127
132
  | 'child'
128
133
  | 'lighting'
129
134
  | 'multiperson'
@@ -136,7 +141,19 @@ export interface VirtualTryOnCompatibilityError {
136
141
  | 'others'
137
142
  | 'empty';
138
143
  detail: string;
139
- error: string;
144
+ error?: string;
145
+ message?: string;
146
+ }
147
+
148
+ export interface TryOnUsageResponse {
149
+ tryon_limit: number;
150
+ skus: { sku: string; used_count: number }[];
151
+ }
152
+
153
+ export interface TryOnEventResponse {
154
+ sku: string;
155
+ event_name: string;
156
+ created_at: string;
140
157
  }
141
158
 
142
159
  export interface BasketProduct {
@@ -429,6 +446,11 @@ export interface CustomRendererProps {
429
446
  text: string;
430
447
  disabled?: boolean;
431
448
  }) => React.ReactNode;
449
+ renderUsageInfo?: (props: {
450
+ used: number;
451
+ limit: number;
452
+ text: string;
453
+ }) => React.ReactNode;
432
454
  };
433
455
  productSelector?: {
434
456
  renderModal?: (props: {
@@ -498,6 +520,11 @@ export interface CustomRendererProps {
498
520
  renderBackIcon?: () => React.ReactNode;
499
521
  renderCloseIcon?: () => React.ReactNode;
500
522
  renderErrorIcon?: () => React.ReactNode;
523
+ renderUsageInfo?: (props: {
524
+ used: number;
525
+ limit: number;
526
+ text: string;
527
+ }) => React.ReactNode;
501
528
  };
502
529
  basketAsyncModal?: {
503
530
  renderModal?: (props: {
@@ -512,6 +539,11 @@ export interface CustomRendererProps {
512
539
  }) => React.ReactNode;
513
540
  renderCloseIcon?: () => React.ReactNode;
514
541
  renderProcessingSpinner?: () => React.ReactNode;
542
+ renderUsageInfo?: (props: {
543
+ used: number;
544
+ limit: number;
545
+ text: string;
546
+ }) => React.ReactNode;
515
547
  };
516
548
  basketVirtualTryOn?: {
517
549
  renderButton?: (props: {
@@ -524,6 +556,7 @@ export interface CustomRendererProps {
524
556
  text: string;
525
557
  className?: string;
526
558
  }) => React.ReactNode;
559
+ renderUsageCheckLoader?: () => React.ReactNode;
527
560
  };
528
561
  multipleModal?: {
529
562
  renderCloseIcon?: () => React.ReactNode;
@@ -551,6 +584,7 @@ export interface CustomRendererProps {
551
584
  variant?: 'default' | 'outlined' | 'ghost';
552
585
  size?: 'sm' | 'md' | 'lg';
553
586
  }) => React.ReactNode;
587
+ renderUsageCheckLoader?: () => React.ReactNode;
554
588
  renderIcon?: (props: {
555
589
  name: string;
556
590
  size: number;
@@ -814,6 +848,12 @@ export interface VirtualTryOnPluginSettings {
814
848
  loadingOverlay?: string;
815
849
  loadingText?: string;
816
850
 
851
+ usageCheckOverlay?: string;
852
+ usageCheckSpinner?: string;
853
+ resultModalUsageInfo?: string;
854
+ multipleResultModalUsageInfo?: string;
855
+ basketAsyncModalUsageInfo?: string;
856
+
817
857
  modalEnter?: string;
818
858
  modalExit?: string;
819
859
 
@@ -1,4 +1,5 @@
1
1
  import type { VirtualTryOnJobStatusResponse } from '../types';
2
+ import { parseVirtualTryOnError } from './error-parser';
2
3
 
3
4
  export interface PollingConfig {
4
5
  referenceUrl: string;
@@ -89,11 +90,13 @@ export class VirtualTryOnPoller {
89
90
  if (response.status === 'failed') {
90
91
  this.stop();
91
92
 
92
- const errorMessage =
93
+ const rawError =
93
94
  typeof response.error === 'string'
94
95
  ? response.error
95
- : response.error?.detail || 'Processing failed';
96
- this.config.onError(errorMessage);
96
+ : response.error?.detail || response.error?.message;
97
+ this.config.onError(
98
+ rawError ? parseVirtualTryOnError(rawError) : 'Processing failed'
99
+ );
97
100
  return false;
98
101
  }
99
102
 
@@ -185,6 +185,7 @@ export function BasketAsyncModal({
185
185
  },
186
186
  clearError: () => {},
187
187
  selectedFeedback: hookData.feedbackStates[selectedDetailIndex],
188
+ limitInfo: hookData.limitInfo,
188
189
  retryTryOn: async () => {
189
190
  if (hookData.uploadedImage && hookData.retrySingleProduct) {
190
191
  setCurrentStep('processing');
@@ -414,6 +415,43 @@ export function BasketAsyncModal({
414
415
  >
415
416
  {t('product.virtual_try_on.click_to_view_details')}
416
417
  </p>
418
+ {hookData.limitInfo?.tryon_limit !== undefined &&
419
+ hookData.limitInfo?.used_count !== undefined &&
420
+ (settings?.customRenderers?.render?.basketAsyncModal
421
+ ?.renderUsageInfo ? (
422
+ settings.customRenderers.render.basketAsyncModal.renderUsageInfo(
423
+ {
424
+ used: hookData.limitInfo.used_count,
425
+ limit: hookData.limitInfo.tryon_limit,
426
+ text: t('product.virtual_try_on.usage_info')
427
+ .replace(
428
+ '{{used}}',
429
+ String(hookData.limitInfo.used_count)
430
+ )
431
+ .replace(
432
+ '{{limit}}',
433
+ String(hookData.limitInfo.tryon_limit)
434
+ )
435
+ }
436
+ )
437
+ ) : (
438
+ <p
439
+ className={twMerge(
440
+ 'text-xs text-gray-500 pb-4',
441
+ settings?.customStyles?.basketAsyncModalUsageInfo
442
+ )}
443
+ >
444
+ {t('product.virtual_try_on.usage_info')
445
+ .replace(
446
+ '{{used}}',
447
+ String(hookData.limitInfo.used_count)
448
+ )
449
+ .replace(
450
+ '{{limit}}',
451
+ String(hookData.limitInfo.tryon_limit)
452
+ )}
453
+ </p>
454
+ ))}
417
455
  </div>
418
456
 
419
457
  <div
@@ -2,7 +2,12 @@
2
2
 
3
3
  import React, { useState, useMemo, useEffect } from 'react';
4
4
  import { twMerge } from 'tailwind-merge';
5
- import type { BasketProduct, VirtualTryOnPluginSettings } from '../types';
5
+ import { useSession } from 'next-auth/react';
6
+ import type {
7
+ BasketProduct,
8
+ TryOnUsageResponse,
9
+ VirtualTryOnPluginSettings
10
+ } from '../types';
6
11
  import { VirtualTryOnProductSelector } from './virtual-try-on-product-selector';
7
12
  import { VirtualTryOnUploadModal } from './virtual-try-on-upload-modal';
8
13
  import { BasketAsyncModal } from './basket-async-modal';
@@ -12,9 +17,13 @@ import {
12
17
  isVirtualTryOnEnabled,
13
18
  VirtualTryOnCache
14
19
  } from '../utils';
15
- import { useGetVirtualTryOnLimitedCategoriesQuery } from '../data/endpoints';
20
+ import {
21
+ useGetTryOnUsageMutation,
22
+ useGetVirtualTryOnLimitedCategoriesQuery
23
+ } from '../data/endpoints';
16
24
  import virtualCabinIcon from '../../static/virtual-cabin-icon.svg';
17
- import { useLocalization } from '@akinon/next/hooks';
25
+ import { LoaderSpinner } from '@akinon/next/components';
26
+ import { useLocalization, useRouter } from '@akinon/next/hooks';
18
27
 
19
28
  export interface BasketVirtualTryOnProps {
20
29
  basketItems: BasketProduct[];
@@ -32,12 +41,19 @@ export function BasketVirtualTryOn({
32
41
  settings
33
42
  }: BasketVirtualTryOnProps) {
34
43
  const { t } = useLocalization();
44
+ const { status } = useSession();
45
+ const router = useRouter();
35
46
  const [isSelectorOpen, setIsSelectorOpen] = useState(false);
36
47
  const [showUploadModal, setShowUploadModal] = useState(false);
37
48
  const [showAsyncModal, setShowAsyncModal] = useState(false);
38
49
  const [selectedProducts, setSelectedProducts] = useState<BasketProduct[]>([]);
39
- const [categoryMapping, setCategoryMapping] = useState<Record<string, number[]>>({});
50
+ const [categoryMapping, setCategoryMapping] = useState<
51
+ Record<string, number[]>
52
+ >({});
40
53
  const [isCategoriesLoading, setIsCategoriesLoading] = useState(true);
54
+ const [usageData, setUsageData] = useState<TryOnUsageResponse | null>(null);
55
+ const [isUsageChecking, setIsUsageChecking] = useState(false);
56
+ const [getTryOnUsage] = useGetTryOnUsageMutation();
41
57
 
42
58
  const hookData = useVirtualTryOnAsync(selectedProducts, categoryMapping);
43
59
 
@@ -66,9 +82,9 @@ export function BasketVirtualTryOn({
66
82
 
67
83
  const fetchCategories = async () => {
68
84
  try {
69
- const pks = basketItems.map(item => item.pk).join(',');
85
+ const pks = basketItems.map((item) => item.pk).join(',');
70
86
  const response = await fetch(`/api/product-categories?pks=${pks}`);
71
-
87
+
72
88
  if (response.ok) {
73
89
  const mapping = await response.json();
74
90
  setCategoryMapping(mapping);
@@ -170,10 +186,51 @@ export function BasketVirtualTryOn({
170
186
  isCategoriesLoading
171
187
  ]);
172
188
 
173
- const handleOpenSelector = () => {
189
+ const handleOpenSelector = async () => {
190
+ if (isUsageChecking) {
191
+ return;
192
+ }
193
+
194
+ setIsUsageChecking(true);
195
+ try {
196
+ const usage = await getTryOnUsage({
197
+ skus: eligibleProducts.map((item) => item.sku)
198
+ }).unwrap();
199
+ setUsageData(usage);
200
+ } catch (usageError: any) {
201
+ if (
202
+ usageError?.status === 400 &&
203
+ usageError?.data?.code === 'VALIDATION'
204
+ ) {
205
+ setIsUsageChecking(false);
206
+ router.push(
207
+ `/users/auth?callbackUrl=${encodeURIComponent(
208
+ window.location.pathname + window.location.search
209
+ )}`
210
+ );
211
+ return;
212
+ }
213
+ setUsageData(null);
214
+ }
215
+ setIsUsageChecking(false);
216
+
174
217
  setIsSelectorOpen(true);
175
218
  };
176
219
 
220
+ const disabledSkus = useMemo(() => {
221
+ if (
222
+ status !== 'authenticated' ||
223
+ !usageData ||
224
+ !(usageData.tryon_limit > 0)
225
+ ) {
226
+ return [];
227
+ }
228
+
229
+ return usageData.skus
230
+ .filter((item) => item.used_count >= usageData.tryon_limit)
231
+ .map((item) => item.sku);
232
+ }, [status, usageData]);
233
+
177
234
  const handleProductsSelected = (products: BasketProduct[]) => {
178
235
  setSelectedProducts(products);
179
236
  setIsSelectorOpen(false);
@@ -202,7 +259,11 @@ export function BasketVirtualTryOn({
202
259
  onComplete?.(result);
203
260
  };
204
261
 
205
- if (!isFeatureEnabled || isCategoriesLoading || eligibleProducts.length === 0) {
262
+ if (
263
+ !isFeatureEnabled ||
264
+ isCategoriesLoading ||
265
+ eligibleProducts.length === 0
266
+ ) {
206
267
  return null;
207
268
  }
208
269
 
@@ -215,12 +276,30 @@ export function BasketVirtualTryOn({
215
276
  isEnabled: isFeatureEnabled
216
277
  })}
217
278
 
279
+ {isUsageChecking &&
280
+ (settings?.customRenderers?.render?.basketVirtualTryOn
281
+ ?.renderUsageCheckLoader ? (
282
+ settings.customRenderers.render.basketVirtualTryOn.renderUsageCheckLoader()
283
+ ) : (
284
+ <div
285
+ className={twMerge(
286
+ 'fixed inset-0 z-[9999]',
287
+ settings?.customStyles?.usageCheckOverlay
288
+ )}
289
+ >
290
+ <LoaderSpinner
291
+ className={settings?.customStyles?.usageCheckSpinner}
292
+ />
293
+ </div>
294
+ ))}
295
+
218
296
  {isSelectorOpen && (
219
297
  <VirtualTryOnProductSelector
220
298
  isOpen={isSelectorOpen}
221
299
  onClose={() => setIsSelectorOpen(false)}
222
300
  products={eligibleProducts}
223
301
  maxSelection={3}
302
+ disabledSkus={disabledSkus}
224
303
  onConfirm={handleProductsSelected}
225
304
  settings={settings}
226
305
  />
@@ -285,8 +364,9 @@ export function BasketVirtualTryOn({
285
364
  >
286
365
  <button
287
366
  onClick={handleOpenSelector}
367
+ disabled={isUsageChecking}
288
368
  className={twMerge(
289
- 'px-6 py-3 rounded-[24px] border border-[#979797] bg-white text-black font-medium hover:bg-gray-50 transition-colors duration-200 flex items-center gap-2',
369
+ 'px-6 py-3 rounded-[24px] border border-[#979797] bg-white text-black font-medium hover:bg-gray-50 transition-colors duration-200 flex items-center gap-2 disabled:opacity-60',
290
370
  settings?.customStyles?.basketVirtualTryOnButton,
291
371
  buttonClassName
292
372
  )}
@@ -323,12 +403,30 @@ export function BasketVirtualTryOn({
323
403
  </button>
324
404
  </div>
325
405
 
406
+ {isUsageChecking &&
407
+ (settings?.customRenderers?.render?.basketVirtualTryOn
408
+ ?.renderUsageCheckLoader ? (
409
+ settings.customRenderers.render.basketVirtualTryOn.renderUsageCheckLoader()
410
+ ) : (
411
+ <div
412
+ className={twMerge(
413
+ 'fixed inset-0 z-[9999]',
414
+ settings?.customStyles?.usageCheckOverlay
415
+ )}
416
+ >
417
+ <LoaderSpinner
418
+ className={settings?.customStyles?.usageCheckSpinner}
419
+ />
420
+ </div>
421
+ ))}
422
+
326
423
  {isSelectorOpen && (
327
424
  <VirtualTryOnProductSelector
328
425
  isOpen={isSelectorOpen}
329
426
  onClose={() => setIsSelectorOpen(false)}
330
427
  products={eligibleProducts}
331
428
  maxSelection={3}
429
+ disabledSkus={disabledSkus}
332
430
  onConfirm={handleProductsSelected}
333
431
  settings={settings}
334
432
  />
@@ -3,10 +3,12 @@
3
3
  import React, { useState, useMemo, useEffect } from 'react';
4
4
  import { Product } from '@akinon/next/types';
5
5
  import { twMerge } from 'tailwind-merge';
6
+ import { useLocalization, useRouter, useToast } from '@akinon/next/hooks';
6
7
  import { VirtualTryOnModal } from './virtual-try-on-modal';
7
8
  import { VirtualTryOnButton } from './virtual-try-on-button';
8
9
  import { useVirtualTryOn } from '../hooks/use-virtual-try-on';
9
- import { ProcessingSpinner } from '../components/processing-spinner';
10
+ import { useGetTryOnUsageMutation } from '../data/endpoints';
11
+ import { LoaderSpinner } from '@akinon/next/components';
10
12
  import type { VirtualTryOnPluginSettings } from '../types';
11
13
 
12
14
  interface VirtualTryOnPluginProps {
@@ -28,7 +30,12 @@ export function VirtualTryOnPlugin({
28
30
  }: VirtualTryOnPluginProps) {
29
31
  const [isModalOpen, setIsModalOpen] = useState(autoOpen);
30
32
  const [isModalLoading, setIsModalLoading] = useState(false);
33
+ const [isUsageChecking, setIsUsageChecking] = useState(false);
31
34
  const { isEnabled } = useVirtualTryOn(product);
35
+ const { t } = useLocalization();
36
+ const { error: showErrorToast } = useToast();
37
+ const router = useRouter();
38
+ const [getTryOnUsage] = useGetTryOnUsageMutation();
32
39
 
33
40
  useEffect(() => {
34
41
  return () => {
@@ -105,7 +112,42 @@ export function VirtualTryOnPlugin({
105
112
  return variables;
106
113
  }, [settings?.cssVariables, settings?.theme]);
107
114
 
108
- const handleOpenModal = () => {
115
+ const handleOpenModal = async () => {
116
+ if (isUsageChecking) {
117
+ return;
118
+ }
119
+
120
+ const sku = product.sku || product.pk.toString();
121
+
122
+ setIsUsageChecking(true);
123
+ try {
124
+ const usage = await getTryOnUsage({ skus: [sku] }).unwrap();
125
+
126
+ if (usage?.tryon_limit > 0) {
127
+ const skuUsage = usage.skus?.find((item) => item.sku === sku);
128
+
129
+ if (skuUsage && skuUsage.used_count >= usage.tryon_limit) {
130
+ setIsUsageChecking(false);
131
+ showErrorToast(t('product.virtual_try_on.limit_exceeded'));
132
+ return;
133
+ }
134
+ }
135
+ } catch (usageError: any) {
136
+ if (
137
+ usageError?.status === 400 &&
138
+ usageError?.data?.code === 'VALIDATION'
139
+ ) {
140
+ setIsUsageChecking(false);
141
+ router.push(
142
+ `/users/auth?callbackUrl=${encodeURIComponent(
143
+ window.location.pathname + window.location.search
144
+ )}`
145
+ );
146
+ return;
147
+ }
148
+ }
149
+ setIsUsageChecking(false);
150
+
109
151
  setIsModalLoading(true);
110
152
  setTimeout(() => {
111
153
  setIsModalLoading(false);
@@ -139,7 +181,7 @@ export function VirtualTryOnPlugin({
139
181
  {settings.customRenderers.render.button.renderButton({
140
182
  onClick: handleOpenModal,
141
183
  isLoading: false,
142
- disabled: isModalLoading,
184
+ disabled: isModalLoading || isUsageChecking,
143
185
  className: twMerge(settings?.customStyles?.button, className),
144
186
  variant: buttonVariant,
145
187
  size: buttonSize
@@ -150,7 +192,7 @@ export function VirtualTryOnPlugin({
150
192
  <CustomButton
151
193
  onClick={handleOpenModal}
152
194
  isLoading={false}
153
- disabled={isModalLoading}
195
+ disabled={isModalLoading || isUsageChecking}
154
196
  className={twMerge(settings?.customStyles?.button, className)}
155
197
  />
156
198
  </div>
@@ -165,26 +207,28 @@ export function VirtualTryOnPlugin({
165
207
  variant={buttonVariant}
166
208
  size={buttonSize}
167
209
  isLoading={false}
168
- disabled={isModalLoading}
210
+ disabled={isModalLoading || isUsageChecking}
169
211
  settings={settings}
170
212
  style={cssVariables}
171
213
  />
172
214
  )}
173
215
 
174
- {isModalLoading && (
175
- <div
176
- className={twMerge(
177
- 'fixed inset-0 flex flex-col items-center justify-center backdrop-blur-[12px] z-[9999]',
178
- settings?.customStyles?.processingOverlay
179
- )}
180
- style={{
181
- ...cssVariables,
182
- backgroundColor: 'rgba(0, 0, 0, 0.6)'
183
- }}
184
- >
185
- <ProcessingSpinner settings={settings} />
186
- </div>
187
- )}
216
+ {(isUsageChecking || isModalLoading) &&
217
+ (settings?.customRenderers?.render?.button?.renderUsageCheckLoader ? (
218
+ settings.customRenderers.render.button.renderUsageCheckLoader()
219
+ ) : (
220
+ <div
221
+ className={twMerge(
222
+ 'fixed inset-0 z-[9999]',
223
+ settings?.customStyles?.usageCheckOverlay
224
+ )}
225
+ style={cssVariables}
226
+ >
227
+ <LoaderSpinner
228
+ className={settings?.customStyles?.usageCheckSpinner}
229
+ />
230
+ </div>
231
+ ))}
188
232
 
189
233
  <VirtualTryOnModal
190
234
  isOpen={isModalOpen}
@@ -82,6 +82,7 @@ export function VirtualTryOnMultipleModal({
82
82
  isStarting,
83
83
  isProcessing,
84
84
  isComplete,
85
+ limitInfo,
85
86
  handleFileUpload,
86
87
  acceptLegalConsent,
87
88
  startTryOn,
@@ -129,6 +130,7 @@ export function VirtualTryOnMultipleModal({
129
130
  results={results}
130
131
  products={products}
131
132
  settings={settings}
133
+ limitInfo={limitInfo}
132
134
  />
133
135
  );
134
136
  }
@@ -358,7 +360,9 @@ export function VirtualTryOnMultipleModal({
358
360
  settings?.customStyles?.multipleModalUploadDescription
359
361
  )}
360
362
  >
361
- {`${t('product.virtual_try_on.upload_title')} - ${products.length} ${t('product.virtual_try_on.products_count')}`}
363
+ {`${t('product.virtual_try_on.upload_title')} - ${
364
+ products.length
365
+ } ${t('product.virtual_try_on.products_count')}`}
362
366
  </p>
363
367
  </div>
364
368
 
@@ -407,7 +411,9 @@ export function VirtualTryOnMultipleModal({
407
411
  {t('product.virtual_try_on.photo_preview')}
408
412
  </h3>
409
413
  <p className="text-sm text-gray-600">
410
- {`${products.length} ${t('product.virtual_try_on.products_count')} ${t('product.virtual_try_on.will_start')}`}
414
+ {`${products.length} ${t(
415
+ 'product.virtual_try_on.products_count'
416
+ )} ${t('product.virtual_try_on.will_start')}`}
411
417
  </p>
412
418
  </div>
413
419
 
@@ -11,7 +11,7 @@ import type {
11
11
  } from '../types';
12
12
  import { Image } from '@akinon/next/components/image';
13
13
  import { useSubmitVirtualTryOnFeedbackMutation } from '../data/endpoints';
14
- import { getErrorMessage, hasCompatibilityError } from '../utils/error-mapping';
14
+ import { parseVirtualTryOnError } from '../utils';
15
15
  import { useLocalization } from '@akinon/next/hooks';
16
16
 
17
17
  export interface VirtualTryOnMultipleResultModalProps {
@@ -25,6 +25,7 @@ export interface VirtualTryOnMultipleResultModalProps {
25
25
  settings?: VirtualTryOnPluginSettings;
26
26
  hasFailedResults?: boolean;
27
27
  isRetrying?: boolean;
28
+ limitInfo?: { tryon_limit?: number; used_count?: number } | null;
28
29
  }
29
30
 
30
31
  export function VirtualTryOnMultipleResultModal({
@@ -37,7 +38,8 @@ export function VirtualTryOnMultipleResultModal({
37
38
  className,
38
39
  settings,
39
40
  hasFailedResults = false,
40
- isRetrying = false
41
+ isRetrying = false,
42
+ limitInfo = null
41
43
  }: VirtualTryOnMultipleResultModalProps) {
42
44
  const { t } = useLocalization();
43
45
  const [feedbackStates, setFeedbackStates] = useState<
@@ -277,6 +279,31 @@ export function VirtualTryOnMultipleResultModal({
277
279
  products.length.toString()
278
280
  ) || `${products.length} ürün için deneme tamamlandı`}
279
281
  </p>
282
+ {limitInfo?.tryon_limit !== undefined &&
283
+ limitInfo?.used_count !== undefined &&
284
+ (settings?.customRenderers?.render?.multipleResultModal
285
+ ?.renderUsageInfo ? (
286
+ settings.customRenderers.render.multipleResultModal.renderUsageInfo(
287
+ {
288
+ used: limitInfo.used_count,
289
+ limit: limitInfo.tryon_limit,
290
+ text: t('product.virtual_try_on.usage_info')
291
+ .replace('{{used}}', String(limitInfo.used_count))
292
+ .replace('{{limit}}', String(limitInfo.tryon_limit))
293
+ }
294
+ )
295
+ ) : (
296
+ <p
297
+ className={twMerge(
298
+ 'text-xs text-gray-500 mt-1',
299
+ settings?.customStyles?.multipleResultModalUsageInfo
300
+ )}
301
+ >
302
+ {t('product.virtual_try_on.usage_info')
303
+ .replace('{{used}}', String(limitInfo.used_count))
304
+ .replace('{{limit}}', String(limitInfo.tryon_limit))}
305
+ </p>
306
+ ))}
280
307
  </div>
281
308
  </div>
282
309
 
@@ -306,7 +333,7 @@ export function VirtualTryOnMultipleResultModal({
306
333
  >
307
334
  {results.map((result, index) => {
308
335
  const product = products[index];
309
- const hasError = hasCompatibilityError(result);
336
+ const hasError = !!result.error;
310
337
  const feedback = feedbackStates[index];
311
338
 
312
339
  return (
@@ -362,7 +389,12 @@ export function VirtualTryOnMultipleResultModal({
362
389
  ?.multipleResultModalErrorMessage
363
390
  )}
364
391
  >
365
- {getErrorMessage(result.error!)}
392
+ {parseVirtualTryOnError(
393
+ result.error?.detail ||
394
+ result.error?.message ||
395
+ result.error?.error ||
396
+ ''
397
+ )}
366
398
  </p>
367
399
  </div>
368
400
  ) : (