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,534 @@
1
+ /* eslint-disable react/display-name, react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { api, formatTime, Link, Table } from '@blocklet/payment-react';
4
+ import { Box, Tab, Tabs, Typography, Chip } from '@mui/material';
5
+ import { useRequest } from 'ahooks';
6
+ import { useState } from 'react';
7
+ import { useNavigate } from 'react-router-dom';
8
+ import CustomerLink from '../customer/link';
9
+ import SubscriptionDescription from '../subscription/description';
10
+ import SubscriptionStatus from '../subscription/status';
11
+
12
+ interface Props {
13
+ couponId?: string;
14
+ promotionCodeId?: string;
15
+ }
16
+
17
+ // Type guard to ensure at least one ID is provided
18
+ function validateProps(props: Props): asserts props is Props & ({ couponId: string } | { promotionCodeId: string }) {
19
+ if ((!props.couponId || props.couponId === '') && (!props.promotionCodeId || props.promotionCodeId === '')) {
20
+ throw new Error('Either couponId or promotionCodeId must be provided');
21
+ }
22
+ }
23
+
24
+ interface PaginationParams {
25
+ page: number;
26
+ pageSize: number;
27
+ }
28
+
29
+ const fetchActiveRedemptions = (
30
+ id: string,
31
+ params: PaginationParams,
32
+ type?: 'customer' | 'subscription',
33
+ entityType: 'coupon' | 'promotion-code' = 'coupon'
34
+ ): Promise<any> => {
35
+ const query = new URLSearchParams({
36
+ page: params.page.toString(),
37
+ pageSize: params.pageSize.toString(),
38
+ });
39
+
40
+ if (type) {
41
+ query.append('type', type);
42
+ }
43
+
44
+ const endpoint =
45
+ entityType === 'coupon' ? `/api/coupons/${id}/redemptions` : `/api/promotion-codes/${id}/redemptions`;
46
+ return api.get(`${endpoint}?${query}`).then((res) => res.data);
47
+ };
48
+
49
+ export default function ActiveRedemptions({ couponId = '', promotionCodeId = '' }: Props) {
50
+ // Validate props at the beginning
51
+ const props = { couponId, promotionCodeId };
52
+ validateProps(props);
53
+ const { t } = useLocaleContext();
54
+ const navigate = useNavigate();
55
+ const [tabValue, setTabValue] = useState(0);
56
+ const [customerPagination, setCustomerPagination] = useState({ page: 0, pageSize: 10 });
57
+ const [subscriptionPagination, setSubscriptionPagination] = useState({ page: 0, pageSize: 10 });
58
+
59
+ // Determine which ID and entity type to use
60
+ const id = couponId && couponId !== '' ? couponId : promotionCodeId!; // Safe to use ! after validation
61
+ const entityType: 'coupon' | 'promotion-code' = couponId && couponId !== '' ? 'coupon' : 'promotion-code';
62
+
63
+ // Fetch data based on current tab and pagination
64
+ const currentPagination = tabValue === 0 ? customerPagination : subscriptionPagination;
65
+ const currentType = tabValue === 0 ? 'customer' : 'subscription';
66
+
67
+ const { data = { customers: [], subscriptions: [], count: 0 }, loading } = useRequest(
68
+ () =>
69
+ fetchActiveRedemptions(
70
+ id,
71
+ {
72
+ page: currentPagination.page + 1, // API pages start from 1, table pages start from 0
73
+ pageSize: currentPagination.pageSize,
74
+ },
75
+ currentType,
76
+ entityType
77
+ ),
78
+ {
79
+ refreshDeps: [id, currentPagination.page, currentPagination.pageSize, tabValue],
80
+ }
81
+ );
82
+
83
+ const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
84
+ setTabValue(newValue);
85
+ // Reset pagination when switching tabs
86
+ if (newValue === 0) {
87
+ setCustomerPagination({ page: 0, pageSize: 10 });
88
+ } else {
89
+ setSubscriptionPagination({ page: 0, pageSize: 10 });
90
+ }
91
+ };
92
+
93
+ const handleCustomerTableChange = ({ page, rowsPerPage }: any) => {
94
+ setCustomerPagination({ page, pageSize: rowsPerPage });
95
+ };
96
+
97
+ const handleSubscriptionTableChange = ({ page, rowsPerPage }: any) => {
98
+ setSubscriptionPagination({ page, pageSize: rowsPerPage });
99
+ };
100
+
101
+ // Build customer columns dynamically based on entity type
102
+ const buildCustomerColumns = () => {
103
+ const baseColumns = [
104
+ {
105
+ label: t('common.name'),
106
+ name: 'name',
107
+ options: {
108
+ customBodyRenderLite: (_: string, index: number) => {
109
+ const customer = data.customers?.[index];
110
+ return <CustomerLink customer={customer} size="small" />;
111
+ },
112
+ },
113
+ },
114
+ {
115
+ label: t('common.email'),
116
+ name: 'email',
117
+ options: {
118
+ customBodyRenderLite: (_: string, index: number) => {
119
+ const customer = data.customers?.[index];
120
+ return customer?.email || '—';
121
+ },
122
+ },
123
+ },
124
+ {
125
+ label: t('admin.coupon.usageStats'),
126
+ name: 'usage_stats',
127
+ options: {
128
+ customBodyRenderLite: (_: string, index: number) => {
129
+ const customer = data.customers?.[index];
130
+ const stats = customer?.coupon_usage_stats;
131
+ if (!stats) return '—';
132
+ return <Typography variant="body2">{stats.total_discount_records}</Typography>;
133
+ },
134
+ },
135
+ },
136
+ ];
137
+
138
+ // Only show promotion codes column when in coupon mode (not in promotion code detail page)
139
+ if (entityType === 'coupon') {
140
+ baseColumns.push({
141
+ label: t('admin.coupon.promotionCodesUsed'),
142
+ name: 'promotion_codes',
143
+ options: {
144
+ customBodyRenderLite: (_: string, index: number) => {
145
+ const customer = data.customers?.[index];
146
+ const promoCodes = customer?.coupon_usage_stats?.promotion_codes_used || [];
147
+ const discountRecords = customer?.discount_records || [];
148
+
149
+ if (promoCodes.length === 0) return '—';
150
+
151
+ // Calculate usage count for each promotion code from actual discount records
152
+ const promoUsageMap = new Map();
153
+ discountRecords.forEach((record: any) => {
154
+ if (record.promotion_code) {
155
+ const key = record.promotion_code.id || record.promotion_code.code;
156
+ promoUsageMap.set(key, (promoUsageMap.get(key) || 0) + 1);
157
+ }
158
+ });
159
+
160
+ return (
161
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, maxWidth: '200px' }}>
162
+ {promoCodes.map((promo: any) => {
163
+ // Use actual discount record count instead of promo.times_redeemed
164
+ const actualUsageCount = promoUsageMap.get(promo.id || promo.code) || 0;
165
+ return (
166
+ <Chip
167
+ key={`${promo.id || promo.code}`}
168
+ label={`${promo.code} (${actualUsageCount})`}
169
+ size="small"
170
+ variant="outlined"
171
+ onClick={() => navigate(`/admin/products/promotion-codes/${promo.id}`)}
172
+ sx={{
173
+ cursor: 'pointer',
174
+ '&:hover': {
175
+ backgroundColor: 'action.hover',
176
+ },
177
+ }}
178
+ />
179
+ );
180
+ })}
181
+ </Box>
182
+ );
183
+ },
184
+ },
185
+ });
186
+ }
187
+
188
+ // Add total savings and usage period columns
189
+ baseColumns.push(
190
+ {
191
+ label: t('admin.coupon.totalSavings'),
192
+ name: 'total_savings',
193
+ options: {
194
+ customBodyRenderLite: (_: string, index: number) => {
195
+ const customer = data.customers?.[index];
196
+ const totalSavingsByCurrency = (customer?.coupon_usage_stats?.total_savings || {}) as Record<
197
+ string,
198
+ { formattedAmount: string }
199
+ >;
200
+
201
+ // If no savings data, return dash
202
+ if (Object.keys(totalSavingsByCurrency).length === 0) {
203
+ return <Typography variant="body2">—</Typography>;
204
+ }
205
+
206
+ // Display savings by currency
207
+ return (
208
+ <Box>
209
+ {Object.entries(totalSavingsByCurrency).map(([currencyId, value], i) => {
210
+ if (!value) {
211
+ return null;
212
+ }
213
+ return (
214
+ <Typography key={currencyId} variant="body2" sx={{ display: 'inline-flex' }}>
215
+ {i > 0 && '、'}
216
+ {value.formattedAmount}
217
+ </Typography>
218
+ );
219
+ })}
220
+ </Box>
221
+ );
222
+ },
223
+ },
224
+ },
225
+ {
226
+ label: t('admin.coupon.usagePeriod'),
227
+ name: 'usage_period',
228
+ options: {
229
+ customBodyRenderLite: (_: string, index: number) => {
230
+ const customer = data.customers?.[index];
231
+ const stats = customer?.coupon_usage_stats;
232
+ if (!stats) return '—';
233
+ return (
234
+ <Box>
235
+ <Typography variant="body2">
236
+ {t('admin.coupon.firstUsed')}: {formatTime(stats.first_used)}
237
+ </Typography>
238
+ {stats.first_used !== stats.last_used && (
239
+ <Typography
240
+ variant="body2"
241
+ sx={{
242
+ color: 'text.secondary',
243
+ }}>
244
+ {t('admin.coupon.lastUsed')}: {formatTime(stats.last_used)}
245
+ </Typography>
246
+ )}
247
+ </Box>
248
+ );
249
+ },
250
+ },
251
+ }
252
+ );
253
+
254
+ return baseColumns;
255
+ };
256
+
257
+ const customerColumns = buildCustomerColumns();
258
+
259
+ // Build subscription columns dynamically based on entity type
260
+ const buildSubscriptionColumns = () => {
261
+ const baseColumns = [
262
+ {
263
+ label: t('admin.subscription.product'),
264
+ name: 'id',
265
+ options: {
266
+ customBodyRenderLite: (_: string, index: number) => {
267
+ const subscription = data.subscriptions?.[index];
268
+ return (
269
+ <Link to={`/admin/billing/${subscription.id}`}>
270
+ <SubscriptionDescription subscription={subscription} />
271
+ </Link>
272
+ );
273
+ },
274
+ },
275
+ },
276
+ {
277
+ label: t('common.status'),
278
+ name: 'status',
279
+ options: {
280
+ customBodyRenderLite: (_: string, index: number) => {
281
+ const subscription = data.subscriptions?.[index];
282
+ return (
283
+ <Link to={`/admin/billing/${subscription.id}`}>
284
+ <SubscriptionStatus subscription={subscription} />
285
+ </Link>
286
+ );
287
+ },
288
+ },
289
+ },
290
+ {
291
+ label: t('common.customer'),
292
+ name: 'customer_info',
293
+ options: {
294
+ customBodyRenderLite: (_: string, index: number) => {
295
+ const subscription = data.subscriptions?.[index];
296
+ const customer = subscription?.customer;
297
+ return <CustomerLink customer={customer} size="small" />;
298
+ },
299
+ },
300
+ },
301
+ ];
302
+
303
+ // Only show promotion codes column when in coupon mode
304
+ if (entityType === 'coupon') {
305
+ baseColumns.push({
306
+ label: t('admin.coupon.promotionCodesUsed'),
307
+ name: 'promotion_code_info',
308
+ options: {
309
+ customBodyRenderLite: (_: string, index: number) => {
310
+ const subscription = data.subscriptions?.[index];
311
+ const promoCode = subscription?.discount_info?.promotion_code;
312
+ if (!promoCode) {
313
+ return (
314
+ <Typography
315
+ variant="body2"
316
+ sx={{
317
+ color: 'text.secondary',
318
+ }}>
319
+ {t('admin.coupon.directCouponUsage')}
320
+ </Typography>
321
+ );
322
+ }
323
+ return (
324
+ <Box>
325
+ <Chip
326
+ label={promoCode.code}
327
+ size="small"
328
+ variant="outlined"
329
+ onClick={() => navigate(`/admin/products/promotion-codes/${promoCode.id}`)}
330
+ sx={{
331
+ cursor: 'pointer',
332
+ mb: 0.5,
333
+ '&:hover': {
334
+ backgroundColor: 'action.hover',
335
+ },
336
+ }}
337
+ />
338
+ </Box>
339
+ );
340
+ },
341
+ },
342
+ });
343
+ }
344
+
345
+ // Add total savings, discount info, and created columns
346
+ baseColumns.push(
347
+ {
348
+ label: t('admin.coupon.totalSavings'),
349
+ name: 'total_savings',
350
+ options: {
351
+ customBodyRenderLite: (_: string, index: number) => {
352
+ const subscription = data.subscriptions?.[index];
353
+ const totalSavingsByCurrency = (subscription?.discount_info?.total_savings || {}) as Record<
354
+ string,
355
+ { formattedAmount: string }
356
+ >;
357
+
358
+ // If no savings data, return dash
359
+ if (Object.keys(totalSavingsByCurrency).length === 0) {
360
+ return (
361
+ <Typography variant="body2" sx={{ color: 'text.lighter' }}>
362
+
363
+ </Typography>
364
+ );
365
+ }
366
+
367
+ // Display savings by currency
368
+ return (
369
+ <Box>
370
+ {Object.entries(totalSavingsByCurrency).map(([currencyId, value], i) => {
371
+ if (!value) {
372
+ return null;
373
+ }
374
+ return (
375
+ <Typography key={currencyId} variant="body2" sx={{ lineHeight: 1.2 }}>
376
+ {i > 0 && ', '}
377
+ {value.formattedAmount}
378
+ </Typography>
379
+ );
380
+ })}
381
+ </Box>
382
+ );
383
+ },
384
+ },
385
+ },
386
+ {
387
+ label: t('admin.coupon.discountInfo'),
388
+ name: 'discount_info',
389
+ options: {
390
+ customBodyRenderLite: (_: string, index: number) => {
391
+ const subscription = data.subscriptions?.[index];
392
+ const discountInfo = subscription?.discount_info;
393
+ if (!discountInfo) return <Typography variant="body2">—</Typography>;
394
+
395
+ const couponInfo = discountInfo.coupon_info;
396
+ return (
397
+ <Box>
398
+ <Typography variant="body2">
399
+ {t('admin.coupon.discountPeriod')}: {formatTime(discountInfo.discount_start * 1000)}
400
+ {discountInfo.discount_end && ` - ${formatTime(discountInfo.discount_end * 1000)}`}
401
+ </Typography>
402
+ <Typography
403
+ variant="caption"
404
+ sx={{
405
+ color: 'text.secondary',
406
+ }}>
407
+ {t('admin.coupon.duration')}:{' '}
408
+ {t(`admin.coupon.couponTermsDuration.${couponInfo.duration}`, {
409
+ months: couponInfo.duration_in_months,
410
+ })}
411
+ </Typography>
412
+ </Box>
413
+ );
414
+ },
415
+ },
416
+ },
417
+ {
418
+ label: t('common.created'),
419
+ name: 'created_at',
420
+ options: {
421
+ customBodyRenderLite: (_: string, index: number) => {
422
+ const subscription = data.subscriptions?.[index];
423
+ return formatTime(subscription?.created_at);
424
+ },
425
+ },
426
+ }
427
+ );
428
+
429
+ return baseColumns;
430
+ };
431
+
432
+ const subscriptionColumns = buildSubscriptionColumns();
433
+
434
+ return (
435
+ <Box>
436
+ <Tabs
437
+ value={tabValue}
438
+ onChange={handleTabChange}
439
+ sx={{
440
+ flex: '1 0 auto',
441
+ maxWidth: '100%',
442
+ fontSize: 14,
443
+ borderBottom: 1,
444
+ borderColor: 'divider',
445
+ '.Mui-selected': {
446
+ fontSize: '14px !important',
447
+ color: 'primary.main',
448
+ },
449
+ }}>
450
+ <Tab label={t('admin.coupon.customers')} />
451
+ <Tab label={t('admin.coupon.subscriptions')} />
452
+ </Tabs>
453
+ {tabValue === 0 && (
454
+ <Box sx={{ mt: 2 }}>
455
+ {data.customers?.length > 0 ? (
456
+ <Table
457
+ data={data.customers}
458
+ columns={customerColumns}
459
+ loading={loading}
460
+ toolbar={false}
461
+ options={{
462
+ selectableRows: 'none',
463
+ serverSide: true,
464
+ count: data.count || 0,
465
+ page: customerPagination.page,
466
+ rowsPerPage: customerPagination.pageSize,
467
+ rowsPerPageOptions: [5, 10, 25, 50],
468
+ }}
469
+ onChange={handleCustomerTableChange}
470
+ />
471
+ ) : (
472
+ !loading && (
473
+ <Box sx={{ textAlign: 'center', py: 4 }}>
474
+ <Typography
475
+ variant="h6"
476
+ sx={{
477
+ color: 'text.secondary',
478
+ }}>
479
+ {t('admin.coupon.noCustomersFound')}
480
+ </Typography>
481
+ <Typography
482
+ variant="body2"
483
+ sx={{
484
+ color: 'text.secondary',
485
+ }}>
486
+ {t('admin.coupon.noCustomersRedeem')}
487
+ </Typography>
488
+ </Box>
489
+ )
490
+ )}
491
+ </Box>
492
+ )}
493
+ {tabValue === 1 && (
494
+ <Box>
495
+ {data.subscriptions?.length > 0 ? (
496
+ <Table
497
+ data={data.subscriptions}
498
+ columns={subscriptionColumns}
499
+ loading={loading}
500
+ options={{
501
+ selectableRows: 'none',
502
+ serverSide: true,
503
+ count: data.count || 0,
504
+ page: subscriptionPagination.page,
505
+ rowsPerPage: subscriptionPagination.pageSize,
506
+ rowsPerPageOptions: [5, 10, 25, 50],
507
+ }}
508
+ onChange={handleSubscriptionTableChange}
509
+ />
510
+ ) : (
511
+ !loading && (
512
+ <Box sx={{ textAlign: 'center', py: 4 }}>
513
+ <Typography
514
+ variant="h6"
515
+ sx={{
516
+ color: 'text.secondary',
517
+ }}>
518
+ {t('admin.coupon.noSubscriptionsFound')}
519
+ </Typography>
520
+ <Typography
521
+ variant="body2"
522
+ sx={{
523
+ color: 'text.secondary',
524
+ }}>
525
+ {t('admin.coupon.noSubscriptionsRedeem')}
526
+ </Typography>
527
+ </Box>
528
+ )
529
+ )}
530
+ </Box>
531
+ )}
532
+ </Box>
533
+ );
534
+ }