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
package/src/app.tsx CHANGED
@@ -13,7 +13,7 @@ import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-d
13
13
  import { joinURL } from 'ufo';
14
14
 
15
15
  import ErrorFallback from './components/error-fallback';
16
- import Layout from './components/layout/admin';
16
+ import UserLayout from './components/layout/user';
17
17
  import { TransitionProvider } from './components/progress-bar';
18
18
  import { SessionProvider } from './contexts/session';
19
19
  import { translations } from './locales';
@@ -68,45 +68,45 @@ function App() {
68
68
  key="customer-home"
69
69
  path="/customer"
70
70
  element={
71
- <Layout padding="0">
71
+ <UserLayout>
72
72
  <CustomerHome />
73
- </Layout>
73
+ </UserLayout>
74
74
  }
75
75
  />
76
76
  <Route
77
77
  key="customer-subscription"
78
78
  path="/customer/subscription/:id"
79
79
  element={
80
- <Layout>
80
+ <UserLayout>
81
81
  <CustomerSubscriptionDetail />
82
- </Layout>
82
+ </UserLayout>
83
83
  }
84
84
  />
85
85
  <Route
86
86
  key="customer-subscription-change-plan"
87
87
  path="/customer/subscription/:id/change-plan"
88
88
  element={
89
- <Layout>
89
+ <UserLayout>
90
90
  <CustomerSubscriptionChangePlan />
91
- </Layout>
91
+ </UserLayout>
92
92
  }
93
93
  />
94
94
  <Route
95
95
  key="customer-subscription-change-payment"
96
96
  path="/customer/subscription/:id/change-payment"
97
97
  element={
98
- <Layout>
98
+ <UserLayout>
99
99
  <CustomerSubscriptionChangePayment />
100
- </Layout>
100
+ </UserLayout>
101
101
  }
102
102
  />
103
103
  <Route
104
104
  key="customer-recharge"
105
105
  path="/customer/subscription/:id/recharge"
106
106
  element={
107
- <Layout>
107
+ <UserLayout>
108
108
  <CustomerRecharge />
109
- </Layout>
109
+ </UserLayout>
110
110
  }
111
111
  />
112
112
  <Route
@@ -125,27 +125,27 @@ function App() {
125
125
  key="customer-due"
126
126
  path="/customer/invoice/past-due"
127
127
  element={
128
- <Layout>
128
+ <UserLayout>
129
129
  <CustomerInvoicePastDue />
130
- </Layout>
130
+ </UserLayout>
131
131
  }
132
132
  />
133
133
  <Route
134
134
  key="customer-invoice"
135
135
  path="/customer/invoice/:id"
136
136
  element={
137
- <Layout>
137
+ <UserLayout>
138
138
  <CustomerInvoiceDetail />
139
- </Layout>
139
+ </UserLayout>
140
140
  }
141
141
  />
142
142
  <Route
143
143
  key="customer-payout"
144
144
  path="/customer/payout/:id"
145
145
  element={
146
- <Layout>
146
+ <UserLayout>
147
147
  <CustomerPayoutDetail />
148
- </Layout>
148
+ </UserLayout>
149
149
  }
150
150
  />
151
151
  <Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
@@ -16,7 +16,7 @@ type ActionItem = {
16
16
 
17
17
  export type ActionsProps = {
18
18
  actions: ActionItem[];
19
- variant?: LiteralUnion<'compact' | 'normal', string>;
19
+ variant?: LiteralUnion<'compact' | 'normal' | 'outlined', string>;
20
20
  sx?: any;
21
21
  onOpenCallback?: Function;
22
22
  };
@@ -58,17 +58,40 @@ export default function Actions(props: ActionsProps) {
58
58
  }
59
59
  };
60
60
 
61
- return (
62
- <>
63
- {props.variant === 'compact' ? (
61
+ const renderButton = () => {
62
+ if (props.variant === 'outlined') {
63
+ return (
64
+ <Button
65
+ aria-label="actions"
66
+ sx={{
67
+ minWidth: 0,
68
+ padding: '5px',
69
+ ...props.sx,
70
+ }}
71
+ variant="outlined"
72
+ onClick={onOpen}
73
+ size="small">
74
+ <MoreHorizOutlined />
75
+ </Button>
76
+ );
77
+ }
78
+ if (props.variant === 'compact') {
79
+ return (
64
80
  <IconButton aria-label="actions" sx={props.sx} aria-haspopup="true" onClick={onOpen} size="small">
65
81
  <MoreHorizOutlined />
66
82
  </IconButton>
67
- ) : (
68
- <Button sx={props.sx} onClick={onOpen} size="small" variant="contained" color="primary">
69
- {t('common.actions')} <ExpandMoreOutlined fontSize="small" />
70
- </Button>
71
- )}
83
+ );
84
+ }
85
+ return (
86
+ <Button sx={props.sx} onClick={onOpen} size="small" variant="contained" color="primary">
87
+ {t('common.actions')} <ExpandMoreOutlined fontSize="small" />
88
+ </Button>
89
+ );
90
+ };
91
+
92
+ return (
93
+ <>
94
+ {renderButton()}
72
95
  <Menu
73
96
  anchorEl={anchorEl}
74
97
  open={open}
@@ -1,7 +1,7 @@
1
1
  import { CopyButton } from '@arcblock/ux/lib/ClickToCopy';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { getWordBreakStyle } from '@blocklet/payment-react';
4
- import { Box, Stack, Typography } from '@mui/material';
4
+ import { Stack, Typography } from '@mui/material';
5
5
  import type { ReactNode } from 'react';
6
6
 
7
7
  interface CopyableProps {
@@ -35,7 +35,7 @@ export default function Copyable({ text, children, style }: CopyableProps) {
35
35
  {text}
36
36
  </Typography>
37
37
  )}
38
- <Box sx={{ height: '1em', mt: '-0.4em' }}>{copyButton}</Box>
38
+ <Typography sx={{ display: 'inline-flex', '> span': { display: 'inline-flex' } }}>{copyButton}</Typography>
39
39
  </Stack>
40
40
  )}
41
41
  />
@@ -0,0 +1,37 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import { PaymentProvider } from '@blocklet/payment-react';
3
+ import { UserCenter } from '@blocklet/ui-react';
4
+ import { useEffect } from 'react';
5
+
6
+ import { useSessionContext } from '../../contexts/session';
7
+
8
+ export default function UserLayout(props: any) {
9
+ const { session, connectApi, events } = useSessionContext();
10
+
11
+ useEffect(() => {
12
+ events.once('logout', () => {
13
+ window.location.href = `${window.location.origin}/.well-known/service/user`;
14
+ });
15
+ }, []);
16
+
17
+ useEffect(() => {
18
+ if (session.initialized && !session.user) {
19
+ // @ts-ignore
20
+ session.login(() => {}, { openMode: 'redirect', redirect: window.location.href });
21
+ }
22
+ }, [session.initialized]);
23
+
24
+ if (session.user) {
25
+ return (
26
+ <PaymentProvider session={session} connect={connectApi}>
27
+ <UserCenter
28
+ currentTab={`${window.blocklet.prefix}customer`}
29
+ userDid={session.user.did}
30
+ notLoginContent="undefined">
31
+ {props.children}
32
+ </UserCenter>
33
+ </PaymentProvider>
34
+ );
35
+ }
36
+ return null;
37
+ }
@@ -9,6 +9,7 @@ import {
9
9
  getPrefix,
10
10
  getSubscriptionAction,
11
11
  usePaymentContext,
12
+ OverdueInvoicePayment,
12
13
  } from '@blocklet/payment-react';
13
14
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
14
15
  import { Button, Link, Stack, Tooltip } from '@mui/material';
@@ -22,6 +23,7 @@ import CustomerCancelForm from './cancel';
22
23
  import OverdraftProtectionDialog from '../../customer/overdraft-protection';
23
24
  import Actions from '../../actions';
24
25
  import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
26
+ import { isWillCanceled } from '../../../libs/util';
25
27
 
26
28
  interface ActionConfig {
27
29
  key: string;
@@ -154,6 +156,7 @@ export function SubscriptionActionsInner({
154
156
  openProtection: false,
155
157
  protectionLoading: false,
156
158
  protectionInitValues: null,
159
+ batchPay: false,
157
160
  });
158
161
 
159
162
  const shouldFetchDelegation = showDelegation && ['active', 'trialing', 'past_due'].includes(subscription?.status);
@@ -320,7 +323,7 @@ export function SubscriptionActionsInner({
320
323
  const renderActions = () => {
321
324
  const supportUnsubscribe = action?.action === 'cancel' && showUnsubscribe;
322
325
  const supportAction = action && (action?.action !== 'cancel' || supportUnsubscribe);
323
-
326
+ const supportResume = isWillCanceled(subscription) && action?.action === 'recover';
324
327
  const serviceActions = subscription.service_actions?.filter((x: any) => x?.type !== 'notification') || [];
325
328
  const actionConfigs: ActionConfig[] = [
326
329
  {
@@ -351,7 +354,7 @@ export function SubscriptionActionsInner({
351
354
  },
352
355
  variant: 'outlined',
353
356
  color: 'primary',
354
- primary: true,
357
+ primary: !isWillCanceled(subscription),
355
358
  },
356
359
  {
357
360
  key: 'changePlan',
@@ -371,7 +374,9 @@ export function SubscriptionActionsInner({
371
374
  label: action?.text || t('admin.subscription.batchPay.button'),
372
375
  onClick: (e) => {
373
376
  e?.stopPropagation();
374
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions?.batchPay}`);
377
+ setState({
378
+ batchPay: true,
379
+ });
375
380
  },
376
381
  variant: 'outlined',
377
382
  color: 'error',
@@ -397,6 +402,7 @@ export function SubscriptionActionsInner({
397
402
  color: action?.color || 'primary',
398
403
  sx: action?.sx,
399
404
  divider: serviceActions.length > 0,
405
+ primary: supportResume,
400
406
  },
401
407
  // @ts-ignore
402
408
  ...serviceActions.map((x) => ({
@@ -445,7 +451,7 @@ export function SubscriptionActionsInner({
445
451
  </Button>
446
452
  );
447
453
  if (mode === 'menu-only') {
448
- return <Actions actions={visibleActions.map(toMenuItem)} />;
454
+ return <Actions actions={visibleActions.map(toMenuItem)} variant="outlined" />;
449
455
  }
450
456
 
451
457
  if (mode === 'primary-buttons') {
@@ -454,7 +460,7 @@ export function SubscriptionActionsInner({
454
460
  return (
455
461
  <>
456
462
  {primaryButtons.map(toButton)}
457
- {menuItems.length > 0 && <Actions actions={menuItems.map(toMenuItem)} />}
463
+ {menuItems.length > 0 && <Actions actions={menuItems.map(toMenuItem)} variant="outlined" />}
458
464
  </>
459
465
  );
460
466
  }
@@ -506,6 +512,21 @@ export function SubscriptionActionsInner({
506
512
  initValues={state.protectionInitValues}
507
513
  />
508
514
  )}
515
+
516
+ {state.batchPay && (
517
+ <OverdueInvoicePayment
518
+ subscriptionId={subscription.id}
519
+ onPaid={() => {
520
+ setState({ batchPay: false });
521
+ onChange?.('batch-pay');
522
+ }}
523
+ inSubscriptionDetail
524
+ dialogProps={{
525
+ open: state.batchPay,
526
+ onClose: () => setState({ batchPay: false }),
527
+ }}
528
+ />
529
+ )}
509
530
  </Stack>
510
531
  );
511
532
  }
@@ -2,7 +2,7 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Empty from '@arcblock/ux/lib/Empty';
4
4
  import { api, formatPrice, getSubscriptionTimeSummary, useMobile } from '@blocklet/payment-react';
5
- import type { Paginated, TSubscriptionExpanded } from '@blocklet/payment-types';
5
+ import type { TSubscriptionExpanded } from '@blocklet/payment-types';
6
6
  import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
7
7
  import { useInfiniteScroll } from 'ahooks';
8
8
 
@@ -11,8 +11,16 @@ import SubscriptionDescription from '../description';
11
11
  import SubscriptionActions from './actions';
12
12
  import SubscriptionStatus from '../status';
13
13
  import useDelayedLoading from '../../../hooks/loading';
14
+ import { isWillCanceled } from '../../../libs/util';
14
15
 
15
- const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TSubscriptionExpanded>> => {
16
+ type SubscriptionListResponse = {
17
+ count: number;
18
+ list: TSubscriptionExpanded[];
19
+ paging: { page: number; pageSize: number };
20
+ totalCount: number;
21
+ };
22
+
23
+ const fetchData = (params: Record<string, any> = {}): Promise<SubscriptionListResponse> => {
16
24
  const search = new URLSearchParams();
17
25
  Object.keys(params).forEach((key) => {
18
26
  search.set(key, String(params[key]));
@@ -27,6 +35,7 @@ type Props = {
27
35
  onClickSubscription: (subscription: TSubscriptionExpanded) => void | Promise<void>;
28
36
  onlyActive?: boolean;
29
37
  changeActive?: (active: boolean) => void;
38
+ setStatusState?: (state: boolean) => void;
30
39
  } & Omit<StackProps, 'onChange'>;
31
40
 
32
41
  const pageSize = 5;
@@ -38,19 +47,25 @@ export default function CurrentSubscriptions({
38
47
  onClickSubscription,
39
48
  onlyActive,
40
49
  changeActive = () => {},
50
+ setStatusState = () => {},
41
51
  ...rest
42
52
  }: Props) {
43
53
  const { t } = useLocaleContext();
44
54
  const { isMobile } = useMobile();
45
55
  const listRef = useRef<HTMLDivElement | null>(null);
46
56
 
47
- const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
57
+ const { data, loadMore, loadingMore, loading, reload } = useInfiniteScroll<SubscriptionListResponse>(
48
58
  (d) => {
49
59
  const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
50
- return fetchData({ page, pageSize, status, customer_id: id, activeFirst: true });
60
+ return fetchData({ page, pageSize, status, customer_id: id, activeFirst: true, showTotalCount: true });
51
61
  },
52
62
  {
53
63
  reloadDeps: [id, status],
64
+ onSuccess(res) {
65
+ if (res.totalCount > 0 || res.count > 0) {
66
+ setStatusState(true);
67
+ }
68
+ },
54
69
  ...(isMobile
55
70
  ? {}
56
71
  : {
@@ -68,6 +83,8 @@ export default function CurrentSubscriptions({
68
83
  return <CircularProgress />;
69
84
  }
70
85
 
86
+ const hasAnySubscriptions = data.totalCount > 0;
87
+
71
88
  const hasMore = data && data.list?.length < data.count;
72
89
  const size = { width: 48, height: 48 };
73
90
 
@@ -193,7 +210,7 @@ export default function CurrentSubscriptions({
193
210
  }
194
211
  }}
195
212
  showUnsubscribe={false}
196
- showRecharge
213
+ showRecharge={!isWillCanceled(subscription)}
197
214
  actionProps={{
198
215
  cancel: {
199
216
  variant: 'outlined',
@@ -239,7 +256,7 @@ export default function CurrentSubscriptions({
239
256
  </>
240
257
  ) : (
241
258
  <Empty>
242
- {onlyActive ? (
259
+ {onlyActive && hasAnySubscriptions ? (
243
260
  <Box sx={{ textAlign: 'center' }}>
244
261
  <Typography>{t('admin.subscription.noActiveEmpty')}</Typography>
245
262
  {changeActive && (
@@ -261,4 +278,5 @@ CurrentSubscriptions.defaultProps = {
261
278
  onChange: null,
262
279
  onlyActive: false,
263
280
  changeActive: null,
281
+ setStatusState: null,
264
282
  };
@@ -17,7 +17,7 @@ export default function SubscriptionStatus({
17
17
  <Status
18
18
  icon={<AccessTimeOutlined />}
19
19
  label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.current_period_end * 1000) })}
20
- color="default"
20
+ color="warning"
21
21
  {...rest}
22
22
  />
23
23
  );
@@ -28,7 +28,7 @@ export default function SubscriptionStatus({
28
28
  <Status
29
29
  icon={<AccessTimeOutlined />}
30
30
  label={t('admin.subscription.cancel.will', { date: formatToDate(subscription.cancel_at * 1000) })}
31
- color="default"
31
+ color="warning"
32
32
  {...rest}
33
33
  />
34
34
  );
package/src/libs/util.ts CHANGED
@@ -377,3 +377,18 @@ export function getTokenBalanceLink(method: TPaymentMethod, address: string) {
377
377
  }
378
378
  return '';
379
379
  }
380
+
381
+ export function isWillCanceled(subscription: TSubscriptionExpanded) {
382
+ const now = Date.now() / 1000;
383
+ if (
384
+ ['active', 'trialing'].includes(subscription.status) &&
385
+ subscription.cancel_at_period_end &&
386
+ subscription.current_period_end > now
387
+ ) {
388
+ return true;
389
+ }
390
+ if (subscription.cancel_at && subscription.cancel_at > now) {
391
+ return true;
392
+ }
393
+ return false;
394
+ }
@@ -224,7 +224,12 @@ export default function PayoutDetail(props: { id: string }) {
224
224
  {paymentIntent?.customer?.name} ({paymentIntent?.customer?.email})
225
225
  </Typography>
226
226
  }
227
- description={<DID did={paymentIntent?.customer?.did} />}
227
+ description={
228
+ <DID
229
+ did={paymentIntent?.customer?.did}
230
+ {...(isMobile ? { responsive: false, compact: true } : {})}
231
+ />
232
+ }
228
233
  size={40}
229
234
  variant="rounded"
230
235
  />