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.
- package/api/src/index.ts +2 -0
- package/api/src/libs/audit.ts +28 -34
- package/api/src/libs/payment.ts +2 -11
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/util.ts +8 -5
- package/api/src/routes/checkout-sessions.ts +41 -39
- package/api/src/routes/connect/collect.ts +12 -12
- package/api/src/routes/connect/setup.ts +8 -11
- package/api/src/routes/connect/shared.ts +81 -20
- package/api/src/routes/connect/subscribe.ts +8 -11
- package/api/src/routes/connect/update.ts +134 -0
- package/api/src/routes/pricing-table.ts +9 -121
- package/api/src/routes/subscriptions.ts +417 -142
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/pricing-table.ts +125 -1
- package/api/src/store/models/subscription.ts +4 -0
- package/api/src/store/models/types.ts +8 -0
- package/api/tests/libs/util.spec.ts +6 -6
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/app.tsx +12 -4
- package/src/components/checkout/form/address.tsx +41 -34
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/checkout/pricing-table.tsx +205 -0
- package/src/components/payment-link/product-select.tsx +13 -3
- package/src/components/portal/invoice/list.tsx +1 -1
- package/src/components/portal/subscription/actions.tsx +153 -0
- package/src/components/portal/subscription/list.tsx +21 -150
- package/src/components/subscription/metrics.tsx +46 -0
- package/src/contexts/products.tsx +2 -1
- package/src/libs/util.ts +43 -0
- package/src/locales/en.tsx +15 -1
- package/src/locales/zh.tsx +16 -2
- package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
- package/src/pages/checkout/pricing-table.tsx +9 -158
- package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
- 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={
|
|
95
|
+
<Box flex={2}>
|
|
96
96
|
<Typography textAlign="right">
|
|
97
97
|
{fromUnitToToken(invoice.total, invoice.paymentCurrency.decimal)}
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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:
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
+
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -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
|
|
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',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
<
|
|
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>
|