payment-kit 1.15.33 → 1.15.35
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/integrations/stripe/handlers/setup-intent.ts +3 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
- package/api/src/integrations/stripe/resource.ts +0 -11
- package/api/src/libs/invoice.ts +202 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
- package/api/src/libs/payment.ts +3 -2
- package/api/src/libs/refund.ts +4 -0
- package/api/src/libs/subscription.ts +58 -14
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +3 -1
- package/api/src/queues/refund.ts +9 -8
- package/api/src/queues/subscription.ts +111 -40
- package/api/src/routes/checkout-sessions.ts +22 -6
- package/api/src/routes/connect/change-payment.ts +51 -34
- package/api/src/routes/connect/change-plan.ts +25 -3
- package/api/src/routes/connect/recharge.ts +28 -3
- package/api/src/routes/connect/setup.ts +27 -6
- package/api/src/routes/connect/shared.ts +223 -1
- package/api/src/routes/connect/subscribe.ts +25 -3
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +27 -105
- package/api/src/routes/payment-links.ts +3 -0
- package/api/src/routes/refunds.ts +22 -1
- package/api/src/routes/subscriptions.ts +112 -21
- package/api/src/routes/webhook-attempts.ts +14 -1
- package/api/src/store/models/invoice.ts +3 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +3 -1
- package/src/components/invoice/list.tsx +83 -31
- package/src/components/invoice/recharge.tsx +244 -0
- package/src/components/payment-intent/actions.tsx +2 -1
- package/src/components/payment-link/actions.tsx +6 -6
- package/src/components/payment-link/item.tsx +53 -18
- package/src/components/pricing-table/actions.tsx +14 -3
- package/src/components/pricing-table/payment-settings.tsx +1 -1
- package/src/components/refund/actions.tsx +43 -1
- package/src/components/refund/list.tsx +1 -1
- package/src/components/subscription/actions/cancel.tsx +10 -7
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +22 -1
- package/src/components/subscription/portal/list.tsx +1 -0
- package/src/components/webhook/attempts.tsx +19 -121
- package/src/components/webhook/request-info.tsx +139 -0
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +8 -0
- package/src/pages/admin/billing/invoices/detail.tsx +15 -0
- package/src/pages/admin/billing/invoices/index.tsx +1 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/admin/payments/refunds/detail.tsx +2 -2
- package/src/pages/admin/products/links/create.tsx +4 -1
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +34 -14
- package/src/pages/customer/recharge.tsx +45 -35
- package/src/pages/customer/subscription/change-plan.tsx +8 -1
- package/src/pages/customer/subscription/detail.tsx +12 -22
- package/src/pages/customer/subscription/embed.tsx +3 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import {
|
|
4
|
+
Status,
|
|
5
|
+
api,
|
|
6
|
+
formatBNStr,
|
|
7
|
+
getInvoiceStatusColor,
|
|
8
|
+
Table,
|
|
9
|
+
useDefaultPageSize,
|
|
10
|
+
getInvoiceDescriptionAndReason,
|
|
11
|
+
getTxLink,
|
|
12
|
+
formatToDate,
|
|
13
|
+
} from '@blocklet/payment-react';
|
|
14
|
+
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
15
|
+
import { CircularProgress, Typography, Box } from '@mui/material';
|
|
16
|
+
import { useLocalStorageState } from 'ahooks';
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import { styled } from '@mui/system';
|
|
19
|
+
|
|
20
|
+
const fetchData = (
|
|
21
|
+
subscriptionId: string,
|
|
22
|
+
params: Record<string, any> = {}
|
|
23
|
+
): Promise<{ list: TInvoiceExpanded[]; count: number }> => {
|
|
24
|
+
const search = new URLSearchParams();
|
|
25
|
+
Object.keys(params).forEach((key) => {
|
|
26
|
+
let v = params[key];
|
|
27
|
+
if (key === 'q') {
|
|
28
|
+
v = Object.entries(v)
|
|
29
|
+
.map((x) => x.join(':'))
|
|
30
|
+
.join(' ');
|
|
31
|
+
}
|
|
32
|
+
search.set(key, String(v));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return api.get(`/api/subscriptions/${subscriptionId}/recharge?${search.toString()}`).then((res) => res.data);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type SearchProps = {
|
|
39
|
+
status?: string;
|
|
40
|
+
pageSize: number;
|
|
41
|
+
page: number;
|
|
42
|
+
currency_id?: string;
|
|
43
|
+
subscription_id?: string;
|
|
44
|
+
q?: any;
|
|
45
|
+
o?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type ListProps = {
|
|
49
|
+
features?: {
|
|
50
|
+
customer?: boolean;
|
|
51
|
+
toolbar?: boolean;
|
|
52
|
+
filter?: boolean;
|
|
53
|
+
footer?: boolean;
|
|
54
|
+
};
|
|
55
|
+
currency_id?: string;
|
|
56
|
+
subscription_id?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getListKey = (props: ListProps) => {
|
|
60
|
+
if (props.subscription_id) {
|
|
61
|
+
return `subscription-recharge-${props.subscription_id}`;
|
|
62
|
+
}
|
|
63
|
+
return 'invoices';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
RechargeList.defaultProps = {
|
|
67
|
+
features: {
|
|
68
|
+
customer: true,
|
|
69
|
+
filter: true,
|
|
70
|
+
},
|
|
71
|
+
currency_id: '',
|
|
72
|
+
subscription_id: '',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default function RechargeList({ currency_id, subscription_id, features }: ListProps) {
|
|
76
|
+
const listKey = getListKey({ currency_id, subscription_id });
|
|
77
|
+
|
|
78
|
+
const { t, locale } = useLocaleContext();
|
|
79
|
+
const defaultPageSize = useDefaultPageSize(20);
|
|
80
|
+
const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
|
|
81
|
+
defaultValue: {
|
|
82
|
+
currency_id,
|
|
83
|
+
pageSize: defaultPageSize,
|
|
84
|
+
page: 1,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const [data, setData] = useState({}) as any;
|
|
89
|
+
|
|
90
|
+
const refresh = () =>
|
|
91
|
+
fetchData(subscription_id!, {
|
|
92
|
+
...search,
|
|
93
|
+
}).then((res: any) => {
|
|
94
|
+
setData(res);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
refresh();
|
|
99
|
+
}, [search]);
|
|
100
|
+
|
|
101
|
+
if (!data.list) {
|
|
102
|
+
return <CircularProgress />;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const getInvoiceLink = (invoice: TInvoiceExpanded) => {
|
|
106
|
+
return {
|
|
107
|
+
external: true,
|
|
108
|
+
connect: false,
|
|
109
|
+
url: getTxLink(invoice.paymentMethod, invoice.metadata?.payment_details).link,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const columns = [
|
|
114
|
+
{
|
|
115
|
+
label: t('common.amount'),
|
|
116
|
+
name: 'total',
|
|
117
|
+
width: 80,
|
|
118
|
+
align: 'right',
|
|
119
|
+
options: {
|
|
120
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
121
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
122
|
+
const link = getInvoiceLink(invoice);
|
|
123
|
+
return (
|
|
124
|
+
<a href={link.url} target="_blank" rel="noreferrer">
|
|
125
|
+
<Typography>
|
|
126
|
+
{formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}
|
|
127
|
+
{invoice.paymentCurrency.symbol}
|
|
128
|
+
</Typography>
|
|
129
|
+
</a>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: t('payment.customer.invoice.invoiceNumber'),
|
|
136
|
+
name: 'number',
|
|
137
|
+
options: {
|
|
138
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
139
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
140
|
+
const link = getInvoiceLink(invoice);
|
|
141
|
+
return (
|
|
142
|
+
<a href={link.url} target="_blank" rel="noreferrer">
|
|
143
|
+
{invoice?.number}
|
|
144
|
+
</a>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: t('common.rechargeTime'),
|
|
151
|
+
name: 'name',
|
|
152
|
+
options: {
|
|
153
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
154
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
155
|
+
const link = getInvoiceLink(invoice);
|
|
156
|
+
return (
|
|
157
|
+
<a href={link.url} target="_blank" rel="noreferrer">
|
|
158
|
+
{formatToDate(invoice.created_at, locale, 'YYYY-MM-DD HH:mm:ss')}
|
|
159
|
+
</a>
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: t('common.description'),
|
|
166
|
+
name: '',
|
|
167
|
+
options: {
|
|
168
|
+
sort: false,
|
|
169
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
170
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
171
|
+
const link = getInvoiceLink(invoice);
|
|
172
|
+
return (
|
|
173
|
+
<a href={link.url} target="_blank" rel="noreferrer">
|
|
174
|
+
{getInvoiceDescriptionAndReason(invoice, locale)?.description || invoice.id}
|
|
175
|
+
</a>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: t('common.status'),
|
|
182
|
+
name: 'status',
|
|
183
|
+
options: {
|
|
184
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
185
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
186
|
+
const link = getInvoiceLink(invoice);
|
|
187
|
+
return (
|
|
188
|
+
<a href={link.url} target="_blank" rel="noreferrer">
|
|
189
|
+
<Status label={invoice.status} color={getInvoiceStatusColor(invoice.status)} />
|
|
190
|
+
</a>
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const onTableChange = ({ page, rowsPerPage }: any) => {
|
|
198
|
+
if (search!.pageSize !== rowsPerPage) {
|
|
199
|
+
setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
|
|
200
|
+
} else if (search!.page !== page + 1) {
|
|
201
|
+
// @ts-ignore
|
|
202
|
+
setSearch((x) => ({ ...x, page: page + 1 }));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<InvoiceTableRoot>
|
|
208
|
+
<Table
|
|
209
|
+
hasRowLink
|
|
210
|
+
data={data.list}
|
|
211
|
+
durable={`__${listKey}__`}
|
|
212
|
+
durableKeys={['searchText']}
|
|
213
|
+
columns={columns}
|
|
214
|
+
loading={!data.list}
|
|
215
|
+
onChange={onTableChange}
|
|
216
|
+
mobileTDFlexDirection="row"
|
|
217
|
+
options={{
|
|
218
|
+
count: data.count,
|
|
219
|
+
page: search!.page - 1,
|
|
220
|
+
rowsPerPage: search!.pageSize,
|
|
221
|
+
}}
|
|
222
|
+
toolbar={false}
|
|
223
|
+
showMobile={false}
|
|
224
|
+
footer={features?.footer}
|
|
225
|
+
emptyNodeText={`${t('empty.invoices')}`}
|
|
226
|
+
/>
|
|
227
|
+
</InvoiceTableRoot>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const InvoiceTableRoot = styled(Box)`
|
|
232
|
+
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
233
|
+
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
|
|
234
|
+
> div {
|
|
235
|
+
width: fit-content;
|
|
236
|
+
flex: inherit;
|
|
237
|
+
font-size: 14px;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
.invoice-summary {
|
|
241
|
+
padding-right: 20px;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
@@ -187,6 +187,7 @@ export function PaymentIntentActionsInner({ data, variant, onChange }: Props) {
|
|
|
187
187
|
const amount = formatBNStr(res?.amount, data.paymentCurrency.decimal);
|
|
188
188
|
setRefundMaxAmount(amount);
|
|
189
189
|
},
|
|
190
|
+
manual: true,
|
|
190
191
|
}
|
|
191
192
|
);
|
|
192
193
|
|
|
@@ -247,7 +248,7 @@ export function PaymentIntentActionsInner({ data, variant, onChange }: Props) {
|
|
|
247
248
|
|
|
248
249
|
return (
|
|
249
250
|
<ClickBoundary>
|
|
250
|
-
<Actions variant={variant} actions={actions} />
|
|
251
|
+
<Actions variant={variant} actions={actions} onOpenCallback={runRefundAmountAsync} />
|
|
251
252
|
{state.action === 'refund' && (
|
|
252
253
|
<ConfirmDialog
|
|
253
254
|
onConfirm={handleSubmit(onRefund)}
|
|
@@ -87,12 +87,12 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
|
|
|
87
87
|
handler: onOpenLink,
|
|
88
88
|
color: 'primary',
|
|
89
89
|
},
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
90
|
+
// {
|
|
91
|
+
// label: t('admin.paymentLink.edit'),
|
|
92
|
+
// handler: () => setState({ action: 'edit' }),
|
|
93
|
+
// color: 'primary',
|
|
94
|
+
// disabled: true,
|
|
95
|
+
// },
|
|
96
96
|
{
|
|
97
97
|
label: t('admin.paymentLink.copyLink'),
|
|
98
98
|
handler: onCopyLink,
|
|
@@ -26,6 +26,10 @@ export default function LineItem({ prefix, product, valid, onUpdate, onRemove }:
|
|
|
26
26
|
const { control, setValue } = useFormContext();
|
|
27
27
|
const [state, setState] = useSetState({ editing: false, loading: false });
|
|
28
28
|
const adjustable = useWatch({ control, name: getFieldName('adjustable_quantity.enabled') });
|
|
29
|
+
const minQuantity = useWatch({ control, name: getFieldName('adjustable_quantity.minimum') });
|
|
30
|
+
const maxQuantity = useWatch({ control, name: getFieldName('adjustable_quantity.maximum') });
|
|
31
|
+
|
|
32
|
+
const adjustableError = Number(minQuantity) >= Number(maxQuantity);
|
|
29
33
|
|
|
30
34
|
const onSave = async (updates: TProduct) => {
|
|
31
35
|
try {
|
|
@@ -110,24 +114,55 @@ export default function LineItem({ prefix, product, valid, onUpdate, onRemove }:
|
|
|
110
114
|
)}
|
|
111
115
|
/>
|
|
112
116
|
{adjustable && (
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
<>
|
|
118
|
+
<Stack direction="row" alignItems="center" mt={1} ml={6}>
|
|
119
|
+
<Typography sx={{ mr: 0.5 }}>Between</Typography>
|
|
120
|
+
<Controller
|
|
121
|
+
name={getFieldName('adjustable_quantity.minimum')}
|
|
122
|
+
control={control}
|
|
123
|
+
rules={{
|
|
124
|
+
validate: (value) => {
|
|
125
|
+
return !maxQuantity || Number(value) < Number(maxQuantity) || 'Minimum must be less than maximum';
|
|
126
|
+
},
|
|
127
|
+
}}
|
|
128
|
+
render={({ field }) => (
|
|
129
|
+
<TextField
|
|
130
|
+
sx={{ width: 40 }}
|
|
131
|
+
inputProps={{ style: { padding: '4px 8px' } }}
|
|
132
|
+
size="small"
|
|
133
|
+
error={!!adjustableError}
|
|
134
|
+
{...field}
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
/>
|
|
138
|
+
<Typography sx={{ mx: 0.5 }}>and</Typography>
|
|
139
|
+
<Controller
|
|
140
|
+
name={getFieldName('adjustable_quantity.maximum')}
|
|
141
|
+
control={control}
|
|
142
|
+
rules={{
|
|
143
|
+
validate: (value) => {
|
|
144
|
+
return (
|
|
145
|
+
!minQuantity || Number(value) > Number(minQuantity) || 'Maximum must be greater than minimum'
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
}}
|
|
149
|
+
render={({ field }) => (
|
|
150
|
+
<TextField
|
|
151
|
+
sx={{ width: 40 }}
|
|
152
|
+
inputProps={{ style: { padding: '4px 8px' } }}
|
|
153
|
+
size="small"
|
|
154
|
+
error={!!adjustableError}
|
|
155
|
+
{...field}
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
/>
|
|
159
|
+
</Stack>
|
|
160
|
+
{adjustableError && (
|
|
161
|
+
<Typography sx={{ mt: 0.5, ml: 6 }} color="error">
|
|
162
|
+
{t('admin.paymentLink.adjustableQuantityError')}
|
|
163
|
+
</Typography>
|
|
164
|
+
)}
|
|
165
|
+
</>
|
|
131
166
|
)}
|
|
132
167
|
</Stack>
|
|
133
168
|
{state.editing && (
|
|
@@ -73,17 +73,28 @@ export default function PricingTableActions({ data, variant, onChange }: Props)
|
|
|
73
73
|
Toast.success(t('common.copied'));
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
const onOpenLink = () => {
|
|
77
|
+
window.open(
|
|
78
|
+
joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pricing-table/${data.id}`),
|
|
79
|
+
'_blank'
|
|
80
|
+
);
|
|
81
|
+
};
|
|
76
82
|
return (
|
|
77
83
|
<ClickBoundary>
|
|
78
84
|
<Actions
|
|
79
85
|
variant={variant}
|
|
80
86
|
actions={[
|
|
81
87
|
{
|
|
82
|
-
label: t('admin.pricingTable.
|
|
83
|
-
handler:
|
|
88
|
+
label: t('admin.pricingTable.openLink'),
|
|
89
|
+
handler: onOpenLink,
|
|
84
90
|
color: 'primary',
|
|
85
|
-
disabled: true,
|
|
86
91
|
},
|
|
92
|
+
// {
|
|
93
|
+
// label: t('admin.pricingTable.edit'),
|
|
94
|
+
// handler: () => setState({ action: 'edit' }),
|
|
95
|
+
// color: 'primary',
|
|
96
|
+
// disabled: true,
|
|
97
|
+
// },
|
|
87
98
|
{
|
|
88
99
|
label: t('admin.pricingTable.copyLink'),
|
|
89
100
|
handler: onCopyLink,
|
|
@@ -22,7 +22,7 @@ export function PricePaymentSettings({ index }: { index: number }) {
|
|
|
22
22
|
formState: { errors },
|
|
23
23
|
} = useFormContext();
|
|
24
24
|
const type = useWatch({ control, name: getFieldName('after_completion.type') });
|
|
25
|
-
const nftMintEnabled = useWatch({ control, name: 'nft_mint_settings.enabled' });
|
|
25
|
+
const nftMintEnabled = useWatch({ control, name: getFieldName('nft_mint_settings.enabled') });
|
|
26
26
|
|
|
27
27
|
const values = getValues();
|
|
28
28
|
|
|
@@ -3,21 +3,31 @@ import type { TRefundExpanded } from '@blocklet/payment-types';
|
|
|
3
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
4
|
import type { LiteralUnion } from 'type-fest';
|
|
5
5
|
|
|
6
|
+
import { api, ConfirmDialog, formatError } from '@blocklet/payment-react';
|
|
7
|
+
import { useSetState } from 'ahooks';
|
|
8
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
6
9
|
import Actions from '../actions';
|
|
7
10
|
import ClickBoundary from '../click-boundary';
|
|
8
11
|
|
|
9
12
|
type Props = {
|
|
10
13
|
data: TRefundExpanded;
|
|
14
|
+
onChange?: (action: string) => void;
|
|
11
15
|
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
RefundActions.defaultProps = {
|
|
15
19
|
variant: 'compact',
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
21
|
+
onChange: () => {},
|
|
16
22
|
};
|
|
17
23
|
|
|
18
|
-
export default function RefundActions({ data, variant }: Props) {
|
|
24
|
+
export default function RefundActions({ data, variant, onChange }: Props) {
|
|
19
25
|
const { t } = useLocaleContext();
|
|
20
26
|
const navigate = useNavigate();
|
|
27
|
+
const [state, setState] = useSetState({
|
|
28
|
+
action: '',
|
|
29
|
+
loading: false,
|
|
30
|
+
});
|
|
21
31
|
|
|
22
32
|
const actions = [
|
|
23
33
|
{
|
|
@@ -35,10 +45,42 @@ export default function RefundActions({ data, variant }: Props) {
|
|
|
35
45
|
disabled: false,
|
|
36
46
|
});
|
|
37
47
|
}
|
|
48
|
+
if (!['succeeded', 'canceled'].includes(data.status)) {
|
|
49
|
+
actions.push({
|
|
50
|
+
label: t('admin.paymentIntent.cancelRefund'),
|
|
51
|
+
handler: () => setState({ action: 'cancel' }),
|
|
52
|
+
color: 'error',
|
|
53
|
+
disabled: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onCancel = async () => {
|
|
58
|
+
try {
|
|
59
|
+
setState({ loading: true });
|
|
60
|
+
await api.post(`/api/refunds/${data?.id}/cancel`);
|
|
61
|
+
Toast.success(t('admin.paymentIntent.refundCanceled'));
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
onChange(state.action);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err);
|
|
66
|
+
Toast.error(formatError(err));
|
|
67
|
+
} finally {
|
|
68
|
+
setState({ loading: false, action: '' });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
38
71
|
|
|
39
72
|
return (
|
|
40
73
|
<ClickBoundary>
|
|
41
74
|
<Actions variant={variant} actions={actions} />
|
|
75
|
+
{state.action === 'cancel' && (
|
|
76
|
+
<ConfirmDialog
|
|
77
|
+
onConfirm={onCancel}
|
|
78
|
+
onCancel={() => setState({ action: '' })}
|
|
79
|
+
title={t('admin.paymentIntent.cancelRefund')}
|
|
80
|
+
message={t('admin.paymentIntent.refundCanceledTip')}
|
|
81
|
+
loading={state.loading}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
42
84
|
</ClickBoundary>
|
|
43
85
|
);
|
|
44
86
|
}
|
|
@@ -222,7 +222,7 @@ export default function RefundList({
|
|
|
222
222
|
options: {
|
|
223
223
|
customBodyRenderLite: (_: string, index: number) => {
|
|
224
224
|
const item = data.list[index] as TRefundExpanded;
|
|
225
|
-
return <RefundActions data={item} />;
|
|
225
|
+
return <RefundActions data={item} onChange={() => refresh()} />;
|
|
226
226
|
},
|
|
227
227
|
},
|
|
228
228
|
},
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { api, dayjs, formatAmount, formatTime } from '@blocklet/payment-react';
|
|
3
|
-
import type { TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
3
|
+
import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
4
4
|
import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography, styled } from '@mui/material';
|
|
5
5
|
import { useRequest } from 'ahooks';
|
|
6
6
|
import { useEffect, useMemo } from 'react';
|
|
7
7
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
8
8
|
|
|
9
|
-
const fetchData = (
|
|
9
|
+
const fetchData = (
|
|
10
|
+
id: string,
|
|
11
|
+
time: string
|
|
12
|
+
): Promise<{ total: string; unused: string; paymentCurrency: TPaymentCurrency }> => {
|
|
10
13
|
return api.get(`/api/subscriptions/${id}/proration?time=${encodeURIComponent(time)}`).then((res: any) => res.data);
|
|
11
14
|
};
|
|
12
15
|
|
|
@@ -128,8 +131,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
128
131
|
onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'last')}
|
|
129
132
|
control={<Radio checked={refundType === 'last'} />}
|
|
130
133
|
label={t('admin.subscription.cancel.refund.last', {
|
|
131
|
-
symbol,
|
|
132
|
-
total: formatAmount(refund?.total || '0', decimal),
|
|
134
|
+
symbol: refund?.paymentCurrency?.symbol || symbol,
|
|
135
|
+
total: formatAmount(refund?.total || '0', refund?.paymentCurrency?.decimal || decimal),
|
|
133
136
|
})}
|
|
134
137
|
/>
|
|
135
138
|
<FormControlLabel
|
|
@@ -138,9 +141,9 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
|
|
|
138
141
|
onClick={() => !(loading || refundDisabled) && setValue('cancel.refund', 'proration')}
|
|
139
142
|
control={<Radio checked={refundType === 'proration'} />}
|
|
140
143
|
label={t('admin.subscription.cancel.refund.proration', {
|
|
141
|
-
symbol,
|
|
142
|
-
total: formatAmount(refund?.total || '0', decimal),
|
|
143
|
-
unused: formatAmount(refund?.unused || '0', decimal),
|
|
144
|
+
symbol: refund?.paymentCurrency?.symbol || symbol,
|
|
145
|
+
total: formatAmount(refund?.total || '0', refund?.paymentCurrency?.decimal || decimal),
|
|
146
|
+
unused: formatAmount(refund?.unused || '0', refund?.paymentCurrency?.decimal || decimal),
|
|
144
147
|
})}
|
|
145
148
|
/>
|
|
146
149
|
</RadioGroup>
|
|
@@ -40,7 +40,7 @@ export default function SubscriptionMetrics({ subscription }: Props) {
|
|
|
40
40
|
divider
|
|
41
41
|
/>
|
|
42
42
|
)}
|
|
43
|
-
{upcoming?.amount && upcoming.amount !== '0' && (
|
|
43
|
+
{['active', 'trialing'].includes(subscription.status) && upcoming?.amount && upcoming.amount !== '0' && (
|
|
44
44
|
<InfoMetric
|
|
45
45
|
label={t('admin.subscription.nextInvoiceAmount')}
|
|
46
46
|
value={`${formatBNStr(upcoming.amount, subscription.paymentCurrency.decimal)} ${
|
|
@@ -26,12 +26,14 @@ type ActionProps = {
|
|
|
26
26
|
type Props = {
|
|
27
27
|
subscription: TSubscriptionExpanded;
|
|
28
28
|
showExtra?: boolean;
|
|
29
|
+
showRecharge?: boolean;
|
|
29
30
|
onChange?: (action?: string) => any | Promise<any>;
|
|
30
31
|
actionProps?: ActionProps;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
SubscriptionActions.defaultProps = {
|
|
34
35
|
showExtra: false,
|
|
36
|
+
showRecharge: false,
|
|
35
37
|
onChange: null,
|
|
36
38
|
actionProps: {},
|
|
37
39
|
};
|
|
@@ -67,7 +69,14 @@ const fetchExtraActions = async ({
|
|
|
67
69
|
return { changePlan, batchPay };
|
|
68
70
|
};
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
const supportRecharge = (subscription: TSubscriptionExpanded) => {
|
|
73
|
+
return (
|
|
74
|
+
['active', 'trialing', 'past_due'].includes(subscription?.status) &&
|
|
75
|
+
['arcblock', 'ethereum'].includes(subscription?.paymentMethod?.type)
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function SubscriptionActionsInner({ subscription, showExtra, showRecharge, onChange, actionProps }: Props) {
|
|
71
80
|
const { t, locale } = useLocaleContext();
|
|
72
81
|
const { reset, getValues } = useFormContext();
|
|
73
82
|
const navigate = useNavigate();
|
|
@@ -121,6 +130,17 @@ export function SubscriptionActionsInner({ subscription, showExtra, onChange, ac
|
|
|
121
130
|
|
|
122
131
|
return (
|
|
123
132
|
<Stack direction="row" alignItems="center" gap={1} flexWrap="wrap">
|
|
133
|
+
{showRecharge && supportRecharge(subscription) && (
|
|
134
|
+
<Button
|
|
135
|
+
variant="outlined"
|
|
136
|
+
color="primary"
|
|
137
|
+
onClick={(e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
navigate(`/customer/subscription/${subscription.id}/recharge`);
|
|
140
|
+
}}>
|
|
141
|
+
{t('customer.recharge.title')}
|
|
142
|
+
</Button>
|
|
143
|
+
)}
|
|
124
144
|
{!extraActions?.batchPay && action && (
|
|
125
145
|
<Button
|
|
126
146
|
variant={action.variant as any}
|
|
@@ -233,6 +253,7 @@ export default function SubscriptionActions(props: Props) {
|
|
|
233
253
|
|
|
234
254
|
SubscriptionActionsInner.defaultProps = {
|
|
235
255
|
showExtra: false,
|
|
256
|
+
showRecharge: false,
|
|
236
257
|
onChange: null,
|
|
237
258
|
actionProps: {},
|
|
238
259
|
};
|