payment-kit 1.18.11 → 1.18.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +5 -3
- package/api/src/libs/notification/template/subscription-canceled.ts +3 -3
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +4 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +5 -4
- package/api/src/libs/notification/template/subscription-renewed.ts +2 -1
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +3 -4
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -1
- package/api/src/libs/notification/template/subscription-upgraded.ts +6 -4
- package/api/src/libs/notification/template/subscription-will-canceled.ts +6 -3
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/routes/connect/change-payment.ts +1 -0
- package/api/src/routes/connect/change-plan.ts +1 -0
- package/api/src/routes/connect/setup.ts +1 -0
- package/api/src/routes/connect/shared.ts +39 -33
- package/api/src/routes/connect/subscribe.ts +14 -8
- package/api/src/routes/customers.ts +79 -5
- package/api/src/routes/subscriptions.ts +13 -1
- package/api/src/store/models/invoice.ts +4 -2
- package/blocklet.yml +3 -3
- package/package.json +15 -15
- package/src/app.tsx +17 -17
- package/src/components/actions.tsx +32 -9
- package/src/components/copyable.tsx +2 -2
- package/src/components/layout/user.tsx +37 -0
- package/src/components/subscription/portal/actions.tsx +26 -5
- package/src/components/subscription/portal/list.tsx +24 -6
- package/src/components/subscription/status.tsx +2 -2
- package/src/libs/util.ts +15 -0
- package/src/pages/admin/payments/payouts/detail.tsx +6 -1
- package/src/pages/customer/index.tsx +247 -154
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/payout/detail.tsx +9 -2
- package/src/pages/customer/recharge.tsx +6 -2
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +8 -3
- package/src/pages/customer/subscription/embed.tsx +142 -84
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formatTime,
|
|
10
10
|
getCustomerAvatar,
|
|
11
11
|
getPayoutStatusColor,
|
|
12
|
+
useMobile,
|
|
12
13
|
} from '@blocklet/payment-react';
|
|
13
14
|
import type { TCustomer, TPaymentLink, TPayoutExpanded } from '@blocklet/payment-types';
|
|
14
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -41,6 +42,7 @@ const InfoAlignItems = 'flex-start';
|
|
|
41
42
|
|
|
42
43
|
export default function PayoutDetail() {
|
|
43
44
|
const { t } = useLocaleContext();
|
|
45
|
+
const { isMobile } = useMobile();
|
|
44
46
|
const params = useParams<{ id: string }>();
|
|
45
47
|
const { loading, error, data } = useRequest(() => fetchData(params.id!), {
|
|
46
48
|
ready: !!params.id,
|
|
@@ -60,7 +62,7 @@ export default function PayoutDetail() {
|
|
|
60
62
|
return (
|
|
61
63
|
<Root direction="column" spacing={2.5} mb={4}>
|
|
62
64
|
<Box>
|
|
63
|
-
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center"
|
|
65
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
64
66
|
<Stack
|
|
65
67
|
direction="row"
|
|
66
68
|
alignItems="center"
|
|
@@ -177,7 +179,12 @@ export default function PayoutDetail() {
|
|
|
177
179
|
{paymentIntent?.customer?.name} ({paymentIntent?.customer?.email})
|
|
178
180
|
</Typography>
|
|
179
181
|
}
|
|
180
|
-
description={
|
|
182
|
+
description={
|
|
183
|
+
<DID
|
|
184
|
+
did={paymentIntent?.customer?.did}
|
|
185
|
+
{...(isMobile ? { responsive: false, compact: true } : {})}
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
181
188
|
size={40}
|
|
182
189
|
variant="rounded"
|
|
183
190
|
/>
|
|
@@ -45,7 +45,6 @@ const Root = styled(Stack)(({ theme }) => ({
|
|
|
45
45
|
gap: theme.spacing(3),
|
|
46
46
|
flexDirection: 'column',
|
|
47
47
|
margin: '0 auto',
|
|
48
|
-
padding: '20px 0',
|
|
49
48
|
}));
|
|
50
49
|
|
|
51
50
|
const BalanceCard = styled(Box)(({ theme }) => ({
|
|
@@ -141,7 +140,12 @@ export default function RechargePage() {
|
|
|
141
140
|
if (rechargeRef.current && subscription) {
|
|
142
141
|
setTimeout(() => {
|
|
143
142
|
// @ts-ignore
|
|
144
|
-
rechargeRef.current
|
|
143
|
+
const rechargePosition = rechargeRef.current.getBoundingClientRect();
|
|
144
|
+
const absoluteTop = window.scrollY + rechargePosition.top;
|
|
145
|
+
const scrollToPosition = absoluteTop - 20;
|
|
146
|
+
|
|
147
|
+
window.scrollTo({
|
|
148
|
+
top: scrollToPosition,
|
|
145
149
|
behavior: 'smooth',
|
|
146
150
|
});
|
|
147
151
|
}, 200);
|
|
@@ -220,7 +220,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
|
|
|
220
220
|
<Stack
|
|
221
221
|
direction="row"
|
|
222
222
|
alignItems="center"
|
|
223
|
-
sx={{ fontWeight: 'normal',
|
|
223
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}
|
|
224
224
|
onClick={() => goBackOrFallback(`/customer/subscription/${subscription.id}`)}>
|
|
225
225
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
226
226
|
<SubscriptionDescription subscription={subscription} variant="h5" />
|
|
@@ -236,7 +236,7 @@ export default function CustomerSubscriptionChangePlan() {
|
|
|
236
236
|
<Stack
|
|
237
237
|
direction="row"
|
|
238
238
|
alignItems="center"
|
|
239
|
-
sx={{ fontWeight: 'normal',
|
|
239
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}
|
|
240
240
|
onClick={() => goBackOrFallback(`/customer/subscription/${data.subscription.id}`)}>
|
|
241
241
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
242
242
|
<SubscriptionDescription subscription={data.subscription} variant="h5" />
|
|
@@ -230,7 +230,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
230
230
|
<Root>
|
|
231
231
|
<Box>
|
|
232
232
|
{hasUnpaid && (
|
|
233
|
-
<Alert severity="error" sx={{
|
|
233
|
+
<Alert severity="error" sx={{ mb: 2 }}>
|
|
234
234
|
{t('customer.unpaidInvoicesWarningTip')}
|
|
235
235
|
</Alert>
|
|
236
236
|
)}
|
|
@@ -239,7 +239,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
239
239
|
direction="row"
|
|
240
240
|
justifyContent="space-between"
|
|
241
241
|
alignItems="center"
|
|
242
|
-
sx={{ position: 'relative'
|
|
242
|
+
sx={{ position: 'relative' }}>
|
|
243
243
|
<Stack
|
|
244
244
|
direction="row"
|
|
245
245
|
onClick={() => navigate('/customer', { replace: true })}
|
|
@@ -253,7 +253,12 @@ export default function CustomerSubscriptionDetail() {
|
|
|
253
253
|
<Stack direction="row" gap={1}>
|
|
254
254
|
<SubscriptionActions
|
|
255
255
|
subscription={data}
|
|
256
|
-
onChange={() =>
|
|
256
|
+
onChange={(action) => {
|
|
257
|
+
refresh();
|
|
258
|
+
if (action === 'batch-pay') {
|
|
259
|
+
checkUnpaidInvoices();
|
|
260
|
+
}
|
|
261
|
+
}}
|
|
257
262
|
showExtra
|
|
258
263
|
showDelegation
|
|
259
264
|
showOverdraftProtection={{
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
getInvoiceDescriptionAndReason,
|
|
17
17
|
getInvoiceStatusColor,
|
|
18
18
|
getPrefix,
|
|
19
|
-
getSubscriptionStatusColor,
|
|
20
19
|
useDefaultPageSize,
|
|
21
20
|
useMobile,
|
|
21
|
+
OverdueInvoicePayment,
|
|
22
|
+
PaymentProvider,
|
|
22
23
|
} from '@blocklet/payment-react';
|
|
23
24
|
import type { Paginated, TInvoiceExpanded, TSubscriptionExpanded } from '@blocklet/payment-types';
|
|
24
25
|
import {
|
|
@@ -35,12 +36,16 @@ import {
|
|
|
35
36
|
Tooltip,
|
|
36
37
|
Typography,
|
|
37
38
|
} from '@mui/material';
|
|
38
|
-
import { useRequest } from 'ahooks';
|
|
39
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
39
40
|
import { useMemo } from 'react';
|
|
40
41
|
import { joinURL, withQuery } from 'ufo';
|
|
41
42
|
import prettyMs from 'pretty-ms-i18n';
|
|
43
|
+
import { isEmpty } from 'lodash';
|
|
44
|
+
import { PaymentOutlined } from '@mui/icons-material';
|
|
42
45
|
import InfoRow from '../../../components/info-row';
|
|
43
46
|
import InfoCard from '../../../components/info-card';
|
|
47
|
+
import { useSessionContext } from '../../../contexts/session';
|
|
48
|
+
import SubscriptionStatus from '../../../components/subscription/status';
|
|
44
49
|
|
|
45
50
|
const fetchInvoiceData = (params: Record<string, any> = {}): Promise<Paginated<TInvoiceExpanded>> => {
|
|
46
51
|
const search = new URLSearchParams();
|
|
@@ -58,16 +63,39 @@ const fetchSubscriptionData = (id: string, authToken: string): Promise<TSubscrip
|
|
|
58
63
|
return api.get(`/api/subscriptions/${id}?authToken=${authToken}`).then((res) => res.data);
|
|
59
64
|
};
|
|
60
65
|
|
|
66
|
+
const checkHasPastDue = async (subscriptionId: string): Promise<boolean> => {
|
|
67
|
+
try {
|
|
68
|
+
const res = await api.get(`/api/subscriptions/${subscriptionId}/summary`);
|
|
69
|
+
if (!isEmpty(res.data) && Object.keys(res.data).length >= 1) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to check past due:', error);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
61
79
|
export default function SubscriptionEmbed() {
|
|
62
80
|
const { t, locale } = useLocaleContext();
|
|
63
81
|
const params = new URL(window.location.href).searchParams;
|
|
82
|
+
const [state, setState] = useSetState({
|
|
83
|
+
batchPay: false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { session, connectApi } = useSessionContext();
|
|
64
87
|
|
|
65
88
|
const subscriptionId = params.get('id') || '';
|
|
66
89
|
const authToken = params.get('authToken') || '';
|
|
67
90
|
const defaultPageSize = useDefaultPageSize(20);
|
|
68
91
|
const { isMobile } = useMobile();
|
|
69
|
-
const {
|
|
70
|
-
|
|
92
|
+
const {
|
|
93
|
+
data: subscription,
|
|
94
|
+
error,
|
|
95
|
+
loading,
|
|
96
|
+
runAsync: refreshSubscription,
|
|
97
|
+
} = useRequest(() => fetchSubscriptionData(subscriptionId, authToken));
|
|
98
|
+
const { data, runAsync: refreshInvoices } = useRequest(
|
|
71
99
|
() =>
|
|
72
100
|
fetchInvoiceData({
|
|
73
101
|
page: 1,
|
|
@@ -98,6 +126,10 @@ export default function SubscriptionEmbed() {
|
|
|
98
126
|
});
|
|
99
127
|
}, [subscription]);
|
|
100
128
|
|
|
129
|
+
const { data: hasPastDue, runAsync: runCheckHasPastDue } = useRequest(() => checkHasPastDue(subscriptionId), {
|
|
130
|
+
refreshDeps: [subscriptionId],
|
|
131
|
+
});
|
|
132
|
+
|
|
101
133
|
if (error) {
|
|
102
134
|
return (
|
|
103
135
|
<Position>
|
|
@@ -126,7 +158,7 @@ export default function SubscriptionEmbed() {
|
|
|
126
158
|
},
|
|
127
159
|
{
|
|
128
160
|
name: t('common.status'),
|
|
129
|
-
value: <
|
|
161
|
+
value: <SubscriptionStatus subscription={subscription} />,
|
|
130
162
|
},
|
|
131
163
|
];
|
|
132
164
|
|
|
@@ -183,7 +215,7 @@ export default function SubscriptionEmbed() {
|
|
|
183
215
|
48
|
|
184
216
|
)}
|
|
185
217
|
name={`${subscription.customer.name} (${subscription.customer.email})`}
|
|
186
|
-
description={<DidAddress did={subscription.customer.did} responsive />}
|
|
218
|
+
description={<DidAddress did={subscription.customer.did} responsive={false} compact />}
|
|
187
219
|
/>
|
|
188
220
|
),
|
|
189
221
|
});
|
|
@@ -191,87 +223,113 @@ export default function SubscriptionEmbed() {
|
|
|
191
223
|
|
|
192
224
|
return (
|
|
193
225
|
<Position>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
<Typography component="
|
|
231
|
-
{
|
|
226
|
+
<PaymentProvider session={session} connect={connectApi}>
|
|
227
|
+
<Box
|
|
228
|
+
className="mini-invoice-wrap"
|
|
229
|
+
sx={{
|
|
230
|
+
display: 'flex',
|
|
231
|
+
flexDirection: 'column',
|
|
232
|
+
alignItem: 'center',
|
|
233
|
+
justifyContent: 'flex-start',
|
|
234
|
+
padding: '8px',
|
|
235
|
+
gap: '12px',
|
|
236
|
+
width: '100%',
|
|
237
|
+
height: '100%',
|
|
238
|
+
}}>
|
|
239
|
+
<Typography component="h2" sx={{ textAlign: 'center' }} variant="h3" gutterBottom>
|
|
240
|
+
{t('payment.customer.subscriptions.current')}
|
|
241
|
+
</Typography>
|
|
242
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
243
|
+
{infoList.map(({ name, value }) => {
|
|
244
|
+
return <InfoRow label={name} value={value} direction="column" alignItems="flex-start" sx={{ mb: 0 }} />;
|
|
245
|
+
})}
|
|
246
|
+
</Box>
|
|
247
|
+
<Divider />
|
|
248
|
+
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
|
249
|
+
<List sx={{ height: '100%', display: 'flex', flexDirection: 'column' }} className="mini-invoice-list">
|
|
250
|
+
<ListSubheader disableGutters sx={{ padding: 0 }}>
|
|
251
|
+
<Typography component="h2" variant="h6" fontSize="16px">
|
|
252
|
+
{t('payment.customer.invoices')}
|
|
253
|
+
</Typography>
|
|
254
|
+
</ListSubheader>
|
|
255
|
+
{(invoices as any).length === 0 ? (
|
|
256
|
+
<Typography color="text.lighter">{t('payment.customer.invoice.empty')}</Typography>
|
|
257
|
+
) : (
|
|
258
|
+
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
|
259
|
+
{(invoices as any).map((item: any) => {
|
|
260
|
+
return (
|
|
261
|
+
<ListItem key={item.id} disableGutters sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
262
|
+
<Typography component="div" sx={{ flex: 3, gap: 1, display: 'flex', alignItems: 'center' }}>
|
|
263
|
+
<Typography component="span" sx={{ whiteSpace: 'nowrap' }}>
|
|
264
|
+
{formatToDate(item.created_at, locale, 'YYYY-MM-DD')}
|
|
265
|
+
</Typography>
|
|
266
|
+
{!isMobile && <Status label={getInvoiceDescriptionAndReason(item, locale)?.type} />}
|
|
267
|
+
</Typography>
|
|
268
|
+
<Typography component="span" sx={{ flex: 1, textAlign: 'right' }}>
|
|
269
|
+
{formatBNStr(item.total, item.paymentCurrency.decimal)}
|
|
270
|
+
{item.paymentCurrency.symbol}
|
|
271
|
+
</Typography>
|
|
272
|
+
<Typography component="span" sx={{ flex: 2, textAlign: 'right' }}>
|
|
273
|
+
<Status label={item.status} color={getInvoiceStatusColor(item.status)} />
|
|
232
274
|
</Typography>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
</ListItem>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</Box>
|
|
279
|
+
)}
|
|
280
|
+
</List>
|
|
281
|
+
</Box>
|
|
282
|
+
<Stack direction="row" justifyContent="center" spacing={2} sx={{ mt: 2 }}>
|
|
283
|
+
{subscription.service_actions
|
|
284
|
+
?.filter((x: any) => x?.type !== 'notification')
|
|
285
|
+
?.map((x) => (
|
|
286
|
+
// @ts-ignore
|
|
287
|
+
<Button
|
|
288
|
+
component={Link}
|
|
289
|
+
key={x.name}
|
|
290
|
+
variant={x?.variant || 'contained'}
|
|
291
|
+
color={x.color || 'primary'}
|
|
292
|
+
href={x.link}
|
|
293
|
+
size="small"
|
|
294
|
+
target="_blank"
|
|
295
|
+
sx={{ textDecoration: 'none !important' }}>
|
|
296
|
+
{x.text[locale] || x.text.en || x.name}
|
|
297
|
+
</Button>
|
|
298
|
+
))}
|
|
299
|
+
<Button
|
|
300
|
+
variant="contained"
|
|
301
|
+
sx={{ color: '#fff!important', width: subscription.service_actions?.length ? 'auto' : '100%' }}
|
|
302
|
+
target="_blank"
|
|
303
|
+
href={subscriptionPageUrl}>
|
|
304
|
+
{t('payment.customer.subscriptions.view')}
|
|
305
|
+
</Button>
|
|
306
|
+
{hasPastDue && (
|
|
307
|
+
<Tooltip title={t('admin.subscription.batchPay.button')}>
|
|
308
|
+
<Button variant="outlined" color="error" onClick={() => setState({ batchPay: true })}>
|
|
309
|
+
<PaymentOutlined />
|
|
310
|
+
</Button>
|
|
311
|
+
</Tooltip>
|
|
246
312
|
)}
|
|
247
|
-
</
|
|
313
|
+
</Stack>
|
|
248
314
|
</Box>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
variant="contained"
|
|
268
|
-
sx={{ color: '#fff!important', width: subscription.service_actions?.length ? 'auto' : '100%' }}
|
|
269
|
-
target="_blank"
|
|
270
|
-
href={subscriptionPageUrl}>
|
|
271
|
-
{t('payment.customer.subscriptions.view')}
|
|
272
|
-
</Button>
|
|
273
|
-
</Stack>
|
|
274
|
-
</Box>
|
|
315
|
+
{state.batchPay && (
|
|
316
|
+
<OverdueInvoicePayment
|
|
317
|
+
subscriptionId={subscriptionId}
|
|
318
|
+
onPaid={() => {
|
|
319
|
+
setState({
|
|
320
|
+
batchPay: false,
|
|
321
|
+
});
|
|
322
|
+
refreshSubscription();
|
|
323
|
+
runCheckHasPastDue();
|
|
324
|
+
refreshInvoices();
|
|
325
|
+
}}
|
|
326
|
+
dialogProps={{
|
|
327
|
+
open: state.batchPay,
|
|
328
|
+
onClose: () => setState({ batchPay: false }),
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
)}
|
|
332
|
+
</PaymentProvider>
|
|
275
333
|
</Position>
|
|
276
334
|
);
|
|
277
335
|
}
|