payment-kit 1.17.2 → 1.17.3

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.
@@ -3,21 +3,36 @@ import { api, formatBNStr, formatTime } from '@blocklet/payment-react';
3
3
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
4
4
  import { useRequest } from 'ahooks';
5
5
 
6
+ import { Button, Stack, Typography, Tooltip, Avatar, Box, CircularProgress, Skeleton } from '@mui/material';
7
+ import { BN } from '@ocap/util';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import { ArrowForward, InfoOutlined } from '@mui/icons-material';
6
10
  import InfoMetric from '../info-metric';
7
11
  import SubscriptionStatus from './status';
8
12
 
9
13
  type Props = {
10
14
  subscription: TSubscriptionExpanded;
15
+ showBalance?: boolean;
11
16
  };
12
17
 
13
18
  const fetchUpcoming = (id: string): Promise<{ amount: string }> => {
14
19
  return api.get(`/api/subscriptions/${id}/upcoming`).then((res) => res.data);
15
20
  };
16
21
 
17
- export default function SubscriptionMetrics({ subscription }: Props) {
22
+ const fetchPayer = (id: string): Promise<{ token: string; paymentAddress: string }> => {
23
+ return api.get(`/api/subscriptions/${id}/payer-token`).then((res) => res.data);
24
+ };
25
+
26
+ export default function SubscriptionMetrics({ subscription, showBalance = true }: Props) {
18
27
  const { t } = useLocaleContext();
19
- const { data: upcoming } = useRequest(() => fetchUpcoming(subscription.id));
28
+ const { data: upcoming, loading: upcomingLoading } = useRequest(() => fetchUpcoming(subscription.id));
29
+ const navigate = useNavigate();
30
+
31
+ const { data: payerValue, loading: payerLoading } = useRequest(() => fetchPayer(subscription.id), {
32
+ ready: showBalance,
33
+ });
20
34
 
35
+ const supportShowBalance = showBalance && ['arcblock', 'ethereum'].includes(subscription.paymentMethod.type);
21
36
  // let scheduleToCancelTime = 0;
22
37
  // if (['active', 'trialing', 'past_due'].includes(subscription.status) && subscription.cancel_at) {
23
38
  // scheduleToCancelTime = subscription.cancel_at * 1000;
@@ -25,14 +40,43 @@ export default function SubscriptionMetrics({ subscription }: Props) {
25
40
  // scheduleToCancelTime = subscription.current_period_end * 1000;
26
41
  // }
27
42
 
43
+ const isInsufficientBalance = new BN(payerValue?.token || '0').lt(new BN(upcoming?.amount || '0'));
44
+ const renderBalanceValue = () => {
45
+ if (upcomingLoading || payerLoading) {
46
+ return <CircularProgress size={16} />;
47
+ }
48
+
49
+ if (isInsufficientBalance) {
50
+ return (
51
+ <Button
52
+ component="a"
53
+ color="error"
54
+ onClick={() => navigate(`/customer/subscription/${subscription.id}/recharge`)}>
55
+ {t('admin.subscription.insufficientBalance')}
56
+ <ArrowForward sx={{ fontSize: '14px' }} />
57
+ </Button>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <Stack flexDirection="row" alignItems="center" gap={0.5} sx={{ fontSize: '16px', fontWeight: 500 }}>
63
+ <Avatar
64
+ src={subscription.paymentCurrency?.logo}
65
+ sx={{ width: 16, height: 16 }}
66
+ alt={subscription.paymentCurrency?.name}
67
+ />
68
+ <Box display="flex" alignItems="baseline">
69
+ {formatBNStr(payerValue?.token, subscription.paymentCurrency.decimal)}
70
+ <Typography sx={{ fontSize: '14px', color: 'text.secondary', ml: 0.5 }}>
71
+ {subscription.paymentCurrency.symbol}
72
+ </Typography>
73
+ </Box>
74
+ </Stack>
75
+ );
76
+ };
28
77
  return (
29
78
  <>
30
79
  <InfoMetric label={t('common.status')} value={<SubscriptionStatus subscription={subscription} />} divider />
31
- <InfoMetric
32
- label={t('admin.subscription.startedAt')}
33
- value={formatTime(subscription.start_date ? subscription.start_date * 1000 : subscription.created_at)}
34
- divider
35
- />
36
80
  {subscription.status === 'active' && !subscription.cancel_at && (
37
81
  <InfoMetric
38
82
  label={t('admin.subscription.nextInvoice')}
@@ -49,6 +93,34 @@ export default function SubscriptionMetrics({ subscription }: Props) {
49
93
  divider
50
94
  />
51
95
  )}
96
+ {supportShowBalance && ['active', 'trialing'].includes(subscription.status) && (
97
+ <InfoMetric
98
+ label={
99
+ <Stack direction="row" spacing={1} alignItems="center">
100
+ <Typography>{t('admin.subscription.currentBalance')}</Typography>
101
+ <Tooltip
102
+ title={
103
+ <Typography sx={{ fontFamily: 'monospace', fontSize: '13px' }}>
104
+ {t('admin.subscription.paymentAddress')}:
105
+ {payerLoading ? <Skeleton width={120} /> : payerValue?.paymentAddress}
106
+ </Typography>
107
+ }
108
+ arrow>
109
+ <InfoOutlined
110
+ sx={{
111
+ fontSize: '16px',
112
+ color: 'text.secondary',
113
+ cursor: 'pointer',
114
+ '&:hover': { color: 'primary.main' },
115
+ }}
116
+ />
117
+ </Tooltip>
118
+ </Stack>
119
+ }
120
+ value={renderBalanceValue()}
121
+ divider
122
+ />
123
+ )}
52
124
  {/* {scheduleToCancelTime > 0 && (
53
125
  <InfoMetric label={t('admin.subscription.cancel.schedule')} value={formatTime(scheduleToCancelTime)} divider />
54
126
  )} */}
@@ -62,3 +134,7 @@ export default function SubscriptionMetrics({ subscription }: Props) {
62
134
  </>
63
135
  );
64
136
  }
137
+
138
+ SubscriptionMetrics.defaultProps = {
139
+ showBalance: true,
140
+ };
@@ -14,7 +14,7 @@ import type { TSubscriptionExpanded } from '@blocklet/payment-types';
14
14
  import { Button, Link, Stack, Tooltip } from '@mui/material';
15
15
  import { useRequest, useSetState } from 'ahooks';
16
16
  import isEmpty from 'lodash/isEmpty';
17
- import { useState } from 'react';
17
+ import { useEffect, useState } from 'react';
18
18
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
19
19
  import { useNavigate } from 'react-router-dom';
20
20
  import { joinURL } from 'ufo';
@@ -23,6 +23,22 @@ import OverdraftProtectionDialog from '../../customer/overdraft-protection';
23
23
  import Actions from '../../actions';
24
24
  import { useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
25
25
 
26
+ interface ActionConfig {
27
+ key: string;
28
+ show: boolean;
29
+ label: string;
30
+ tooltip?: string;
31
+ onClick: (e?: React.MouseEvent) => void;
32
+ variant?: 'text' | 'outlined' | 'contained';
33
+ color?: 'inherit' | 'primary' | 'secondary' | 'error';
34
+ sx?: any;
35
+ primary?: boolean;
36
+ component?: any;
37
+ href?: string;
38
+ target?: string;
39
+ divider?: boolean;
40
+ }
41
+
26
42
  type ActionProps = {
27
43
  [key: string]: {
28
44
  color?: string;
@@ -33,6 +49,15 @@ type ActionProps = {
33
49
  text?: string;
34
50
  };
35
51
  };
52
+ export interface ProtectionInitValues {
53
+ enabled?: boolean;
54
+ return_stake?: boolean;
55
+ }
56
+ export interface ActionMethods {
57
+ openOverdraftProtection: (initValues?: ProtectionInitValues) => void;
58
+ }
59
+
60
+ export type ActionDisplayMode = 'all-buttons' | 'primary-buttons' | 'menu-only';
36
61
 
37
62
  type Props = {
38
63
  subscription: TSubscriptionExpanded;
@@ -45,9 +70,11 @@ type Props = {
45
70
  onChange: () => any | Promise<any>;
46
71
  };
47
72
  showDelegation?: boolean;
73
+ showUnsubscribe?: boolean;
48
74
  onChange?: (action?: string) => any | Promise<any>;
49
75
  actionProps?: ActionProps;
50
- mode?: 'menu' | 'btn';
76
+ mode?: ActionDisplayMode;
77
+ setUp?: (methods: ActionMethods) => void;
51
78
  };
52
79
 
53
80
  SubscriptionActions.defaultProps = {
@@ -57,7 +84,9 @@ SubscriptionActions.defaultProps = {
57
84
  showDelegation: false,
58
85
  onChange: null,
59
86
  actionProps: {},
60
- mode: 'btn',
87
+ mode: 'all-buttons',
88
+ setUp: null,
89
+ showUnsubscribe: true,
61
90
  };
62
91
  const fetchExtraActions = async ({
63
92
  id,
@@ -102,9 +131,11 @@ export function SubscriptionActionsInner({
102
131
  showRecharge,
103
132
  showOverdraftProtection,
104
133
  showDelegation,
134
+ showUnsubscribe,
105
135
  onChange,
106
136
  actionProps,
107
137
  mode,
138
+ setUp,
108
139
  }: Props) {
109
140
  const { t, locale } = useLocaleContext();
110
141
  const { reset, getValues } = useFormContext();
@@ -122,6 +153,7 @@ export function SubscriptionActionsInner({
122
153
  loading: false,
123
154
  openProtection: false,
124
155
  protectionLoading: false,
156
+ protectionInitValues: null,
125
157
  });
126
158
 
127
159
  const shouldFetchDelegation = showDelegation && ['active', 'trialing', 'past_due'].includes(subscription?.status);
@@ -146,6 +178,19 @@ export function SubscriptionActionsInner({
146
178
  refreshDeps: [subscription.id, shouldFetchOverdraftProtection],
147
179
  });
148
180
 
181
+ useEffect(() => {
182
+ if (setUp) {
183
+ setUp({
184
+ openOverdraftProtection: (initValues?: ProtectionInitValues) =>
185
+ setState({
186
+ openProtection: true,
187
+ // @ts-ignore
188
+ protectionInitValues: initValues,
189
+ }),
190
+ });
191
+ }
192
+ }, [setUp]);
193
+
149
194
  const handleCancel = async () => {
150
195
  try {
151
196
  const result = await checkUnpaidInvoices();
@@ -273,162 +318,148 @@ export function SubscriptionActionsInner({
273
318
  };
274
319
 
275
320
  const renderActions = () => {
276
- if (mode === 'menu') {
277
- const actions = [
278
- showDelegation &&
279
- noDelegation && {
280
- label: t('customer.delegation.btn'),
281
- handler: handleDelegate,
282
- color: 'primary',
283
- divider: true,
284
- },
285
- shouldFetchOverdraftProtection && {
286
- label: t('customer.overdraftProtection.title'),
287
- handler: () => setState({ openProtection: true }),
288
- color: 'primary',
321
+ const supportUnsubscribe = action?.action === 'cancel' && showUnsubscribe;
322
+ const supportAction = action && (action?.action !== 'cancel' || supportUnsubscribe);
323
+
324
+ const serviceActions = subscription.service_actions?.filter((x: any) => x?.type !== 'notification') || [];
325
+ const actionConfigs: ActionConfig[] = [
326
+ {
327
+ key: 'delegation',
328
+ show: showDelegation && noDelegation,
329
+ label: t('customer.delegation.btn'),
330
+ tooltip: t('customer.delegation.title'),
331
+ onClick: handleDelegate,
332
+ variant: 'outlined',
333
+ color: 'primary',
334
+ primary: true,
335
+ },
336
+ {
337
+ key: 'protection',
338
+ show: shouldFetchOverdraftProtection,
339
+ label: t('customer.overdraftProtection.setting'),
340
+ onClick: () => setState({ openProtection: true, protectionInitValues: null }),
341
+ variant: 'outlined',
342
+ color: 'primary',
343
+ },
344
+ {
345
+ key: 'recharge',
346
+ show: !!(showRecharge && supportRecharge(subscription)),
347
+ label: t('customer.recharge.title'),
348
+ onClick: (e) => {
349
+ e?.stopPropagation();
350
+ navigate(`/customer/subscription/${subscription.id}/recharge`);
289
351
  },
290
- showRecharge &&
291
- supportRecharge(subscription) && {
292
- label: t('customer.recharge.title'),
293
- handler: () => navigate(`/customer/subscription/${subscription.id}/recharge`),
294
- color: 'primary',
295
- divider: true,
296
- },
297
- !extraActions?.batchPay &&
298
- action && {
299
- label: action?.text || t(`payment.customer.${action.action}.button`),
300
- handler: () => {
301
- if (action.action === 'pastDue') {
302
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
303
- } else {
304
- setState({
305
- action: action.action,
306
- subscription: subscription.id,
307
- });
308
- }
309
- },
310
- color: action.color as any,
311
- },
312
- extraActions?.changePlan && {
313
- label: action?.text || t('payment.customer.changePlan.button'),
314
- handler: () => navigate(`/customer/subscription/${subscription.id}/change-plan`),
315
- color: 'primary',
352
+ variant: 'outlined',
353
+ color: 'primary',
354
+ primary: true,
355
+ },
356
+ {
357
+ key: 'changePlan',
358
+ show: !!extraActions?.changePlan,
359
+ label: action?.text || t('payment.customer.changePlan.button'),
360
+ onClick: (e) => {
361
+ e?.stopPropagation();
362
+ navigate(`/customer/subscription/${subscription.id}/change-plan`);
363
+ },
364
+ variant: 'contained',
365
+ color: 'primary',
366
+ sx: action?.sx,
367
+ },
368
+ {
369
+ key: 'batchPay',
370
+ show: !!extraActions?.batchPay,
371
+ label: action?.text || t('admin.subscription.batchPay.button'),
372
+ onClick: (e) => {
373
+ e?.stopPropagation();
374
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions?.batchPay}`);
316
375
  },
317
- !!extraActions?.batchPay && {
318
- label: action?.text || t('admin.subscription.batchPay.button'),
319
- handler: () =>
320
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions.batchPay}`),
321
- color: 'error',
376
+ variant: 'outlined',
377
+ color: 'error',
378
+ sx: action?.sx,
379
+ primary: true,
380
+ },
381
+ {
382
+ key: 'mainAction',
383
+ show: !!(!extraActions?.batchPay && supportAction),
384
+ label: action?.text || t(`payment.customer.${action?.action}.button`),
385
+ onClick: (e) => {
386
+ e?.stopPropagation();
387
+ if (action?.action === 'pastDue') {
388
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
389
+ } else {
390
+ // @ts-ignore
391
+ setState({ action: action?.action, subscription: subscription.id });
392
+ }
322
393
  },
323
- ...(subscription.service_actions
324
- ?.filter((x: any) => x?.type !== 'notification')
325
- .map((x) => ({
326
- label: x.text[locale] || x.text.en || x.name,
327
- handler: () => {
328
- window.open(x.link, '_blank');
329
- },
330
- color: x?.color || 'primary',
331
- })) || []),
332
- ].filter(Boolean);
394
+ // @ts-ignore
395
+ variant: action?.variant || 'outlined',
396
+ // @ts-ignore
397
+ color: action?.color || 'primary',
398
+ sx: action?.sx,
399
+ divider: serviceActions.length > 0,
400
+ },
401
+ // @ts-ignore
402
+ ...serviceActions.map((x) => ({
403
+ key: x.name,
404
+ show: true,
405
+ label: x.text[locale] || x.text.en || x.name,
406
+ onClick: () => window.open(x.link, '_blank'),
407
+ variant: x?.variant || 'contained',
408
+ color: x?.color || 'primary',
409
+ component: Link,
410
+ href: x.link,
411
+ target: '_blank',
412
+ sx: { textDecoration: 'none !important' },
413
+ })),
414
+ ];
333
415
 
334
- return (
335
- <Actions
336
- // @ts-ignore
337
- actions={actions}
338
- />
339
- );
340
- }
341
- return (
342
- <>
343
- {showDelegation && noDelegation && (
344
- <Tooltip title={t('customer.delegation.title')}>
345
- <Button variant="outlined" color="primary" onClick={handleDelegate}>
346
- {t('customer.delegation.btn')}
347
- </Button>
348
- </Tooltip>
349
- )}
416
+ // 过滤出要显示的操作
417
+ const visibleActions = actionConfigs.filter((a) => a.show);
350
418
 
351
- {shouldFetchOverdraftProtection && (
352
- <Button variant="outlined" color="primary" onClick={() => setState({ openProtection: true })}>
353
- {t('customer.overdraftProtection.title')}
354
- </Button>
355
- )}
419
+ // 转换为菜单项
420
+ const toMenuItem = (item: any) => ({
421
+ label: item.label,
422
+ handler: item.onClick,
423
+ color: item.color,
424
+ divider: item.divider,
425
+ });
356
426
 
357
- {showRecharge && supportRecharge(subscription) && (
358
- <Button
359
- variant="outlined"
360
- color="primary"
361
- onClick={(e) => {
362
- e.stopPropagation();
363
- navigate(`/customer/subscription/${subscription.id}/recharge`);
364
- }}>
365
- {t('customer.recharge.title')}
366
- </Button>
367
- )}
368
- {!extraActions?.batchPay && action && (
369
- <Button
370
- variant={action.variant as any}
371
- color={action.color as any}
372
- size="small"
373
- sx={action?.sx as any}
374
- onClick={(e) => {
375
- e.stopPropagation();
376
- if (action.action === 'pastDue') {
377
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
378
- } else {
379
- setState({
380
- action: action.action,
381
- subscription: subscription.id,
382
- });
383
- }
384
- }}>
385
- {action?.text || t(`payment.customer.${action.action}.button`)}
386
- </Button>
387
- )}
388
- {extraActions?.changePlan && (
389
- <Button
390
- variant="contained"
391
- color="primary"
392
- size="small"
393
- sx={action?.sx as any}
394
- onClick={(e) => {
395
- e.stopPropagation();
396
- navigate(`/customer/subscription/${subscription.id}/change-plan`);
397
- }}>
398
- {action?.text || t('payment.customer.changePlan.button')}
399
- </Button>
400
- )}
401
- {!!extraActions?.batchPay && (
402
- <Button
403
- variant="outlined"
404
- color="error"
405
- size="small"
406
- sx={action?.sx as any}
407
- onClick={(e) => {
408
- e.stopPropagation();
409
- navigate(`/customer/invoice/past-due?subscription=${subscription.id}&currency=${extraActions.batchPay}`);
410
- }}>
411
- {action?.text || t('admin.subscription.batchPay.button')}
412
- </Button>
427
+ const toButton = (item: ActionConfig) => (
428
+ <Button
429
+ key={item.key}
430
+ variant={item.variant}
431
+ color={item.color}
432
+ onClick={item.onClick}
433
+ component={item.component}
434
+ href={item.href}
435
+ target={item.target}
436
+ sx={item.sx}
437
+ size="small">
438
+ {item.tooltip ? (
439
+ <Tooltip title={item.tooltip}>
440
+ <span>{item.label}</span>
441
+ </Tooltip>
442
+ ) : (
443
+ item.label
413
444
  )}
414
- {subscription.service_actions
415
- ?.filter((x: any) => x?.type !== 'notification')
416
- .map((x) => (
417
- // @ts-ignore
418
- <Button
419
- component={Link}
420
- key={x.name}
421
- variant={x?.variant || 'contained'}
422
- color={x?.color || 'primary'}
423
- href={x.link}
424
- size="small"
425
- target="_blank"
426
- sx={{ textDecoration: 'none !important' }}>
427
- {x.text[locale] || x.text.en || x.name}
428
- </Button>
429
- ))}
430
- </>
445
+ </Button>
431
446
  );
447
+ if (mode === 'menu-only') {
448
+ return <Actions actions={visibleActions.map(toMenuItem)} />;
449
+ }
450
+
451
+ if (mode === 'primary-buttons') {
452
+ const primaryButtons = visibleActions.filter((a) => a.primary);
453
+ const menuItems = visibleActions.filter((a) => !a.primary);
454
+ return (
455
+ <>
456
+ {primaryButtons.map(toButton)}
457
+ {menuItems.length > 0 && <Actions actions={menuItems.map(toMenuItem)} />}
458
+ </>
459
+ );
460
+ }
461
+
462
+ return <>{visibleActions.map(toButton)}</>;
432
463
  };
433
464
 
434
465
  return (
@@ -466,11 +497,13 @@ export function SubscriptionActionsInner({
466
497
  value={overdraftProtection}
467
498
  onSave={handleOverdraftProtection}
468
499
  open={state.openProtection}
469
- paymentAddress={subscription.overdraft_protection?.payment_details?.arcblock?.payer}
500
+ payerAddress={subscription.overdraft_protection?.payment_details?.arcblock?.payer}
501
+ stakingAddress={subscription.overdraft_protection?.payment_details?.arcblock?.staking?.address}
470
502
  currency={subscription.paymentCurrency}
471
503
  subscription={subscription}
472
504
  loading={state.protectionLoading}
473
505
  onCancel={() => setState({ openProtection: false })}
506
+ initValues={state.protectionInitValues}
474
507
  />
475
508
  )}
476
509
  </Stack>
@@ -500,7 +533,9 @@ SubscriptionActionsInner.defaultProps = {
500
533
  showRecharge: false,
501
534
  showOverdraftProtection: false,
502
535
  showDelegation: false,
536
+ showUnsubscribe: true,
503
537
  onChange: null,
504
538
  actionProps: {},
505
- mode: 'btn',
539
+ mode: 'all-buttons',
540
+ setUp: null,
506
541
  };
@@ -192,6 +192,7 @@ export default function CurrentSubscriptions({
192
192
  onChange(v);
193
193
  }
194
194
  }}
195
+ showUnsubscribe={false}
195
196
  showRecharge
196
197
  actionProps={{
197
198
  cancel: {