payment-kit 1.18.38 → 1.18.40

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 (41) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +22 -0
  2. package/api/src/integrations/stripe/handlers/payment-intent.ts +9 -1
  3. package/api/src/integrations/stripe/handlers/subscription.ts +137 -1
  4. package/api/src/libs/subscription.ts +107 -97
  5. package/api/src/queues/payment.ts +4 -0
  6. package/api/src/routes/checkout-sessions.ts +3 -1
  7. package/api/src/routes/subscriptions.ts +27 -18
  8. package/blocklet.yml +1 -1
  9. package/package.json +19 -19
  10. package/scripts/sdk.js +27 -1
  11. package/src/components/customer/link.tsx +6 -0
  12. package/src/components/info-card.tsx +2 -2
  13. package/src/components/info-metric.tsx +1 -1
  14. package/src/components/metadata/list.tsx +4 -2
  15. package/src/components/subscription/description.tsx +8 -6
  16. package/src/components/subscription/portal/actions.tsx +224 -45
  17. package/src/components/subscription/portal/list.tsx +153 -74
  18. package/src/components/subscription/status.tsx +17 -5
  19. package/src/locales/en.tsx +12 -7
  20. package/src/locales/zh.tsx +6 -2
  21. package/src/pages/admin/billing/invoices/detail.tsx +1 -5
  22. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -5
  23. package/src/pages/admin/customers/customers/detail.tsx +1 -5
  24. package/src/pages/admin/developers/events/detail.tsx +1 -5
  25. package/src/pages/admin/developers/index.tsx +1 -1
  26. package/src/pages/admin/developers/webhooks/detail.tsx +1 -3
  27. package/src/pages/admin/overview.tsx +4 -4
  28. package/src/pages/admin/payments/intents/detail.tsx +5 -6
  29. package/src/pages/admin/payments/payouts/detail.tsx +1 -5
  30. package/src/pages/admin/payments/refunds/detail.tsx +1 -5
  31. package/src/pages/admin/products/links/detail.tsx +1 -5
  32. package/src/pages/admin/products/prices/detail.tsx +1 -5
  33. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -5
  34. package/src/pages/admin/products/products/detail.tsx +1 -5
  35. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
  36. package/src/pages/customer/index.tsx +67 -138
  37. package/src/pages/customer/payout/detail.tsx +37 -49
  38. package/src/pages/customer/subscription/change-payment.tsx +2 -35
  39. package/src/pages/customer/subscription/detail.tsx +5 -6
  40. package/src/pages/integrations/donations/index.tsx +1 -1
  41. package/src/pages/integrations/overview.tsx +1 -1
package/scripts/sdk.js CHANGED
@@ -196,6 +196,32 @@ const checkoutModule = {
196
196
  console.log('createCheckoutWithSettingId', checkoutSession);
197
197
  return { setting, checkoutSession };
198
198
  },
199
+
200
+ async createCheckoutWithServiceAction() {
201
+ const checkoutSession = await payment.checkout.sessions.create({
202
+ mode: 'subscription',
203
+ line_items: [{ price_id: 'price_fQFIS12yi0JR3KePLmitjrhA', quantity: 1 }],
204
+ subscription_data: {
205
+ service_actions: [
206
+ {
207
+ text: {
208
+ zh: '全链接测试',
209
+ en: 'Full Link Test',
210
+ },
211
+ link: 'https://baidu.com',
212
+ },
213
+ {
214
+ text: {
215
+ zh: '相对路径访问',
216
+ en: 'Relative Path Access',
217
+ },
218
+ link: '/.well-known/service/user/notifications',
219
+ },
220
+ ],
221
+ },
222
+ });
223
+ console.log('createCheckoutWithServiceAction', checkoutSession);
224
+ },
199
225
  };
200
226
 
201
227
  const paymentModule = {
@@ -515,7 +541,6 @@ const subscriptionModule = {
515
541
  },
516
542
  };
517
543
 
518
- // 测试模块注册
519
544
  const testModules = {
520
545
  checkout: checkoutModule,
521
546
  payment: paymentModule,
@@ -523,6 +548,7 @@ const testModules = {
523
548
  subscription: subscriptionModule,
524
549
  };
525
550
 
551
+ // 测试入口
526
552
  async function runTest() {
527
553
  payment.environments.setTestMode(true);
528
554
  await testModules.checkout.createBatchSubscriptionWithCustomField();
@@ -2,18 +2,21 @@ import type { TCustomer } from '@blocklet/payment-types';
2
2
  import { Link } from 'react-router-dom';
3
3
  import UserCard from '@arcblock/ux/lib/UserCard';
4
4
  import { getCustomerAvatar } from '@blocklet/payment-react';
5
+ import { InfoType, UserCardProps } from '@arcblock/ux/lib/UserCard/types';
5
6
 
6
7
  export default function CustomerLink({
7
8
  customer,
8
9
  linked,
9
10
  linkTo,
10
11
  size,
12
+ cardProps,
11
13
  }: {
12
14
  customer: TCustomer;
13
15
  linked?: boolean;
14
16
  linkTo?: string;
15
17
  size?: 'default' | 'small';
16
18
  tooltip?: boolean;
19
+ cardProps?: UserCardProps;
17
20
  }) {
18
21
  if (!customer) {
19
22
  return null;
@@ -31,6 +34,7 @@ export default function CustomerLink({
31
34
  avatarProps={{
32
35
  size: size === 'small' ? 24 : 40,
33
36
  }}
37
+ popupInfoType={InfoType.Minimal}
34
38
  showDid={size !== 'small'}
35
39
  {...(customer.metadata.anonymous === true
36
40
  ? {
@@ -45,6 +49,7 @@ export default function CustomerLink({
45
49
  },
46
50
  }
47
51
  : {})}
52
+ {...cardProps}
48
53
  />
49
54
  );
50
55
  if (linked) {
@@ -59,4 +64,5 @@ CustomerLink.defaultProps = {
59
64
  linkTo: '',
60
65
  size: 'default',
61
66
  tooltip: true,
67
+ cardProps: {},
62
68
  };
@@ -41,11 +41,11 @@ export default function InfoCard(props: Props) {
41
41
  wordBreak: getWordBreakStyle(props.name),
42
42
  minWidth: 140,
43
43
  }}>
44
- <Typography variant="body1" color="text.primary" component="div">
44
+ <Typography variant="body2" color="text.primary" component="div">
45
45
  {props.name}
46
46
  </Typography>
47
47
  {props.description && (
48
- <Typography variant="subtitle1" color="text.secondary">
48
+ <Typography variant="subtitle2" color="text.secondary">
49
49
  {props.description}
50
50
  </Typography>
51
51
  )}
@@ -16,7 +16,7 @@ export default function InfoMetric(props: Props) {
16
16
  return (
17
17
  <>
18
18
  <Stack direction="column" alignItems="flex-start">
19
- <Typography component="div" variant="body1" mb={1} color="text.primary" sx={{ fontWeight: 500 }}>
19
+ <Typography component="div" variant="subtitle2" mb={1} color="text.primary">
20
20
  {props.label}
21
21
  {!!props.tip && (
22
22
  <Tooltip title={props.tip}>
@@ -18,10 +18,12 @@ export default function MetadataList({
18
18
  if (isEmpty(data)) {
19
19
  return (
20
20
  <Box sx={{ textAlign: 'center' }}>
21
- <Typography color="text.primary" fontWeight={500}>
21
+ <Typography variant="subtitle1" color="text.primary">
22
22
  {t('common.metadata.empty')}
23
23
  </Typography>
24
- <Typography color="text.lighter">{t('common.metadata.emptyTip')}</Typography>
24
+ <Typography variant="body2" color="text.lighter">
25
+ {t('common.metadata.emptyTip')}
26
+ </Typography>
25
27
  <Button sx={{ color: 'text.link' }} onClick={handleEditMetadata}>
26
28
  {t('common.add')}
27
29
  </Button>
@@ -5,17 +5,18 @@ import { Stack, Tooltip, Typography } from '@mui/material';
5
5
 
6
6
  type Props = {
7
7
  subscription: TSubscriptionExpanded;
8
- variant?: 'body1' | 'h5' | 'h4' | 'h3' | 'h2' | 'h1';
8
+ variant?: 'subtitle1' | 'subtitle2' | 'body1' | 'body2' | 'h5' | 'h4' | 'h3' | 'h2' | 'h1';
9
9
  hideSubscription?: boolean;
10
+ maxLength?: number;
10
11
  };
11
12
 
12
- export default function SubscriptionDescription({ subscription, variant, hideSubscription }: Props) {
13
+ export default function SubscriptionDescription({ subscription, variant, hideSubscription, maxLength = 80 }: Props) {
13
14
  const { isMobile } = useMobile();
14
15
  if (subscription.description) {
15
16
  return (
16
17
  <Stack direction="row" alignItems="center" spacing={1}>
17
- <Typography variant={variant} fontWeight={600} className="subscription-description">
18
- <TruncatedText text={subscription.description} maxLength={80} useWidth />
18
+ <Typography variant={variant} className="subscription-description">
19
+ <TruncatedText text={subscription.description} maxLength={maxLength} useWidth />
19
20
  </Typography>
20
21
  {!hideSubscription && !isMobile && (
21
22
  <Tooltip title={formatSubscriptionProduct(subscription.items)}>
@@ -27,8 +28,8 @@ export default function SubscriptionDescription({ subscription, variant, hideSub
27
28
  }
28
29
 
29
30
  return (
30
- <Typography variant={variant} fontWeight={600}>
31
- {formatSubscriptionProduct(subscription.items)}
31
+ <Typography variant={variant}>
32
+ <TruncatedText text={formatSubscriptionProduct(subscription.items)} maxLength={maxLength} useWidth />
32
33
  </Typography>
33
34
  );
34
35
  }
@@ -36,4 +37,5 @@ export default function SubscriptionDescription({ subscription, variant, hideSub
36
37
  SubscriptionDescription.defaultProps = {
37
38
  variant: 'body1',
38
39
  hideSubscription: false,
40
+ maxLength: 80,
39
41
  };
@@ -10,15 +10,19 @@ import {
10
10
  getSubscriptionAction,
11
11
  usePaymentContext,
12
12
  OverdueInvoicePayment,
13
+ formatBNStr,
13
14
  } from '@blocklet/payment-react';
14
15
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
15
- import { Button, Link, Stack, Tooltip } from '@mui/material';
16
+ import { Button, Link, Stack, Tooltip, Typography, Box, Alert } from '@mui/material';
16
17
  import { useRequest, useSetState } from 'ahooks';
17
18
  import isEmpty from 'lodash/isEmpty';
18
- import { useEffect, useState } from 'react';
19
+ import { useEffect, useState, ReactNode } from 'react';
19
20
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
20
21
  import { useNavigate } from 'react-router-dom';
21
22
  import { joinURL } from 'ufo';
23
+ import { BN } from '@ocap/util';
24
+ import DID from '@arcblock/ux/lib/DID';
25
+ import { AddOutlined } from '@mui/icons-material';
22
26
  import CustomerCancelForm from './cancel';
23
27
  import OverdraftProtectionDialog from '../../customer/overdraft-protection';
24
28
  import Actions from '../../actions';
@@ -28,11 +32,12 @@ import { isWillCanceled } from '../../../libs/util';
28
32
  interface ActionConfig {
29
33
  key: string;
30
34
  show: boolean;
31
- label: string;
35
+ label: string | ReactNode | (() => ReactNode);
36
+ labelText?: string;
32
37
  tooltip?: string;
33
38
  onClick: (e?: React.MouseEvent) => void;
34
39
  variant?: 'text' | 'outlined' | 'contained';
35
- color?: 'inherit' | 'primary' | 'secondary' | 'error';
40
+ color?: 'inherit' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
36
41
  sx?: any;
37
42
  primary?: boolean;
38
43
  component?: any;
@@ -43,8 +48,8 @@ interface ActionConfig {
43
48
 
44
49
  type ActionProps = {
45
50
  [key: string]: {
46
- color?: string;
47
- variant?: string;
51
+ color?: 'inherit' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error';
52
+ variant?: 'text' | 'outlined' | 'contained';
48
53
  sx?: {
49
54
  [key: string]: any;
50
55
  };
@@ -65,6 +70,7 @@ type Props = {
65
70
  subscription: TSubscriptionExpanded;
66
71
  showExtra?: boolean;
67
72
  showRecharge?: boolean;
73
+ showBalanceInfo?: boolean;
68
74
  showOverdraftProtection?:
69
75
  | boolean
70
76
  | {
@@ -77,11 +83,16 @@ type Props = {
77
83
  actionProps?: ActionProps;
78
84
  mode?: ActionDisplayMode;
79
85
  setUp?: (methods: ActionMethods) => void;
86
+ includeActions?: string[] | null;
87
+ excludeActions?: string[] | null;
88
+ forceShowDetailAction?: boolean;
89
+ buttonSize?: 'small' | 'medium' | 'large';
80
90
  };
81
91
 
82
92
  SubscriptionActions.defaultProps = {
83
93
  showExtra: false,
84
94
  showRecharge: false,
95
+ showBalanceInfo: false,
85
96
  showOverdraftProtection: false,
86
97
  showDelegation: false,
87
98
  onChange: null,
@@ -89,6 +100,10 @@ SubscriptionActions.defaultProps = {
89
100
  mode: 'all-buttons',
90
101
  setUp: null,
91
102
  showUnsubscribe: true,
103
+ includeActions: null,
104
+ excludeActions: null,
105
+ forceShowDetailAction: false,
106
+ buttonSize: 'small',
92
107
  };
93
108
  const fetchExtraActions = async ({
94
109
  id,
@@ -138,6 +153,11 @@ export function SubscriptionActionsInner({
138
153
  actionProps,
139
154
  mode,
140
155
  setUp,
156
+ showBalanceInfo,
157
+ includeActions,
158
+ excludeActions,
159
+ forceShowDetailAction,
160
+ buttonSize,
141
161
  }: Props) {
142
162
  const { t, locale } = useLocaleContext();
143
163
  const { reset, getValues } = useFormContext();
@@ -147,8 +167,35 @@ export function SubscriptionActionsInner({
147
167
  const { checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(subscription.id, true);
148
168
  const action = getSubscriptionAction(subs, actionProps ?? {});
149
169
 
170
+ const showAction = (actionName: string) => {
171
+ if (excludeActions && excludeActions.length > 0) {
172
+ return !excludeActions.includes(actionName);
173
+ }
174
+ if (includeActions && includeActions.length > 0) {
175
+ return includeActions.includes(actionName);
176
+ }
177
+ return true;
178
+ };
179
+
150
180
  const { data: extraActions } = useRequest(() => fetchExtraActions({ id: subscription.id, showExtra: !!showExtra }));
151
181
 
182
+ const { data: upcoming = {} } = useRequest(
183
+ () => api.get(`/api/subscriptions/${subscription.id}/upcoming`).then((res) => res.data),
184
+ {
185
+ ready: showAction('recharge') && !!(showRecharge && supportRecharge(subscription)) && showBalanceInfo,
186
+ }
187
+ );
188
+
189
+ const { data: payerValue = {} } = useRequest(
190
+ () => api.get(`/api/subscriptions/${subscription.id}/payer-token`).then((res) => res.data),
191
+ {
192
+ ready: showAction('recharge') && !!(showRecharge && supportRecharge(subscription)) && showBalanceInfo,
193
+ }
194
+ );
195
+
196
+ const isInsufficientBalance =
197
+ upcoming?.amount && payerValue?.token && new BN(payerValue.token || '0').lt(new BN(upcoming.amount || '0'));
198
+
152
199
  const [state, setState] = useSetState({
153
200
  action: '',
154
201
  subscription: '',
@@ -164,12 +211,13 @@ export function SubscriptionActionsInner({
164
211
  ((typeof showOverdraftProtection === 'boolean' && showOverdraftProtection) ||
165
212
  (typeof showOverdraftProtection === 'object' && showOverdraftProtection.show)) &&
166
213
  subscription?.paymentMethod?.type === 'arcblock' &&
167
- ['active', 'trialing', 'past_due'].includes(subscription?.status);
214
+ ['active', 'trialing', 'past_due'].includes(subscription?.status) &&
215
+ showAction('protection');
168
216
 
169
217
  const { data: delegation = { sufficient: true }, refresh: refreshDelegation } = useRequest(
170
218
  () => api.get(`/api/subscriptions/${subscription.id}/delegation`).then((res) => res.data),
171
219
  {
172
- ready: shouldFetchDelegation,
220
+ ready: showAction('delegation') && shouldFetchDelegation,
173
221
  refreshDeps: [subscription.id, shouldFetchDelegation],
174
222
  }
175
223
  );
@@ -336,7 +384,17 @@ export function SubscriptionActionsInner({
336
384
  const supportUnsubscribe = action?.action === 'cancel' && showUnsubscribe;
337
385
  const supportAction = action && (action?.action !== 'cancel' || supportUnsubscribe);
338
386
  const supportResume = isWillCanceled(subscription) && action?.action === 'recover';
339
- const serviceActions = subscription.service_actions?.filter((x: any) => x?.type !== 'notification') || [];
387
+ const serviceActions = subscription.service_actions?.filter((x: any) => x?.type !== 'notification') || [
388
+ {
389
+ name: 'notification',
390
+ text: {
391
+ en: 'Application Details',
392
+ zh: '应用详情',
393
+ },
394
+ link: '/customer/notification',
395
+ color: 'primary',
396
+ },
397
+ ];
340
398
  const actionConfigs: ActionConfig[] = [
341
399
  {
342
400
  key: 'delegation',
@@ -344,8 +402,8 @@ export function SubscriptionActionsInner({
344
402
  label: t('customer.delegation.btn'),
345
403
  tooltip: t('customer.delegation.title'),
346
404
  onClick: handleDelegate,
347
- variant: 'outlined',
348
- color: 'primary',
405
+ variant: actionProps?.delegation?.variant || 'outlined',
406
+ color: actionProps?.delegation?.color || 'primary',
349
407
  primary: true,
350
408
  },
351
409
  {
@@ -353,20 +411,105 @@ export function SubscriptionActionsInner({
353
411
  show: shouldFetchOverdraftProtection,
354
412
  label: t('customer.overdraftProtection.setting'),
355
413
  onClick: () => setState({ openProtection: true, protectionInitValues: null }),
356
- variant: 'outlined',
357
- color: 'primary',
414
+ variant: actionProps?.protection?.variant || 'outlined',
415
+ color: actionProps?.protection?.color || 'primary',
358
416
  },
359
417
  {
360
418
  key: 'recharge',
361
419
  show: !!(showRecharge && supportRecharge(subscription)),
362
- label: t('customer.recharge.title'),
420
+ label: () => {
421
+ const balanceDisplay = (
422
+ <Stack direction="row" spacing={0.5} alignItems="center">
423
+ <AddOutlined fontSize="small" />
424
+ {t('customer.recharge.title')}
425
+ </Stack>
426
+ );
427
+
428
+ if (showBalanceInfo && subscription.paymentCurrency) {
429
+ const formattedBalance = formatBNStr(payerValue?.token || '0', subscription.paymentCurrency.decimal);
430
+ const formattedUpcoming = formatBNStr(upcoming?.amount || '0', subscription.paymentCurrency.decimal);
431
+
432
+ return (
433
+ <Tooltip
434
+ componentsProps={{
435
+ tooltip: {
436
+ sx: {
437
+ bgcolor: 'background.paper',
438
+ color: 'text.primary',
439
+ boxShadow: 2,
440
+ padding: '10px 16px',
441
+ maxWidth: 480,
442
+ minWidth: 350,
443
+ wordBreak: 'break-word',
444
+ },
445
+ },
446
+ }}
447
+ title={
448
+ <Box>
449
+ {isInsufficientBalance && (
450
+ <Alert severity="error" sx={{ py: 0, mb: 1 }}>
451
+ {t('admin.subscription.insufficientBalance')}
452
+ </Alert>
453
+ )}
454
+ <Stack spacing={0.5}>
455
+ <Box display="flex" justifyContent="space-between">
456
+ <Typography sx={{ color: 'text.secondary' }} variant="body2">
457
+ {t('admin.subscription.currentBalance')}
458
+ </Typography>
459
+ <Typography variant="body2">
460
+ {formattedBalance} {subscription.paymentCurrency.symbol}
461
+ </Typography>
462
+ </Box>
463
+ <Box display="flex" justifyContent="space-between">
464
+ <Typography sx={{ color: 'text.secondary' }} variant="body2">
465
+ {t('admin.subscription.nextInvoiceAmount')}
466
+ </Typography>
467
+ <Typography variant="body2">
468
+ {formattedUpcoming} {subscription.paymentCurrency.symbol}
469
+ </Typography>
470
+ </Box>
471
+ <Box display="flex" justifyContent="space-between">
472
+ <Typography sx={{ color: 'text.secondary' }} variant="body2">
473
+ {t('admin.subscription.paymentAddress')}
474
+ </Typography>
475
+ <DID did={payerValue?.paymentAddress} responsive={false} compact showAvatar={false} />
476
+ </Box>
477
+ </Stack>
478
+ </Box>
479
+ }
480
+ placement="top">
481
+ {balanceDisplay}
482
+ </Tooltip>
483
+ );
484
+ }
485
+
486
+ return balanceDisplay;
487
+ },
488
+ labelText: t('customer.recharge.title'),
363
489
  onClick: (e) => {
364
490
  e?.stopPropagation();
365
491
  navigate(`/customer/subscription/${subscription.id}/recharge`);
366
492
  },
367
- variant: 'outlined',
368
- color: 'primary',
493
+ variant: actionProps?.recharge?.variant || 'outlined',
494
+ color: actionProps?.recharge?.color || 'primary',
369
495
  primary: !isWillCanceled(subscription),
496
+ sx:
497
+ isInsufficientBalance || payerValue.token === '0'
498
+ ? {
499
+ '&::before': {
500
+ content: '""',
501
+ backgroundColor: 'error.main',
502
+ borderRadius: '50%',
503
+ position: 'absolute',
504
+ top: '3px',
505
+ right: '-3px',
506
+ width: '6px',
507
+ height: '6px',
508
+ zIndex: 1,
509
+ },
510
+ ...(actionProps?.recharge?.sx || {}),
511
+ }
512
+ : actionProps?.recharge?.sx || {},
370
513
  },
371
514
  {
372
515
  key: 'changePlan',
@@ -376,9 +519,9 @@ export function SubscriptionActionsInner({
376
519
  e?.stopPropagation();
377
520
  navigate(`/customer/subscription/${subscription.id}/change-plan`);
378
521
  },
379
- variant: 'contained',
380
- color: 'primary',
381
- sx: action?.sx,
522
+ variant: actionProps?.changePlan?.variant || 'contained',
523
+ color: actionProps?.changePlan?.color || 'primary',
524
+ sx: actionProps?.changePlan?.sx,
382
525
  },
383
526
  {
384
527
  key: 'batchPay',
@@ -390,9 +533,9 @@ export function SubscriptionActionsInner({
390
533
  batchPay: true,
391
534
  });
392
535
  },
393
- variant: 'outlined',
394
- color: 'error',
395
- sx: action?.sx,
536
+ variant: actionProps?.batchPay?.variant || 'outlined',
537
+ color: actionProps?.batchPay?.color || 'error',
538
+ sx: actionProps?.batchPay?.sx,
396
539
  primary: true,
397
540
  },
398
541
  {
@@ -432,38 +575,61 @@ export function SubscriptionActionsInner({
432
575
  ];
433
576
 
434
577
  // 过滤出要显示的操作
435
- const visibleActions = actionConfigs.filter((a) => a.show);
578
+ const visibleActions = actionConfigs.filter((a) => a.show && showAction(a.key));
436
579
 
437
580
  // 转换为菜单项
438
581
  const toMenuItem = (item: any) => ({
439
- label: item.label,
582
+ label: item.labelText || (typeof item.label === 'function' ? item.label() : item.label),
440
583
  handler: item.onClick,
441
584
  color: item.color,
442
585
  divider: item.divider,
443
586
  });
444
-
445
- const toButton = (item: ActionConfig) => (
587
+ const detailAction = forceShowDetailAction && (
446
588
  <Button
447
- key={item.key}
448
- variant={item.variant}
449
- color={item.color}
450
- onClick={item.onClick}
451
- component={item.component}
452
- href={item.href}
453
- target={item.target}
454
- sx={item.sx}
455
- size="small">
456
- {item.tooltip ? (
457
- <Tooltip title={item.tooltip}>
458
- <span>{item.label}</span>
459
- </Tooltip>
460
- ) : (
461
- item.label
462
- )}
589
+ className="action-button"
590
+ key="detail"
591
+ sx={actionProps?.detail?.sx || {}}
592
+ variant={actionProps?.detail?.variant || 'outlined'}
593
+ color={actionProps?.detail?.color || 'primary'}
594
+ onClick={() => navigate(`/customer/subscription/${subscription.id}`)}>
595
+ {t('customer.subscription.manage')}
463
596
  </Button>
464
597
  );
598
+
599
+ const toButton = (item: ActionConfig) => {
600
+ const labelContent = typeof item.label === 'function' ? item.label() : item.label;
601
+
602
+ return (
603
+ <Button
604
+ key={item.key}
605
+ variant={item.variant}
606
+ color={item.color}
607
+ onClick={item.onClick}
608
+ component={item.component}
609
+ href={item.href}
610
+ target={item.target}
611
+ sx={item.sx}
612
+ className="action-button"
613
+ size={buttonSize}>
614
+ {item.tooltip ? (
615
+ <Tooltip title={item.tooltip}>
616
+ <span>{labelContent}</span>
617
+ </Tooltip>
618
+ ) : (
619
+ labelContent
620
+ )}
621
+ </Button>
622
+ );
623
+ };
465
624
  if (mode === 'menu-only') {
466
- return <Actions actions={visibleActions.map(toMenuItem)} variant="outlined" />;
625
+ return (
626
+ <>
627
+ {detailAction}
628
+ {visibleActions.length > 0 && (
629
+ <Actions actions={visibleActions.map(toMenuItem)} variant="outlined" sx={actionProps?.menu?.sx || {}} />
630
+ )}
631
+ </>
632
+ );
467
633
  }
468
634
 
469
635
  if (mode === 'primary-buttons') {
@@ -471,13 +637,21 @@ export function SubscriptionActionsInner({
471
637
  const menuItems = visibleActions.filter((a) => !a.primary);
472
638
  return (
473
639
  <>
640
+ {detailAction}
474
641
  {primaryButtons.map(toButton)}
475
- {menuItems.length > 0 && <Actions actions={menuItems.map(toMenuItem)} variant="outlined" />}
642
+ {menuItems.length > 0 && (
643
+ <Actions actions={menuItems.map(toMenuItem)} variant="outlined" sx={actionProps?.menu?.sx || {}} />
644
+ )}
476
645
  </>
477
646
  );
478
647
  }
479
648
 
480
- return <>{visibleActions.map(toButton)}</>;
649
+ return (
650
+ <>
651
+ {detailAction}
652
+ {visibleActions.map(toButton)}
653
+ </>
654
+ );
481
655
  };
482
656
 
483
657
  return (
@@ -566,10 +740,15 @@ SubscriptionActionsInner.defaultProps = {
566
740
  showExtra: false,
567
741
  showRecharge: false,
568
742
  showOverdraftProtection: false,
743
+ showBalanceInfo: false,
569
744
  showDelegation: false,
570
745
  showUnsubscribe: true,
571
746
  onChange: null,
572
747
  actionProps: {},
573
748
  mode: 'all-buttons',
574
749
  setUp: null,
750
+ includeActions: null,
751
+ excludeActions: null,
752
+ forceShowDetailAction: false,
753
+ buttonSize: 'small',
575
754
  };