@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.
- package/CHANGELOG.md +4 -3
- package/README.md +96 -23
- package/package.json +3 -2
- package/src/data/endpoints.ts +50 -5
- package/src/hooks/use-virtual-try-on-async.ts +115 -43
- package/src/hooks/use-virtual-try-on.ts +49 -28
- package/src/index.ts +5 -0
- package/src/types/index.ts +48 -8
- package/src/utils/polling.ts +6 -3
- package/src/views/basket-async-modal.tsx +38 -0
- package/src/views/basket-virtual-try-on.tsx +107 -9
- package/src/views/main.tsx +63 -19
- package/src/views/virtual-try-on-multiple-modal.tsx +8 -2
- package/src/views/virtual-try-on-multiple-result-modal.tsx +36 -4
- package/src/views/virtual-try-on-product-selector.tsx +5 -1
- package/src/views/virtual-try-on-result-modal.tsx +40 -2
package/src/types/index.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface VirtualTryOnAsyncRequest {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export interface VirtualTryOnAsyncResponse {
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
99
|
+
selected?: string[];
|
|
100
|
+
reference?: string;
|
|
101
101
|
};
|
|
102
102
|
multiple?: {
|
|
103
|
-
reference
|
|
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
|
|
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
|
|
package/src/utils/polling.ts
CHANGED
|
@@ -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
|
|
93
|
+
const rawError =
|
|
93
94
|
typeof response.error === 'string'
|
|
94
95
|
? response.error
|
|
95
|
-
: response.error?.detail ||
|
|
96
|
-
this.config.onError(
|
|
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
|
|
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 {
|
|
20
|
+
import {
|
|
21
|
+
useGetTryOnUsageMutation,
|
|
22
|
+
useGetVirtualTryOnLimitedCategoriesQuery
|
|
23
|
+
} from '../data/endpoints';
|
|
16
24
|
import virtualCabinIcon from '../../static/virtual-cabin-icon.svg';
|
|
17
|
-
import {
|
|
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<
|
|
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 (
|
|
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
|
/>
|
package/src/views/main.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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')} - ${
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
-
{
|
|
392
|
+
{parseVirtualTryOnError(
|
|
393
|
+
result.error?.detail ||
|
|
394
|
+
result.error?.message ||
|
|
395
|
+
result.error?.error ||
|
|
396
|
+
''
|
|
397
|
+
)}
|
|
366
398
|
</p>
|
|
367
399
|
</div>
|
|
368
400
|
) : (
|