payment-kit 1.13.92 → 1.13.94

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 (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/audit.ts +28 -34
  3. package/api/src/libs/payment.ts +2 -11
  4. package/api/src/libs/session.ts +1 -1
  5. package/api/src/libs/util.ts +8 -5
  6. package/api/src/routes/checkout-sessions.ts +41 -39
  7. package/api/src/routes/connect/collect.ts +12 -12
  8. package/api/src/routes/connect/setup.ts +8 -11
  9. package/api/src/routes/connect/shared.ts +81 -20
  10. package/api/src/routes/connect/subscribe.ts +8 -11
  11. package/api/src/routes/connect/update.ts +134 -0
  12. package/api/src/routes/pricing-table.ts +9 -121
  13. package/api/src/routes/subscriptions.ts +417 -142
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/pricing-table.ts +125 -1
  16. package/api/src/store/models/subscription.ts +4 -0
  17. package/api/src/store/models/types.ts +8 -0
  18. package/api/tests/libs/util.spec.ts +6 -6
  19. package/blocklet.yml +1 -1
  20. package/package.json +6 -6
  21. package/src/app.tsx +12 -4
  22. package/src/components/checkout/form/address.tsx +41 -34
  23. package/src/components/checkout/form/index.tsx +1 -1
  24. package/src/components/checkout/pricing-table.tsx +205 -0
  25. package/src/components/payment-link/product-select.tsx +13 -3
  26. package/src/components/portal/invoice/list.tsx +1 -1
  27. package/src/components/portal/subscription/actions.tsx +153 -0
  28. package/src/components/portal/subscription/list.tsx +21 -150
  29. package/src/components/subscription/metrics.tsx +46 -0
  30. package/src/contexts/products.tsx +2 -1
  31. package/src/libs/util.ts +43 -0
  32. package/src/locales/en.tsx +15 -1
  33. package/src/locales/zh.tsx +16 -2
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
  35. package/src/pages/checkout/pricing-table.tsx +9 -158
  36. package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
  37. package/src/pages/customer/subscription/update.tsx +281 -0
@@ -92,7 +92,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
92
92
  <Box flex={3}>
93
93
  <Typography>{formatToDate(invoice.created_at)}</Typography>
94
94
  </Box>
95
- <Box flex={1}>
95
+ <Box flex={2}>
96
96
  <Typography textAlign="right">
97
97
  {fromUnitToToken(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
98
98
  {invoice.paymentCurrency.symbol}
@@ -0,0 +1,153 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import type { TSubscriptionExpanded } from '@did-pay/types';
5
+ import { Button, Stack } from '@mui/material';
6
+ import { useRequest, useSetState } from 'ahooks';
7
+ import { FormProvider, useForm, useFormContext } from 'react-hook-form';
8
+ import { useNavigate } from 'react-router-dom';
9
+
10
+ import api from '../../../libs/api';
11
+ import { formatError, formatToDate, getSubscriptionAction } from '../../../libs/util';
12
+ import ConfirmDialog from '../../confirm';
13
+ import CustomerCancelForm from './cancel';
14
+
15
+ type Props = {
16
+ subscription: TSubscriptionExpanded;
17
+ showUpdate?: boolean;
18
+ onChange: (action?: string) => any | Promise<any>;
19
+ };
20
+
21
+ SubscriptionActions.defaultProps = {
22
+ showUpdate: false,
23
+ };
24
+
25
+ const fetchUpdateOptions = ({ id, showUpdate }: { id: string; showUpdate: boolean }): Promise<boolean> => {
26
+ if (!showUpdate) {
27
+ return Promise.resolve(false);
28
+ }
29
+
30
+ return api.get(`/api/subscriptions/${id}/update`).then((res) => !!res.data);
31
+ };
32
+
33
+ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }: Props) {
34
+ const { t } = useLocaleContext();
35
+ const { reset, getValues } = useFormContext();
36
+ const navigate = useNavigate();
37
+ const action = getSubscriptionAction(subscription);
38
+
39
+ const { data } = useRequest(() => fetchUpdateOptions({ id: subscription.id, showUpdate: !!showUpdate }));
40
+
41
+ const [state, setState] = useSetState({
42
+ action: '',
43
+ subscription: '',
44
+ loading: false,
45
+ });
46
+
47
+ const handleCancel = async () => {
48
+ try {
49
+ setState({ loading: true });
50
+ await api
51
+ .put(`/api/subscriptions/${state.subscription}/cancel`, { at: 'current_period_end', ...getValues().cancel })
52
+ .then((res) => res.data);
53
+ Toast.success(t('common.saved'));
54
+ onChange(state.action);
55
+ } catch (err) {
56
+ console.error(err);
57
+ Toast.error(formatError(err));
58
+ } finally {
59
+ setState({ loading: false, action: '', subscription: '' });
60
+ reset();
61
+ }
62
+ };
63
+
64
+ const handleRecover = async () => {
65
+ try {
66
+ setState({ loading: true });
67
+ await api.put(`/api/subscriptions/${state.subscription}/recover`).then((res) => res.data);
68
+ Toast.success(t('common.saved'));
69
+ onChange(state.action);
70
+ } catch (err) {
71
+ console.error(err);
72
+ Toast.error(formatError(err));
73
+ } finally {
74
+ setState({ loading: false, action: '', subscription: '' });
75
+ }
76
+ };
77
+
78
+ return (
79
+ <Stack direction="row" alignItems="center" spacing={1}>
80
+ {action && (
81
+ <Button
82
+ variant={action.variant as any}
83
+ color={action.color as any}
84
+ size="small"
85
+ onClick={() => {
86
+ if (action.action === 'pastDue') {
87
+ navigate(`/customer/invoice/${subscription.latest_invoice_id}?action=pay`);
88
+ } else {
89
+ setState({
90
+ action: action.action,
91
+ subscription: subscription.id,
92
+ });
93
+ }
94
+ }}>
95
+ {t(`customer.${action.action}.button`)}
96
+ </Button>
97
+ )}
98
+ {data && (
99
+ <Button
100
+ variant="contained"
101
+ color="primary"
102
+ size="small"
103
+ onClick={() => {
104
+ navigate(`/customer/subscription/${subscription.id}/update`);
105
+ }}>
106
+ {t('customer.upgrade.button')}
107
+ </Button>
108
+ )}
109
+ {state.action === 'cancel' && state.subscription && (
110
+ <ConfirmDialog
111
+ onConfirm={handleCancel}
112
+ onCancel={() => setState({ action: '', subscription: '' })}
113
+ title={t('customer.cancel.title')}
114
+ message={<CustomerCancelForm data={subscription} />}
115
+ loading={state.loading}
116
+ />
117
+ )}
118
+ {state.action === 'recover' && state.subscription && (
119
+ <ConfirmDialog
120
+ onConfirm={handleRecover}
121
+ onCancel={() => setState({ action: '', subscription: '' })}
122
+ title={t('customer.recover.title')}
123
+ message={t('customer.recover.description', {
124
+ date: formatToDate(subscription.current_period_end * 1000),
125
+ })}
126
+ loading={state.loading}
127
+ />
128
+ )}
129
+ </Stack>
130
+ );
131
+ }
132
+
133
+ export default function SubscriptionActions(props: Props) {
134
+ const methods = useForm({
135
+ defaultValues: {
136
+ cancel: {
137
+ at: 'current_period_end',
138
+ feedback: 'unused',
139
+ comment: '',
140
+ },
141
+ },
142
+ });
143
+
144
+ return (
145
+ <FormProvider {...methods}>
146
+ <SubscriptionActionsInner {...props} />
147
+ </FormProvider>
148
+ );
149
+ }
150
+
151
+ SubscriptionActionsInner.defaultProps = {
152
+ showUpdate: false,
153
+ };
@@ -1,23 +1,18 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import Toast from '@arcblock/ux/lib/Toast';
4
3
  import type { Paginated, TSubscriptionExpanded } from '@did-pay/types';
5
4
  import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
6
- import { useInfiniteScroll, useSetState } from 'ahooks';
7
- import { FormProvider, useForm, useFormContext } from 'react-hook-form';
8
- import { useNavigate } from 'react-router-dom';
5
+ import { useInfiniteScroll } from 'ahooks';
9
6
 
10
7
  import api from '../../../libs/api';
11
8
  import {
12
- formatError,
13
9
  formatPrice,
14
10
  formatSubscriptionProduct,
15
- formatToDate,
16
11
  getSubscriptionStatusColor,
12
+ getSubscriptionTimeSummary,
17
13
  } from '../../../libs/util';
18
- import ConfirmDialog from '../../confirm';
19
14
  import Status from '../../status';
20
- import CustomerCancelForm from './cancel';
15
+ import SubscriptionActions from './actions';
21
16
 
22
17
  const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TSubscriptionExpanded>> => {
23
18
  const search = new URLSearchParams();
@@ -35,47 +30,8 @@ type Props = {
35
30
 
36
31
  const pageSize = 4;
37
32
 
38
- export const getSubscriptionTimeSummary = (subscription: TSubscriptionExpanded) => {
39
- const lines = [`Started on ${formatToDate(subscription.start_date * 1000)}`];
40
- if (subscription.status === 'active' || subscription.status === 'trialing') {
41
- if (subscription.cancel_at) {
42
- lines.push(`will cancel on ${formatToDate(subscription.cancel_at * 1000)}`);
43
- } else if (subscription.cancel_at_period_end) {
44
- lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
45
- } else {
46
- lines.push(`will renew on ${formatToDate(subscription.current_period_end * 1000)}`);
47
- }
48
- } else if (subscription.status === 'past_due') {
49
- lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
50
- } else if (subscription.status === 'canceled') {
51
- lines.push(`canceled on ${formatToDate(subscription.canceled_at * 1000)}`);
52
- }
53
-
54
- return lines.join(', ');
55
- };
56
-
57
- export const getSubscriptionAction = (subscription: TSubscriptionExpanded) => {
58
- if (subscription.status === 'active' || subscription.status === 'trialing') {
59
- if (subscription.cancel_at) {
60
- return null;
61
- }
62
- if (subscription.cancel_at_period_end) {
63
- return { action: 'recover', color: 'secondary' };
64
- }
65
-
66
- return { action: 'cancel', color: 'error' };
67
- }
68
- if (subscription.status === 'past_due') {
69
- return { action: 'pastDue', color: 'primary' };
70
- }
71
-
72
- return null;
73
- };
74
-
75
- export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, ...rest }: Props) {
33
+ export default function CurrentSubscriptions({ id, onChange, onClickSubscription, ...rest }: Props) {
76
34
  const { t } = useLocaleContext();
77
- const { reset, getValues } = useFormContext();
78
- const navigate = useNavigate();
79
35
 
80
36
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
81
37
  (d) => {
@@ -87,43 +43,6 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
87
43
  }
88
44
  );
89
45
 
90
- const [state, setState] = useSetState({
91
- action: '',
92
- subscription: '',
93
- loading: false,
94
- });
95
-
96
- const handleCancel = async () => {
97
- try {
98
- setState({ loading: true });
99
- await api
100
- .put(`/api/subscriptions/${state.subscription}/cancel`, { at: 'current_period_end', ...getValues().cancel })
101
- .then((res) => res.data);
102
- Toast.success(t('common.saved'));
103
- onChange(state.action);
104
- } catch (err) {
105
- console.error(err);
106
- Toast.error(formatError(err));
107
- } finally {
108
- setState({ loading: false, action: '', subscription: '' });
109
- reset();
110
- }
111
- };
112
-
113
- const handleRecover = async () => {
114
- try {
115
- setState({ loading: true });
116
- await api.put(`/api/subscriptions/${state.subscription}/recover`).then((res) => res.data);
117
- Toast.success(t('common.saved'));
118
- onChange(state.action);
119
- } catch (err) {
120
- console.error(err);
121
- Toast.error(formatError(err));
122
- } finally {
123
- setState({ loading: false, action: '', subscription: '' });
124
- }
125
- };
126
-
127
46
  if (loading || !data) {
128
47
  return <CircularProgress />;
129
48
  }
@@ -136,9 +55,8 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
136
55
  const size = { width: 48, height: 48 };
137
56
 
138
57
  return (
139
- <Stack direction="column" spacing={4} sx={{ mt: 2 }}>
58
+ <Stack direction="column" spacing={2} sx={{ mt: 2 }}>
140
59
  {data.list.map((subscription) => {
141
- const action = getSubscriptionAction(subscription);
142
60
  return (
143
61
  <Stack
144
62
  key={subscription.id}
@@ -146,10 +64,23 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
146
64
  justifyContent="space-between"
147
65
  gap={{
148
66
  xs: 1,
149
- sm: 3,
67
+ sm: 2,
68
+ }}
69
+ sx={{
70
+ padding: 1,
71
+ '&:hover': {
72
+ backgroundColor: 'grey.50',
73
+ transition: 'background-color 200ms linear',
74
+ cursor: 'pointer',
75
+ },
150
76
  }}
151
77
  flexWrap="wrap">
152
- <Stack direction="column" spacing={0.5} onClick={() => onClickSubscription(subscription)} {...rest}>
78
+ <Stack
79
+ direction="column"
80
+ flex={1}
81
+ spacing={0.5}
82
+ onClick={() => onClickSubscription(subscription)}
83
+ {...rest}>
153
84
  <Stack direction="row" spacing={1} alignItems="center">
154
85
  <AvatarGroup max={3}>
155
86
  {subscription.items.map((item) =>
@@ -196,25 +127,7 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
196
127
  </Stack>
197
128
  </Stack>
198
129
  <Stack direction="column" alignItems="flex-end" spacing={1}>
199
- {action && (
200
- <Button
201
- variant="outlined"
202
- // @ts-ignore
203
- color={action.color}
204
- size="small"
205
- onClick={() => {
206
- if (action.action === 'pastDue') {
207
- navigate(`/customer/invoice/${subscription.latest_invoice_id}?action=pay`);
208
- } else {
209
- setState({
210
- action: action.action,
211
- subscription: subscription.id,
212
- });
213
- }
214
- }}>
215
- {t(`customer.${action.action}.button`)}
216
- </Button>
217
- )}
130
+ <SubscriptionActions subscription={subscription} onChange={onChange} />
218
131
  </Stack>
219
132
  </Stack>
220
133
  );
@@ -231,48 +144,6 @@ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, .
231
144
  <Typography color="text.secondary">{t('common.noMore', { resource: t('admin.subscriptions') })}</Typography>
232
145
  )}
233
146
  </Box>
234
- {state.action === 'cancel' && state.subscription && (
235
- <ConfirmDialog
236
- onConfirm={handleCancel}
237
- onCancel={() => setState({ action: '', subscription: '' })}
238
- title={t('customer.cancel.title')}
239
- message={
240
- <CustomerCancelForm data={data.list.find((x) => x.id === state.subscription) as TSubscriptionExpanded} />
241
- }
242
- loading={state.loading}
243
- />
244
- )}
245
- {state.action === 'recover' && state.subscription && (
246
- <ConfirmDialog
247
- onConfirm={handleRecover}
248
- onCancel={() => setState({ action: '', subscription: '' })}
249
- title={t('customer.recover.title')}
250
- message={t('customer.recover.description', {
251
- date: formatToDate(
252
- (data.list.find((x) => x.id === state.subscription) as TSubscriptionExpanded).current_period_end * 1000
253
- ),
254
- })}
255
- loading={state.loading}
256
- />
257
- )}
258
147
  </Stack>
259
148
  );
260
149
  }
261
-
262
- export default function CurrentSubscriptions(props: Props) {
263
- const methods = useForm({
264
- defaultValues: {
265
- cancel: {
266
- at: 'current_period_end',
267
- feedback: 'unused',
268
- comment: '',
269
- },
270
- },
271
- });
272
-
273
- return (
274
- <FormProvider {...methods}>
275
- <CurrentSubscriptionsInner {...props} />
276
- </FormProvider>
277
- );
278
- }
@@ -0,0 +1,46 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type { TSubscriptionExpanded } from '@did-pay/types';
3
+
4
+ import { formatTime } from '../../libs/util';
5
+ import InfoMetric from '../info-metric';
6
+
7
+ type Props = {
8
+ subscription: TSubscriptionExpanded;
9
+ };
10
+
11
+ export default function SubscriptionMetrics({ subscription }: Props) {
12
+ const { t } = useLocaleContext();
13
+ let scheduleToCancelTime = 0;
14
+ if (['active', 'trailing'].includes(subscription.status) && subscription.cancel_at) {
15
+ scheduleToCancelTime = subscription.cancel_at * 1000;
16
+ } else if (subscription.status !== 'canceled' && subscription.cancel_at_period_end) {
17
+ scheduleToCancelTime = subscription.current_period_end * 1000;
18
+ }
19
+
20
+ return (
21
+ <>
22
+ <InfoMetric
23
+ label={t('admin.subscription.startedAt')}
24
+ value={formatTime(subscription.start_date ? subscription.start_date * 1000 : subscription.created_at)}
25
+ divider
26
+ />
27
+ {subscription.status === 'active' && !subscription.cancel_at && (
28
+ <InfoMetric
29
+ label={t('admin.subscription.nextInvoice')}
30
+ value={formatTime(subscription.current_period_end * 1000)}
31
+ divider
32
+ />
33
+ )}
34
+ {scheduleToCancelTime > 0 && (
35
+ <InfoMetric label={t('admin.subscription.cancel.schedule')} value={formatTime(scheduleToCancelTime)} divider />
36
+ )}
37
+ {subscription.status === 'canceled' && subscription.canceled_at && (
38
+ <InfoMetric
39
+ label={t('admin.subscription.cancel.done')}
40
+ value={formatTime(subscription.canceled_at * 1000)}
41
+ divider
42
+ />
43
+ )}
44
+ </>
45
+ );
46
+ }
@@ -15,7 +15,8 @@ const ProductsContext = createContext<ProductsContextType>({ api });
15
15
  const { Provider, Consumer } = ProductsContext;
16
16
 
17
17
  const getProducts = async (): Promise<TProductExpanded[]> => {
18
- const { data } = await api.get('/api/products?active=true');
18
+ // FIXME: pagination here
19
+ const { data } = await api.get('/api/products?active=true&page=1&pageSize=100');
19
20
  return data.list || [];
20
21
  };
21
22
 
package/src/libs/util.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  TPaymentMethodExpanded,
12
12
  TPrice,
13
13
  TProductExpanded,
14
+ TSubscriptionExpanded,
14
15
  TSubscriptionItemExpanded,
15
16
  } from '@did-pay/types';
16
17
  import { BN, fromUnitToToken } from '@ocap/util';
@@ -734,3 +735,45 @@ export function sleep(ms: number) {
734
735
  export function isSuccessAttempt(code: number) {
735
736
  return code >= 200 && code < 300;
736
737
  }
738
+
739
+ export const getSubscriptionTimeSummary = (subscription: TSubscriptionExpanded) => {
740
+ const lines = [`Started on ${formatToDate(subscription.start_date * 1000)}`];
741
+ if (subscription.status === 'active' || subscription.status === 'trialing') {
742
+ if (subscription.cancel_at) {
743
+ lines.push(`will cancel on ${formatToDate(subscription.cancel_at * 1000)}`);
744
+ } else if (subscription.cancel_at_period_end) {
745
+ lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
746
+ } else {
747
+ lines.push(`will renew on ${formatToDate(subscription.current_period_end * 1000)}`);
748
+ }
749
+ } else if (subscription.status === 'past_due') {
750
+ lines.push(`will cancel on ${formatToDate(subscription.current_period_end * 1000)}`);
751
+ } else if (subscription.status === 'canceled') {
752
+ lines.push(`canceled on ${formatToDate(subscription.canceled_at * 1000)}`);
753
+ }
754
+
755
+ return lines.join(', ');
756
+ };
757
+
758
+ export const getSubscriptionAction = (subscription: TSubscriptionExpanded) => {
759
+ if (subscription.status !== 'canceled' && subscription.cancel_at_period_end) {
760
+ return { action: 'recover', variant: 'contained', color: 'primary' };
761
+ }
762
+
763
+ if (subscription.status === 'active' || subscription.status === 'trialing') {
764
+ if (subscription.cancel_at) {
765
+ return null;
766
+ }
767
+ if (subscription.cancel_at_period_end) {
768
+ return { action: 'recover', variant: 'contained', color: 'primary' };
769
+ }
770
+
771
+ return { action: 'cancel', variant: 'outlined', color: 'inherit' };
772
+ }
773
+
774
+ if (subscription.status === 'past_due') {
775
+ return { action: 'pastDue', variant: 'contained', color: 'primary' };
776
+ }
777
+
778
+ return null;
779
+ };
@@ -509,6 +509,8 @@ export default flat({
509
509
  try: 'Try for free',
510
510
  include: 'This includes:',
511
511
  subscription: 'Subscribe',
512
+ select: 'Select',
513
+ selected: 'Selected',
512
514
  noPricing: 'No items to purchase',
513
515
  setup: 'Subscribe',
514
516
  continue: 'Confirm {action}',
@@ -603,6 +605,18 @@ export default flat({
603
605
  title: 'Renew your subscription',
604
606
  description: 'Your subscription will no longer be canceled, it will renew on {date}',
605
607
  },
608
+ upgrade: {
609
+ button: 'Update',
610
+ current: 'Current',
611
+ pay: 'Payment Required',
612
+ scan: 'Complete the payment to upgrade your subscription',
613
+ success: 'Your subscription is successfully upgraded',
614
+ error: 'Failed to upgrade your subscription',
615
+ config: 'Switch to another plan or billing cycle',
616
+ confirm: 'Confirm changes to your subscription',
617
+ summary: 'What you will pay for starting {date}',
618
+ due: 'Amount due today',
619
+ },
606
620
  invoice: {
607
621
  summary: 'Summary',
608
622
  details: 'Details',
@@ -611,7 +625,7 @@ export default flat({
611
625
  amountPaid: 'Amount Paid',
612
626
  rawQuantity: 'Raw Quantity: {quantity}',
613
627
  amountDue: 'Amount Due',
614
- amountApplied: 'Applied Balance',
628
+ amountApplied: 'Applied Credit',
615
629
  pay: 'Pay this invoice',
616
630
  paySuccess: 'You have successfully paid the invoice',
617
631
  payError: 'Failed to paid the invoice',
@@ -499,6 +499,8 @@ export default flat({
499
499
  try: '免费试用',
500
500
  include: '包括:',
501
501
  subscription: '订阅',
502
+ select: '选择',
503
+ selected: '已选',
502
504
  noPricing: '没有可购买的物品',
503
505
  setup: '订阅',
504
506
  continue: '确认{action}',
@@ -581,13 +583,25 @@ export default flat({
581
583
  other: '其他原因',
582
584
  },
583
585
  },
586
+ pastDue: {
587
+ button: '续费',
588
+ },
584
589
  recover: {
585
590
  button: '续订',
586
591
  title: '续订您的订阅',
587
592
  description: '您的订阅将不再被取消,将在{date}续订',
588
593
  },
589
- pastDue: {
590
- button: '续费',
594
+ upgrade: {
595
+ button: '更新',
596
+ current: '当前订阅',
597
+ pay: '需要支付',
598
+ scan: '完成支付以更新你的订阅',
599
+ success: '你的订阅已经更新成功',
600
+ error: '订阅更新失败',
601
+ config: '切换套餐或周期',
602
+ confirm: '确认变更细节',
603
+ summary: '新的付款计划({date} 开始)',
604
+ due: '今天还需支付',
591
605
  },
592
606
  invoice: {
593
607
  summary: '摘要',
@@ -13,13 +13,13 @@ import TxLink from '../../../../components/blockchain/tx';
13
13
  import Copyable from '../../../../components/copyable';
14
14
  import Currency from '../../../../components/currency';
15
15
  import EventList from '../../../../components/event/list';
16
- import InfoMetric from '../../../../components/info-metric';
17
16
  import InfoRow from '../../../../components/info-row';
18
17
  import InvoiceList from '../../../../components/invoice/list';
19
18
  import MetadataEditor from '../../../../components/metadata/editor';
20
19
  import SectionHeader from '../../../../components/section/header';
21
20
  import SubscriptionActions from '../../../../components/subscription/actions';
22
21
  import SubscriptionItemList from '../../../../components/subscription/items';
22
+ import SubscriptionMetrics from '../../../../components/subscription/metrics';
23
23
  import SubscriptionStatus from '../../../../components/subscription/status';
24
24
  import api from '../../../../libs/api';
25
25
  import { formatError, formatSubscriptionProduct, formatTime } from '../../../../libs/util';
@@ -106,39 +106,7 @@ export default function SubscriptionDetail(props: { id: string }) {
106
106
  justifyContent="flex-start"
107
107
  flexWrap="wrap"
108
108
  sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
109
- <InfoMetric
110
- label={t('admin.subscription.startedAt')}
111
- value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
112
- divider
113
- />
114
- {data.status === 'active' && !data.cancel_at && (
115
- <InfoMetric
116
- label={t('admin.subscription.nextInvoice')}
117
- value={formatTime(data.current_period_end * 1000)}
118
- divider
119
- />
120
- )}
121
- {['active', 'trailing'].includes(data.status) && data.cancel_at && (
122
- <InfoMetric
123
- label={t('admin.subscription.cancel.schedule')}
124
- value={formatTime(data.cancel_at * 1000)}
125
- divider
126
- />
127
- )}
128
- {data.status !== 'canceled' && data.cancel_at_period_end && (
129
- <InfoMetric
130
- label={t('admin.subscription.cancel.schedule')}
131
- value={formatTime(data.current_period_end * 1000)}
132
- divider
133
- />
134
- )}
135
- {data.status === 'canceled' && data.canceled_at && (
136
- <InfoMetric
137
- label={t('admin.subscription.cancel.done')}
138
- value={formatTime(data.canceled_at * 1000)}
139
- divider
140
- />
141
- )}
109
+ <SubscriptionMetrics subscription={data} />
142
110
  </Stack>
143
111
  </Box>
144
112
  </Box>