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.
Files changed (37) hide show
  1. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +5 -3
  2. package/api/src/libs/notification/template/subscription-canceled.ts +3 -3
  3. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +4 -3
  4. package/api/src/libs/notification/template/subscription-renew-failed.ts +5 -4
  5. package/api/src/libs/notification/template/subscription-renewed.ts +2 -1
  6. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +3 -4
  7. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -1
  8. package/api/src/libs/notification/template/subscription-upgraded.ts +6 -4
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +6 -3
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
  11. package/api/src/routes/connect/change-payment.ts +1 -0
  12. package/api/src/routes/connect/change-plan.ts +1 -0
  13. package/api/src/routes/connect/setup.ts +1 -0
  14. package/api/src/routes/connect/shared.ts +39 -33
  15. package/api/src/routes/connect/subscribe.ts +14 -8
  16. package/api/src/routes/customers.ts +79 -5
  17. package/api/src/routes/subscriptions.ts +13 -1
  18. package/api/src/store/models/invoice.ts +4 -2
  19. package/blocklet.yml +3 -3
  20. package/package.json +15 -15
  21. package/src/app.tsx +17 -17
  22. package/src/components/actions.tsx +32 -9
  23. package/src/components/copyable.tsx +2 -2
  24. package/src/components/layout/user.tsx +37 -0
  25. package/src/components/subscription/portal/actions.tsx +26 -5
  26. package/src/components/subscription/portal/list.tsx +24 -6
  27. package/src/components/subscription/status.tsx +2 -2
  28. package/src/libs/util.ts +15 -0
  29. package/src/pages/admin/payments/payouts/detail.tsx +6 -1
  30. package/src/pages/customer/index.tsx +247 -154
  31. package/src/pages/customer/invoice/detail.tsx +1 -1
  32. package/src/pages/customer/payout/detail.tsx +9 -2
  33. package/src/pages/customer/recharge.tsx +6 -2
  34. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  35. package/src/pages/customer/subscription/change-plan.tsx +1 -1
  36. package/src/pages/customer/subscription/detail.tsx +8 -3
  37. 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" mt={2}>
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={<DID did={paymentIntent?.customer?.did} />}
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?.scrollIntoView({
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', mt: 2, cursor: 'pointer' }}
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', mt: '16px', cursor: 'pointer' }}
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={{ mt: 2 }}>
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', mt: '16px' }}>
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={() => refresh()}
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 { data: subscription, error, loading } = useRequest(() => fetchSubscriptionData(subscriptionId, authToken));
70
- const { data } = useRequest(
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: <Status label={subscription.status} color={getSubscriptionStatusColor(subscription.status)} />,
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
- <Box
195
- className="mini-invoice-wrap"
196
- sx={{
197
- display: 'flex',
198
- flexDirection: 'column',
199
- alignItem: 'center',
200
- justifyContent: 'flex-start',
201
- padding: '8px',
202
- gap: '12px',
203
- width: '100%',
204
- height: '100%',
205
- }}>
206
- <Typography component="h2" sx={{ textAlign: 'center' }} variant="h3" gutterBottom>
207
- {t('payment.customer.subscriptions.current')}
208
- </Typography>
209
- <Box sx={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
210
- {infoList.map(({ name, value }) => {
211
- return <InfoRow label={name} value={value} direction="column" alignItems="flex-start" sx={{ mb: 0 }} />;
212
- })}
213
- </Box>
214
- <Divider />
215
- <Box sx={{ flex: 1, overflow: 'hidden' }}>
216
- <List sx={{ height: '100%', display: 'flex', flexDirection: 'column' }} className="mini-invoice-list">
217
- <ListSubheader disableGutters sx={{ padding: 0 }}>
218
- <Typography component="h2" variant="h6" fontSize="16px">
219
- {t('payment.customer.invoices')}
220
- </Typography>
221
- </ListSubheader>
222
- {(invoices as any).length === 0 ? (
223
- <Typography color="text.lighter">{t('payment.customer.invoice.empty')}</Typography>
224
- ) : (
225
- <Box sx={{ flex: 1, overflow: 'auto' }}>
226
- {(invoices as any).map((item: any) => {
227
- return (
228
- <ListItem key={item.id} disableGutters sx={{ display: 'flex', justifyContent: 'space-between' }}>
229
- <Typography component="div" sx={{ flex: 3, gap: 1, display: 'flex', alignItems: 'center' }}>
230
- <Typography component="span" sx={{ whiteSpace: 'nowrap' }}>
231
- {formatToDate(item.created_at, locale, 'YYYY-MM-DD')}
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)}&nbsp;
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
- {!isMobile && <Status label={getInvoiceDescriptionAndReason(item, locale)?.type} />}
234
- </Typography>
235
- <Typography component="span" sx={{ flex: 1, textAlign: 'right' }}>
236
- {formatBNStr(item.total, item.paymentCurrency.decimal)}&nbsp;
237
- {item.paymentCurrency.symbol}
238
- </Typography>
239
- <Typography component="span" sx={{ flex: 2, textAlign: 'right' }}>
240
- <Status label={item.status} color={getInvoiceStatusColor(item.status)} />
241
- </Typography>
242
- </ListItem>
243
- );
244
- })}
245
- </Box>
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
- </List>
313
+ </Stack>
248
314
  </Box>
249
- <Stack direction="row" justifyContent="center" spacing={2} sx={{ mt: 2 }}>
250
- {subscription.service_actions
251
- ?.filter((x: any) => x?.type !== 'notification')
252
- ?.map((x) => (
253
- // @ts-ignore
254
- <Button
255
- component={Link}
256
- key={x.name}
257
- variant={x?.variant || 'contained'}
258
- color={x.color || 'primary'}
259
- href={x.link}
260
- size="small"
261
- target="_blank"
262
- sx={{ textDecoration: 'none !important' }}>
263
- {x.text[locale] || x.text.en || x.name}
264
- </Button>
265
- ))}
266
- <Button
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
  }