payment-kit 1.20.11 → 1.20.13

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.
Files changed (92) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +2 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
  5. package/api/src/integrations/stripe/resource.ts +253 -2
  6. package/api/src/libs/currency.ts +31 -0
  7. package/api/src/libs/discount/coupon.ts +1061 -0
  8. package/api/src/libs/discount/discount.ts +349 -0
  9. package/api/src/libs/discount/nft.ts +239 -0
  10. package/api/src/libs/discount/redemption.ts +636 -0
  11. package/api/src/libs/discount/vc.ts +73 -0
  12. package/api/src/libs/env.ts +1 -0
  13. package/api/src/libs/invoice.ts +44 -10
  14. package/api/src/libs/math-utils.ts +6 -0
  15. package/api/src/libs/price.ts +43 -0
  16. package/api/src/libs/session.ts +242 -57
  17. package/api/src/libs/subscription.ts +2 -6
  18. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  19. package/api/src/libs/vendor-util/adapters/types.ts +1 -0
  20. package/api/src/libs/vendor-util/fulfillment.ts +1 -1
  21. package/api/src/queues/auto-recharge.ts +1 -1
  22. package/api/src/queues/discount-status.ts +200 -0
  23. package/api/src/queues/subscription.ts +98 -5
  24. package/api/src/queues/usage-record.ts +1 -1
  25. package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
  26. package/api/src/queues/vendors/return-processor.ts +184 -0
  27. package/api/src/queues/vendors/return-scanner.ts +119 -0
  28. package/api/src/queues/vendors/status-check.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +5 -3
  30. package/api/src/routes/checkout-sessions.ts +755 -64
  31. package/api/src/routes/connect/change-payment.ts +6 -1
  32. package/api/src/routes/connect/change-plan.ts +6 -1
  33. package/api/src/routes/connect/setup.ts +6 -1
  34. package/api/src/routes/connect/shared.ts +80 -9
  35. package/api/src/routes/connect/subscribe.ts +12 -2
  36. package/api/src/routes/coupons.ts +518 -0
  37. package/api/src/routes/index.ts +4 -0
  38. package/api/src/routes/invoices.ts +44 -3
  39. package/api/src/routes/meter-events.ts +2 -1
  40. package/api/src/routes/payment-currencies.ts +1 -0
  41. package/api/src/routes/promotion-codes.ts +482 -0
  42. package/api/src/routes/subscriptions.ts +23 -2
  43. package/api/src/routes/vendor.ts +89 -2
  44. package/api/src/store/migrations/20250904-discount.ts +136 -0
  45. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  46. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  47. package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
  48. package/api/src/store/models/checkout-session.ts +17 -2
  49. package/api/src/store/models/coupon.ts +144 -4
  50. package/api/src/store/models/discount.ts +23 -10
  51. package/api/src/store/models/index.ts +13 -2
  52. package/api/src/store/models/product-vendor.ts +6 -0
  53. package/api/src/store/models/promotion-code.ts +295 -18
  54. package/api/src/store/models/types.ts +30 -1
  55. package/api/tests/libs/session.spec.ts +48 -27
  56. package/blocklet.yml +1 -1
  57. package/package.json +20 -20
  58. package/src/app.tsx +2 -0
  59. package/src/components/customer/link.tsx +1 -1
  60. package/src/components/discount/discount-info.tsx +178 -0
  61. package/src/components/invoice/table.tsx +140 -48
  62. package/src/components/invoice-pdf/styles.ts +6 -0
  63. package/src/components/invoice-pdf/template.tsx +59 -33
  64. package/src/components/metadata/form.tsx +14 -5
  65. package/src/components/payment-link/actions.tsx +42 -0
  66. package/src/components/price/form.tsx +91 -65
  67. package/src/components/product/vendor-config.tsx +5 -3
  68. package/src/components/promotion/active-redemptions.tsx +534 -0
  69. package/src/components/promotion/currency-multi-select.tsx +350 -0
  70. package/src/components/promotion/currency-restrictions.tsx +117 -0
  71. package/src/components/promotion/product-select.tsx +292 -0
  72. package/src/components/promotion/promotion-code-form.tsx +534 -0
  73. package/src/components/subscription/portal/list.tsx +6 -1
  74. package/src/components/subscription/vendor-service-list.tsx +13 -2
  75. package/src/locales/en.tsx +227 -0
  76. package/src/locales/zh.tsx +222 -1
  77. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  78. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  79. package/src/pages/admin/products/coupons/create.tsx +612 -0
  80. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  81. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  82. package/src/pages/admin/products/coupons/index.tsx +210 -3
  83. package/src/pages/admin/products/index.tsx +22 -3
  84. package/src/pages/admin/products/products/detail.tsx +12 -2
  85. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  86. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  87. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  88. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  89. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  90. package/src/pages/admin/products/vendors/index.tsx +17 -5
  91. package/src/pages/customer/subscription/detail.tsx +5 -0
  92. package/vite.config.ts +4 -3
@@ -0,0 +1,612 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { api, formatError, usePaymentContext, FormInput, FormLabel, Collapse, Switch } from '@blocklet/payment-react';
4
+ import { AddOutlined } from '@mui/icons-material';
5
+ import {
6
+ Button,
7
+ Box,
8
+ FormControl,
9
+ Select,
10
+ MenuItem,
11
+ FormControlLabel,
12
+ Checkbox,
13
+ Typography,
14
+ InputAdornment,
15
+ Divider,
16
+ IconButton,
17
+ } from '@mui/material';
18
+ import { useState, Fragment } from 'react';
19
+ import { FormProvider, useForm, Controller, useFieldArray } from 'react-hook-form';
20
+ import { cloneDeep } from 'lodash';
21
+ import useBus, { dispatch, EventAction } from 'use-bus';
22
+
23
+ import DrawerForm from '../../../../components/drawer-form';
24
+ import PromotionCodeForm, {
25
+ DEFAULT_PROMOTION_CODE,
26
+ PromotionCodeData,
27
+ } from '../../../../components/promotion/promotion-code-form';
28
+ import ProductDynamicSelect from '../../../../components/promotion/product-select';
29
+ import CurrencyMultiSelect from '../../../../components/promotion/currency-multi-select';
30
+ import { ProductsProvider, useProductsContext } from '../../../../contexts/products';
31
+ import CreateProduct from '../../../../components/product/create';
32
+
33
+ // Using PromotionCodeData from the imported component
34
+ type PromotionCode = PromotionCodeData;
35
+
36
+ type CouponFormData = {
37
+ name: string;
38
+ description?: string;
39
+ id?: string;
40
+ type: 'percentage' | 'fixed_amount';
41
+ percent_off?: number;
42
+ amount_off?: string;
43
+ currency_id?: string;
44
+ currency_options?: Array<{
45
+ currency_id: string;
46
+ amount_off: string;
47
+ currency?: any;
48
+ }>;
49
+ duration: 'once' | 'forever' | 'repeating';
50
+ duration_in_months?: number;
51
+ applies_to_products: boolean;
52
+ product_ids?: string[];
53
+ limit_date_range: boolean;
54
+ redeem_by?: string;
55
+ limit_total_number: boolean;
56
+ max_redemptions?: number;
57
+ use_customer_facing_codes: boolean;
58
+ promotion_codes: PromotionCode[];
59
+ };
60
+
61
+ type Props = {
62
+ open?: boolean;
63
+ onClose?: () => void;
64
+ onSubmit?: () => void;
65
+ };
66
+
67
+ function CouponCreateContent({ onClose = () => {}, onSubmit: onSubmitCallback = () => {} }: Omit<Props, 'open'>) {
68
+ const { t } = useLocaleContext();
69
+ const { settings } = usePaymentContext();
70
+ const { refresh } = useProductsContext();
71
+ const [expandedSection, setExpandedSection] = useState<'configuration' | 'codes'>('configuration');
72
+ const [createProductOpen, setCreateProductOpen] = useState(false);
73
+
74
+ const methods = useForm<CouponFormData>({
75
+ mode: 'onChange',
76
+ defaultValues: {
77
+ name: '',
78
+ description: '',
79
+ id: '',
80
+ type: 'percentage',
81
+ percent_off: 10,
82
+ amount_off: '0',
83
+ currency_id: settings.baseCurrency.id,
84
+ currency_options: [],
85
+ duration: 'once',
86
+ duration_in_months: undefined,
87
+ applies_to_products: false,
88
+ product_ids: [],
89
+ limit_date_range: false,
90
+ redeem_by: '',
91
+ limit_total_number: false,
92
+ max_redemptions: undefined,
93
+ use_customer_facing_codes: false,
94
+ promotion_codes: [],
95
+ },
96
+ });
97
+
98
+ const {
99
+ control,
100
+ handleSubmit,
101
+ watch,
102
+ formState: { errors },
103
+ } = methods;
104
+
105
+ const promotionCodes = useFieldArray({ control, name: 'promotion_codes' });
106
+
107
+ // Handle product creation request
108
+ useBus(
109
+ (event: EventAction) => event.type === 'product.create.request',
110
+ () => {
111
+ setCreateProductOpen(true);
112
+ }
113
+ );
114
+
115
+ const onProductCreated = () => {
116
+ setCreateProductOpen(false);
117
+ refresh();
118
+ };
119
+
120
+ const watchType = watch('type');
121
+ const watchDuration = watch('duration');
122
+ const watchUseCustomerFacingCodes = watch('use_customer_facing_codes');
123
+ const watchLimitDateRange = watch('limit_date_range');
124
+ const watchLimitTotalNumber = watch('limit_total_number');
125
+ const watchCurrencyId = watch('currency_id');
126
+ const watchCurrencyOptions = watch('currency_options') || [];
127
+
128
+ const getAvailableCurrencies = () => {
129
+ const allCurrencies = settings.paymentMethods?.flatMap((pm) => pm.payment_currencies) || [];
130
+
131
+ if (watchType === 'fixed_amount') {
132
+ const currencyIds = new Set<string>();
133
+ if (watchCurrencyId) {
134
+ currencyIds.add(watchCurrencyId);
135
+ }
136
+
137
+ watchCurrencyOptions.forEach((option: any) => {
138
+ if (option.currency_id) {
139
+ currencyIds.add(option.currency_id);
140
+ }
141
+ });
142
+
143
+ return allCurrencies.filter((currency) => currencyIds.has(currency.id));
144
+ }
145
+ return allCurrencies;
146
+ };
147
+
148
+ const onSubmit = (data: CouponFormData) => {
149
+ const payload: any = {
150
+ name: data.name,
151
+ duration: data.duration,
152
+ metadata: {},
153
+ };
154
+
155
+ if (data.id) {
156
+ payload.id = data.id;
157
+ }
158
+
159
+ if (data.description) {
160
+ payload.description = data.description;
161
+ }
162
+
163
+ if (data.type === 'percentage') {
164
+ payload.percent_off = data.percent_off;
165
+ } else {
166
+ // For fixed amount, handle base currency
167
+ payload.amount_off = data.amount_off;
168
+ payload.currency_id = data.currency_id;
169
+
170
+ // Handle additional currencies - convert array to object format for backend
171
+ if (data.currency_options && data.currency_options.length > 0) {
172
+ payload.currency_options = {};
173
+ data.currency_options.forEach((option) => {
174
+ if (option.currency_id && option.amount_off) {
175
+ payload.currency_options[option.currency_id] = {
176
+ amount_off: parseFloat(option.amount_off) || 0,
177
+ };
178
+ }
179
+ });
180
+ }
181
+ }
182
+
183
+ // Only include duration_in_months when duration is 'repeating'
184
+ if (data.duration === 'repeating') {
185
+ payload.duration_in_months = data.duration_in_months || 1; // Default to 1 if not specified
186
+ }
187
+
188
+ if (data.limit_total_number && data.max_redemptions) {
189
+ payload.max_redemptions = data.max_redemptions;
190
+ }
191
+
192
+ if (data.limit_date_range && data.redeem_by) {
193
+ payload.redeem_by = Math.floor(new Date(data.redeem_by).getTime() / 1000);
194
+ }
195
+
196
+ if (data.applies_to_products && data.product_ids && data.product_ids.length > 0) {
197
+ payload.applies_to = {
198
+ products: data.product_ids,
199
+ };
200
+ }
201
+
202
+ // Include promotion codes data directly in the coupon creation request
203
+ if (data.use_customer_facing_codes && data.promotion_codes.length > 0) {
204
+ payload.promotion_codes = data.promotion_codes.map((promoCode) => {
205
+ const promoPayload: any = {
206
+ active: promoCode.active,
207
+ verification_type: promoCode.verification_type,
208
+ max_redemptions: promoCode.limit_number_redemptions ? promoCode.max_redemptions : undefined,
209
+ expires_at: promoCode.expires_at ? Math.floor(new Date(promoCode.expires_at).getTime() / 1000) : undefined,
210
+ restrictions: {},
211
+ metadata: {},
212
+ };
213
+
214
+ // Add description if provided
215
+ if (promoCode.description) {
216
+ promoPayload.description = promoCode.description;
217
+ }
218
+
219
+ // Include restrictions directly from promotion code data
220
+ if (promoCode.restrictions) {
221
+ promoPayload.restrictions = promoCode.restrictions;
222
+ }
223
+
224
+ // Only include custom code if provided, otherwise let backend generate
225
+ if (promoCode.code && promoCode.code.trim()) {
226
+ promoPayload.code = promoCode.code;
227
+ }
228
+
229
+ // Add verification configurations based on type
230
+ if (promoCode.verification_type === 'nft') {
231
+ promoPayload.nft_config = {
232
+ addresses: promoCode.nft_addresses?.filter((addr) => addr.trim()) || [],
233
+ tags: promoCode.nft_tags?.filter((tag) => tag.trim()) || [],
234
+ trusted_issuers: promoCode.trusted_issuers?.filter((issuer) => issuer.trim()) || [],
235
+ trusted_parents: promoCode.trusted_parents?.filter((parent) => parent.trim()) || [],
236
+ min_balance: promoCode.min_balance || 1,
237
+ };
238
+ } else if (promoCode.verification_type === 'vc') {
239
+ promoPayload.vc_config = {
240
+ roles: promoCode.vc_roles?.filter((role) => role.trim()) || [],
241
+ trusted_issuers: promoCode.vc_trusted_issuers?.filter((issuer) => issuer.trim()) || [],
242
+ };
243
+ } else if (promoCode.verification_type === 'user_restricted') {
244
+ promoPayload.customer_dids = promoCode.customer_dids?.filter((did) => did.trim()) || [];
245
+ }
246
+
247
+ return promoPayload;
248
+ });
249
+ }
250
+
251
+ api
252
+ .post('/api/coupons', payload)
253
+ .then(() => {
254
+ Toast.success(t('admin.coupon.saved'));
255
+ dispatch('coupon.created');
256
+ methods.reset();
257
+ dispatch('drawer.submitted');
258
+ if (onSubmitCallback) {
259
+ onSubmitCallback();
260
+ }
261
+ if (onClose) {
262
+ onClose();
263
+ }
264
+ })
265
+ .catch((err: any) => {
266
+ console.error(err);
267
+ Toast.error(formatError(err));
268
+ });
269
+ };
270
+
271
+ const handleFormSubmit = handleSubmit(onSubmit);
272
+
273
+ return (
274
+ <>
275
+ <FormProvider {...methods}>
276
+ <Box component="form" id="coupon-form" onSubmit={handleFormSubmit}>
277
+ <Typography
278
+ variant="body2"
279
+ sx={{
280
+ color: 'text.secondary',
281
+ mb: 3,
282
+ }}>
283
+ {t('admin.coupon.description')}
284
+ </Typography>
285
+
286
+ <Collapse
287
+ expanded={expandedSection === 'configuration'}
288
+ value="configuration"
289
+ card
290
+ onChange={(_, expanded) => {
291
+ if (expanded) {
292
+ setExpandedSection('configuration');
293
+ }
294
+ }}
295
+ trigger={t('admin.coupon.couponConfiguration')}>
296
+ <Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
297
+ <FormInput
298
+ name="name"
299
+ label={t('admin.coupon.name')}
300
+ required
301
+ placeholder="First purchase discount"
302
+ helperText={errors?.name?.message || t('admin.coupon.nameHelp')}
303
+ rules={{ required: t('common.required') }}
304
+ />
305
+
306
+ <FormInput
307
+ name="description"
308
+ label={t('admin.coupon.desc')}
309
+ placeholder={t('admin.coupon.desc')}
310
+ multiline
311
+ minRows={2}
312
+ maxRows={4}
313
+ slotProps={{
314
+ htmlInput: {
315
+ maxLength: 500,
316
+ },
317
+ }}
318
+ />
319
+
320
+ <FormInput name="id" label={t('admin.coupon.idOptional')} helperText={t('admin.coupon.idHelp')} />
321
+
322
+ <Box>
323
+ <FormLabel required>{t('admin.coupon.type')}</FormLabel>
324
+ <Controller
325
+ name="type"
326
+ control={control}
327
+ render={({ field }) => (
328
+ <FormControl fullWidth size="small">
329
+ <Select {...field}>
330
+ <MenuItem value="percentage">{t('admin.coupon.percentageOff')}</MenuItem>
331
+ <MenuItem value="fixed_amount">{t('admin.coupon.fixedAmountOff')}</MenuItem>
332
+ </Select>
333
+ </FormControl>
334
+ )}
335
+ />
336
+ </Box>
337
+
338
+ {watchType === 'percentage' ? (
339
+ <FormInput
340
+ name="percent_off"
341
+ label={t('admin.coupon.percentageDiscount')}
342
+ type="number"
343
+ required
344
+ rules={{
345
+ min: 0,
346
+ max: 100,
347
+ validate: (v) => {
348
+ if (!v && v !== 0) {
349
+ return t('common.required');
350
+ }
351
+ if (Number(v) <= 0 || Number(v) > 100) {
352
+ return t('common.invalid');
353
+ }
354
+ return true;
355
+ },
356
+ }}
357
+ slotProps={{
358
+ input: {
359
+ endAdornment: <InputAdornment position="end">%</InputAdornment>,
360
+ },
361
+ htmlInput: {
362
+ min: 0,
363
+ max: 100,
364
+ precision: 2,
365
+ },
366
+ }}
367
+ />
368
+ ) : (
369
+ <Box>
370
+ <FormLabel required>{t('admin.coupon.fixedAmount')}</FormLabel>
371
+ <CurrencyMultiSelect
372
+ baseCurrencyFieldName="currency_id"
373
+ currencyOptionsFieldName="currency_options"
374
+ unitAmountFieldName="amount_off"
375
+ />
376
+ </Box>
377
+ )}
378
+
379
+ <Box sx={{ pl: 2 }}>
380
+ <Controller
381
+ name="applies_to_products"
382
+ control={control}
383
+ render={({ field }) => (
384
+ <FormControlLabel
385
+ control={<Switch {...field} checked={field.value} />}
386
+ label={t('admin.coupon.applyToProducts')}
387
+ slotProps={{
388
+ typography: {
389
+ sx: {
390
+ marginLeft: '8px',
391
+ },
392
+ },
393
+ }}
394
+ />
395
+ )}
396
+ />
397
+ </Box>
398
+
399
+ {watch('applies_to_products') && (
400
+ <Box>
401
+ <FormLabel>{t('admin.coupon.selectProducts')}</FormLabel>
402
+ <Controller
403
+ name="product_ids"
404
+ control={control}
405
+ render={({ field }) => <ProductDynamicSelect value={field.value || []} onChange={field.onChange} />}
406
+ />
407
+ </Box>
408
+ )}
409
+
410
+ <Box>
411
+ <Controller
412
+ name="duration"
413
+ control={control}
414
+ render={({ field }) => (
415
+ <FormControl fullWidth size="small">
416
+ <FormLabel required>{t('admin.coupon.duration')}</FormLabel>
417
+ <Select {...field}>
418
+ <MenuItem value="once">{t('admin.coupon.once')}</MenuItem>
419
+ <MenuItem value="forever">{t('admin.coupon.forever')}</MenuItem>
420
+ <MenuItem value="repeating">{t('admin.coupon.repeating')}</MenuItem>
421
+ </Select>
422
+ </FormControl>
423
+ )}
424
+ />
425
+ <Typography
426
+ variant="body2"
427
+ sx={{
428
+ color: 'text.secondary',
429
+ mt: 1,
430
+ }}>
431
+ {t('admin.coupon.durationHelp')}
432
+ </Typography>
433
+ </Box>
434
+
435
+ {watchDuration === 'repeating' && (
436
+ <FormInput
437
+ name="duration_in_months"
438
+ label={t('admin.coupon.durationInMonths')}
439
+ type="number"
440
+ rules={{ required: 'Duration in months is required', min: 1 }}
441
+ slotProps={{
442
+ input: {
443
+ endAdornment: <InputAdornment position="end">{t('admin.coupon.months')}</InputAdornment>,
444
+ },
445
+ }}
446
+ />
447
+ )}
448
+
449
+ <Divider />
450
+
451
+ <Typography variant="h6">{t('admin.coupon.redemptionLimits')}</Typography>
452
+
453
+ <Controller
454
+ name="limit_date_range"
455
+ control={control}
456
+ render={({ field }) => (
457
+ <FormControlLabel
458
+ control={<Checkbox {...field} checked={field.value} />}
459
+ label={t('admin.coupon.limitDateRange')}
460
+ />
461
+ )}
462
+ />
463
+
464
+ {watchLimitDateRange && (
465
+ <FormInput name="redeem_by" label="" type="datetime-local" InputLabelProps={{ shrink: true }} />
466
+ )}
467
+
468
+ <Controller
469
+ name="limit_total_number"
470
+ control={control}
471
+ render={({ field }) => (
472
+ <FormControlLabel
473
+ control={<Checkbox {...field} checked={field.value} />}
474
+ label={t('admin.coupon.limitTotalNumber')}
475
+ />
476
+ )}
477
+ />
478
+
479
+ {watchLimitTotalNumber && (
480
+ <FormInput
481
+ name="max_redemptions"
482
+ label={t('admin.coupon.maxRedemptions')}
483
+ type="number"
484
+ slotProps={{
485
+ input: {
486
+ endAdornment: <InputAdornment position="end">{t('admin.coupon.times')}</InputAdornment>,
487
+ },
488
+ }}
489
+ />
490
+ )}
491
+ </Box>
492
+ </Collapse>
493
+
494
+ <Collapse
495
+ expanded={expandedSection === 'codes'}
496
+ value="codes"
497
+ card
498
+ onChange={(_, expanded) => {
499
+ if (expanded) {
500
+ setExpandedSection('codes');
501
+ }
502
+ }}
503
+ trigger={t('admin.coupon.promotionCodes')}
504
+ style={{
505
+ marginTop: 2,
506
+ }}>
507
+ <Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
508
+ <Controller
509
+ name="use_customer_facing_codes"
510
+ control={control}
511
+ render={({ field }) => (
512
+ <FormControlLabel
513
+ control={<Checkbox {...field} checked={field.value} />}
514
+ label={t('admin.coupon.useCustomerFacingCodes')}
515
+ />
516
+ )}
517
+ />
518
+
519
+ {watchUseCustomerFacingCodes && (
520
+ <Box>
521
+ <Typography
522
+ variant="body2"
523
+ sx={{
524
+ color: 'text.secondary',
525
+ mb: 2,
526
+ }}>
527
+ {t('admin.coupon.codeHelp')}
528
+ </Typography>
529
+
530
+ {promotionCodes.fields.map((promoCode, index) => (
531
+ <Fragment key={promoCode.id}>
532
+ <Collapse
533
+ expanded
534
+ style={{ fontWeight: 'bold', width: '100%' }}
535
+ addons={
536
+ <IconButton size="small" onClick={() => promotionCodes.remove(index)} color="error">
537
+ <AddOutlined style={{ transform: 'rotate(45deg)' }} />
538
+ </IconButton>
539
+ }
540
+ trigger={`${t('admin.coupon.promotionCodes')} ${index + 1}: ${methods.watch(`promotion_codes.${index}.code`) || t('admin.coupon.newCode')}`}>
541
+ <Box sx={{ width: '100%', p: 1 }}>
542
+ <PromotionCodeForm
543
+ prefix={`promotion_codes.${index}`}
544
+ showCodeInput
545
+ showExpandedOptions
546
+ availableCurrencies={getAvailableCurrencies()}
547
+ />
548
+ </Box>
549
+ </Collapse>
550
+ <Divider sx={{ mt: 2, mb: 2 }} />
551
+ </Fragment>
552
+ ))}
553
+
554
+ <Button
555
+ variant="outlined"
556
+ color="primary"
557
+ sx={{
558
+ color: 'text.primary',
559
+ width: '100%',
560
+ }}
561
+ onClick={() => {
562
+ const newCode = cloneDeep(DEFAULT_PROMOTION_CODE);
563
+ promotionCodes.append(newCode);
564
+ }}>
565
+ <AddOutlined fontSize="small" /> {t('admin.coupon.addAnotherCode')}
566
+ </Button>
567
+ </Box>
568
+ )}
569
+
570
+ {!watchUseCustomerFacingCodes && (
571
+ <Typography
572
+ variant="body2"
573
+ sx={{
574
+ color: 'text.secondary',
575
+ }}>
576
+ {t('admin.coupon.promotionCodesHelp')}
577
+ </Typography>
578
+ )}
579
+ </Box>
580
+ </Collapse>
581
+ </Box>
582
+ </FormProvider>
583
+ {createProductOpen && <CreateProduct onCancel={() => setCreateProductOpen(false)} onSave={onProductCreated} />}
584
+ </>
585
+ );
586
+ }
587
+
588
+ export default function CouponCreate({
589
+ open = false,
590
+ onClose = () => {},
591
+ onSubmit: onSubmitCallback = () => {},
592
+ }: Props) {
593
+ const { t } = useLocaleContext();
594
+
595
+ return (
596
+ <DrawerForm
597
+ icon={<AddOutlined />}
598
+ text={t('admin.coupon.create')}
599
+ open={open}
600
+ onClose={onClose}
601
+ width={640}
602
+ addons={
603
+ <Button variant="contained" size="small" form="coupon-form" type="submit">
604
+ {t('admin.coupon.create')}
605
+ </Button>
606
+ }>
607
+ <ProductsProvider>
608
+ <CouponCreateContent onClose={onClose} onSubmit={onSubmitCallback} />
609
+ </ProductsProvider>
610
+ </DrawerForm>
611
+ );
612
+ }