payment-kit 1.18.12 → 1.18.14

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 (64) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/integrations/stripe/resource.ts +53 -11
  3. package/api/src/libs/auth.ts +14 -0
  4. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +5 -3
  5. package/api/src/libs/notification/template/subscription-canceled.ts +3 -3
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +4 -3
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +5 -4
  8. package/api/src/libs/notification/template/subscription-renewed.ts +2 -1
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +3 -4
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -1
  11. package/api/src/libs/notification/template/subscription-upgraded.ts +6 -4
  12. package/api/src/libs/notification/template/subscription-will-canceled.ts +6 -3
  13. package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
  14. package/api/src/libs/payment.ts +77 -2
  15. package/api/src/libs/util.ts +8 -0
  16. package/api/src/queues/payment.ts +50 -1
  17. package/api/src/queues/payout.ts +297 -0
  18. package/api/src/routes/checkout-sessions.ts +2 -7
  19. package/api/src/routes/customers.ts +79 -5
  20. package/api/src/routes/payment-currencies.ts +117 -1
  21. package/api/src/routes/payment-methods.ts +19 -9
  22. package/api/src/routes/subscriptions.ts +15 -9
  23. package/api/src/store/migrations/20250305-vault-config.ts +21 -0
  24. package/api/src/store/models/invoice.ts +4 -2
  25. package/api/src/store/models/payment-currency.ts +14 -0
  26. package/api/src/store/models/payout.ts +21 -0
  27. package/api/src/store/models/types.ts +6 -0
  28. package/blocklet.yml +2 -2
  29. package/package.json +18 -18
  30. package/src/app.tsx +117 -121
  31. package/src/components/actions.tsx +32 -9
  32. package/src/components/copyable.tsx +2 -2
  33. package/src/components/customer/overdraft-protection.tsx +1 -0
  34. package/src/components/layout/admin.tsx +6 -0
  35. package/src/components/layout/user.tsx +38 -0
  36. package/src/components/metadata/editor.tsx +7 -1
  37. package/src/components/metadata/list.tsx +3 -0
  38. package/src/components/passport/assign.tsx +3 -0
  39. package/src/components/payment-link/rename.tsx +1 -0
  40. package/src/components/pricing-table/rename.tsx +1 -0
  41. package/src/components/product/add-price.tsx +1 -0
  42. package/src/components/product/edit-price.tsx +1 -0
  43. package/src/components/product/edit.tsx +1 -0
  44. package/src/components/subscription/actions/index.tsx +1 -0
  45. package/src/components/subscription/portal/actions.tsx +27 -5
  46. package/src/components/subscription/portal/list.tsx +24 -6
  47. package/src/components/subscription/status.tsx +2 -2
  48. package/src/libs/util.ts +15 -0
  49. package/src/locales/en.tsx +42 -0
  50. package/src/locales/zh.tsx +37 -0
  51. package/src/pages/admin/payments/payouts/detail.tsx +47 -38
  52. package/src/pages/admin/settings/index.tsx +3 -3
  53. package/src/pages/admin/settings/payment-methods/index.tsx +33 -1
  54. package/src/pages/admin/settings/vault-config/edit-form.tsx +253 -0
  55. package/src/pages/admin/settings/vault-config/index.tsx +352 -0
  56. package/src/pages/customer/index.tsx +247 -154
  57. package/src/pages/customer/invoice/detail.tsx +1 -1
  58. package/src/pages/customer/payout/detail.tsx +9 -2
  59. package/src/pages/customer/recharge.tsx +6 -2
  60. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  61. package/src/pages/customer/subscription/change-plan.tsx +1 -1
  62. package/src/pages/customer/subscription/detail.tsx +8 -3
  63. package/src/pages/customer/subscription/embed.tsx +142 -84
  64. package/src/pages/integrations/donations/edit-form.tsx +0 -1
@@ -39,6 +39,7 @@ export default function AssignPassportDialog(props: { id: string; onCancel: any
39
39
  message={<Alert severity="error">{error.message}</Alert>}
40
40
  onConfirm={onConfirm}
41
41
  onCancel={props.onCancel}
42
+ color="primary"
42
43
  />
43
44
  );
44
45
  }
@@ -50,6 +51,7 @@ export default function AssignPassportDialog(props: { id: string; onCancel: any
50
51
  message={<CircularProgress />}
51
52
  onConfirm={onConfirm}
52
53
  onCancel={props.onCancel}
54
+ color="primary"
53
55
  />
54
56
  );
55
57
  }
@@ -75,6 +77,7 @@ export default function AssignPassportDialog(props: { id: string; onCancel: any
75
77
  }
76
78
  onConfirm={onConfirm}
77
79
  onCancel={props.onCancel}
80
+ color="primary"
78
81
  />
79
82
  );
80
83
  }
@@ -39,6 +39,7 @@ export default function RenamePaymentLink({
39
39
  disableEscapeKeyDown
40
40
  fullWidth
41
41
  maxWidth="sm"
42
+ className="base-dialog"
42
43
  onClose={() => onCancel(null)}
43
44
  showCloseButton={false}
44
45
  title={t('admin.paymentLink.rename')}
@@ -39,6 +39,7 @@ export default function RenamePricingTable({
39
39
  disableEscapeKeyDown
40
40
  fullWidth
41
41
  maxWidth="sm"
42
+ className="base-dialog"
42
43
  onClose={() => onCancel(null)}
43
44
  showCloseButton={false}
44
45
  title={t('admin.pricingTable.rename')}
@@ -48,6 +48,7 @@ export default function AddPrice({
48
48
  disableEscapeKeyDown
49
49
  fullWidth
50
50
  maxWidth="sm"
51
+ className="base-dialog"
51
52
  onClose={() => onCancel(null)}
52
53
  showCloseButton={false}
53
54
  title={t('admin.price.add')}
@@ -75,6 +75,7 @@ export default function EditPrice({
75
75
  disableEscapeKeyDown
76
76
  fullWidth
77
77
  maxWidth="sm"
78
+ className="base-dialog"
78
79
  onClose={() => onCancel(null)}
79
80
  showCloseButton={false}
80
81
  title={t('admin.price.edit')}
@@ -50,6 +50,7 @@ export default function EditProduct({
50
50
  maxWidth="sm"
51
51
  onClose={() => onCancel(null)}
52
52
  showCloseButton={false}
53
+ className="base-dialog"
53
54
  title={t('admin.product.edit')}
54
55
  actions={
55
56
  <Stack direction="row">
@@ -181,6 +181,7 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
181
181
  title={t('admin.subscription.resume')}
182
182
  message={t('admin.subscription.resumeTip')}
183
183
  loading={state.loading}
184
+ color="primary"
184
185
  />
185
186
  )}
186
187
  {state.action === 'slashStake' && (
@@ -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
  }
@@ -489,6 +495,7 @@ export function SubscriptionActionsInner({
489
495
  date: formatToDate(subscription.current_period_end * 1000),
490
496
  })}
491
497
  loading={state.loading}
498
+ color="primary"
492
499
  />
493
500
  )}
494
501
 
@@ -506,6 +513,21 @@ export function SubscriptionActionsInner({
506
513
  initValues={state.protectionInitValues}
507
514
  />
508
515
  )}
516
+
517
+ {state.batchPay && (
518
+ <OverdueInvoicePayment
519
+ subscriptionId={subscription.id}
520
+ onPaid={() => {
521
+ setState({ batchPay: false });
522
+ onChange?.('batch-pay');
523
+ }}
524
+ inSubscriptionDetail
525
+ dialogProps={{
526
+ open: state.batchPay,
527
+ onClose: () => setState({ batchPay: false }),
528
+ }}
529
+ />
530
+ )}
509
531
  </Stack>
510
532
  );
511
533
  }
@@ -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
+ }
@@ -713,6 +713,48 @@ export default flat({
713
713
  note: 'Note: mountLocation must be unique, used to identify the donation instance. After configuration, the instance will appear in the list, and you can make further settings.',
714
714
  },
715
715
  },
716
+ vaultConfig: {
717
+ title: 'Vault',
718
+ learnMore: 'Learn more about vault configuration',
719
+ goToConfig: 'Go to system configuration',
720
+ description:
721
+ 'By enabling the vault wallet, you create a secure offline storage solution that automatically transfers excess funds when your hot wallet balance exceeds the threshold. This separation significantly enhances security by keeping the majority of your assets safely offline, protected from potential online threats.',
722
+ notConfigured: 'Vault wallet not configured',
723
+ configureFirst:
724
+ 'Please #go to the dashboard# to configure the vault wallet address before setting up individual currencies.',
725
+ ownerOnly: 'Only administrators with owner permissions can modify vault wallet settings.',
726
+ permissionRequired: 'Owner permissions required',
727
+ enabled: 'Status',
728
+ enabledYes: 'Enabled',
729
+ enabledNo: 'Disabled',
730
+ depositThreshold: 'Deposit Threshold',
731
+ withdrawThreshold: 'Withdrawal Threshold',
732
+ edit: 'Configure',
733
+ enable: 'Enable',
734
+ editTitle: 'Configure {currency} Vault Settings',
735
+ enableTitle: 'Enable Vault Wallet for {currency}',
736
+ enableVault: 'Enable Vault Wallet',
737
+ enableVaultHelp:
738
+ 'When enabled, excess funds automatically transfer to the vault wallet, and withdrawals exceeding the threshold require admin approval for security and risk control.',
739
+ depositThresholdHelp:
740
+ 'When the hot wallet balance exceeds this amount, the excess funds will be automatically transferred to the vault wallet.',
741
+ withdrawThresholdHelp:
742
+ 'For withdrawals exceeding this amount, approval from the vault wallet administrator is required.',
743
+ notConfig: 'Not configured',
744
+ noLimit: 'No limit',
745
+ withdrawThresholdNoLimit: '0 means no withdrawal limit',
746
+ depositThresholdRequired: 'Deposit threshold must be greater than 0',
747
+ withdrawThresholdInvalid: 'Withdrawal threshold must be greater or equal to 0',
748
+ enableSuccess: 'Successfully enabled vault wallet for {currency}',
749
+ disableSuccess: 'Successfully disabled vault wallet for {currency}',
750
+ updateSuccess: 'Successfully updated vault wallet settings for {currency}',
751
+ depositConfirmTitle: 'Deposit to Vault',
752
+ depositConfirmMessage:
753
+ '{currency} balance has exceeded the threshold, do you want to deposit to vault immediately?',
754
+ depositQueued: 'Deposit to vault request queued, please check the result later',
755
+ depositFailed: 'Deposit to vault request failed',
756
+ appBalance: 'App Balance',
757
+ },
716
758
  },
717
759
  empty: {
718
760
  image: 'No Image',
@@ -696,6 +696,43 @@ export default flat({
696
696
  note: '注意:mountLocation 必须是唯一的,用于标识打赏实例。配置完成后,该实例将出现在列表中,您可以进行进一步的设置。',
697
697
  },
698
698
  },
699
+ vaultConfig: {
700
+ title: '冷钱包配置',
701
+ description:
702
+ '启用冷钱包后,系统会在热钱包余额超过阈值时,自动将多余资金转移至安全的离线存储。这种隔离机制将大部分资产与在线环境分离,有效抵御网络攻击,显著提升资金安全性。',
703
+ learnMore: '了解更多',
704
+ goToConfig: '前往系统配置',
705
+ notConfigured: '冷钱包尚未配置',
706
+ configureFirst: '请先#前往仪表盘#配置冷钱包地址,然后再设置各币种参数。',
707
+ ownerOnly: '仅拥有所有者权限的管理员可修改冷钱包设置。',
708
+ permissionRequired: '需要所有者权限',
709
+ enabled: '状态',
710
+ enabledYes: '已启用',
711
+ enabledNo: '未启用',
712
+ depositThreshold: '存入阈值',
713
+ withdrawThreshold: '提取阈值',
714
+ edit: '配置',
715
+ enable: '启用',
716
+ editTitle: '配置 {currency} 冷钱包设置',
717
+ enableTitle: '为 {currency} 启用冷钱包',
718
+ enableVault: '启用冷钱包',
719
+ enableVaultHelp: '启用冷钱包后,超额资金将自动转入冷钱包,提款超出阈值需管理员审核,确保资产安全与风控。',
720
+ depositThresholdHelp: '当热钱包余额超过此金额时,多余资金将自动转入冷钱包。',
721
+ withdrawThresholdHelp: '当单笔提款超过此金额时,需由冷钱包管理员审核并批准。',
722
+ notConfig: '未配置',
723
+ noLimit: '无限制',
724
+ withdrawThresholdNoLimit: '0 表示无提款限制',
725
+ depositThresholdRequired: '存入阈值必须大于0',
726
+ withdrawThresholdInvalid: '提款阈值不能小于0',
727
+ enableSuccess: '{currency} 冷钱包已启用',
728
+ disableSuccess: '{currency} 冷钱包已关闭',
729
+ updateSuccess: '{currency} 冷钱包设置已更新',
730
+ depositConfirmTitle: '转入冷钱包',
731
+ depositConfirmMessage: '{currency}余额已超过阈值,是否立即转入冷钱包?',
732
+ depositQueued: '申请转入冷钱包成功,请稍后查看结果',
733
+ depositFailed: '申请转入冷钱包失败',
734
+ appBalance: '热钱包余额',
735
+ },
699
736
  },
700
737
  empty: {
701
738
  image: '无图片',
@@ -194,43 +194,50 @@ export default function PayoutDetail(props: { id: string }) {
194
194
  value={<Status label={data.status} color={getPayoutStatusColor(data.status)} />}
195
195
  divider
196
196
  />
197
- <InfoMetric
198
- label={t('customer.payout.payer')}
199
- value={
200
- <InfoCard
201
- logo={getCustomerAvatar(
202
- paymentIntent?.customer?.did,
203
- paymentIntent?.customer?.updated_at
204
- ? new Date(paymentIntent?.customer?.updated_at).toISOString()
205
- : '',
206
- 48
207
- )}
208
- name={
209
- <Typography
210
- variant="subtitle2"
211
- sx={{
212
- cursor: 'pointer',
213
- '&:hover': {
214
- color: 'text.link',
215
- },
216
- }}
217
- onClick={() => {
218
- const url = getCustomerProfileUrl({
219
- userDid: paymentIntent?.customer?.did,
220
- locale: 'zh',
221
- });
222
- window.open(url, '_blank');
223
- }}>
224
- {paymentIntent?.customer?.name} ({paymentIntent?.customer?.email})
225
- </Typography>
226
- }
227
- description={<DID did={paymentIntent?.customer?.did} />}
228
- size={40}
229
- variant="rounded"
230
- />
231
- }
232
- divider
233
- />
197
+ {paymentIntent?.id && (
198
+ <InfoMetric
199
+ label={t('customer.payout.payer')}
200
+ value={
201
+ <InfoCard
202
+ logo={getCustomerAvatar(
203
+ paymentIntent?.customer?.did,
204
+ paymentIntent?.customer?.updated_at
205
+ ? new Date(paymentIntent?.customer?.updated_at).toISOString()
206
+ : '',
207
+ 48
208
+ )}
209
+ name={
210
+ <Typography
211
+ variant="subtitle2"
212
+ sx={{
213
+ cursor: 'pointer',
214
+ '&:hover': {
215
+ color: 'text.link',
216
+ },
217
+ }}
218
+ onClick={() => {
219
+ const url = getCustomerProfileUrl({
220
+ userDid: paymentIntent?.customer?.did,
221
+ locale: 'zh',
222
+ });
223
+ window.open(url, '_blank');
224
+ }}>
225
+ {paymentIntent?.customer?.name} ({paymentIntent?.customer?.email})
226
+ </Typography>
227
+ }
228
+ description={
229
+ <DID
230
+ did={paymentIntent?.customer?.did}
231
+ {...(isMobile ? { responsive: false, compact: true } : {})}
232
+ />
233
+ }
234
+ size={40}
235
+ variant="rounded"
236
+ />
237
+ }
238
+ divider
239
+ />
240
+ )}
234
241
  {/* <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider /> */}
235
242
  {/* <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider /> */}
236
243
  </Stack>
@@ -380,13 +387,15 @@ export default function PayoutDetail(props: { id: string }) {
380
387
  <Box className="section">
381
388
  <SectionHeader title={t('admin.connections')} />
382
389
  <Stack>
383
- {data.payment_intent_id && (
390
+ {data.payment_intent_id ? (
384
391
  <InfoRow
385
392
  label={t('admin.paymentIntent.name')}
386
393
  value={<Link to={`/admin/payments/${data.paymentIntent.id}`}>{data.paymentIntent.id}</Link>}
387
394
  direction={InfoDirection}
388
395
  alignItems={InfoAlignItems}
389
396
  />
397
+ ) : (
398
+ t('common.none')
390
399
  )}
391
400
  </Stack>
392
401
  </Box>
@@ -9,8 +9,8 @@ import { useTransitionContext } from '../../../components/progress-bar';
9
9
  const PaymentMethodCreate = React.lazy(() => import('./payment-methods/create'));
10
10
 
11
11
  const pages = {
12
- paymentMethods: React.lazy(() => import('./payment-methods')),
13
- // branding: React.lazy(() => import('./branding')),
12
+ 'payment-methods': React.lazy(() => import('./payment-methods')),
13
+ 'vault-config': React.lazy(() => import('./vault-config')),
14
14
  // business: React.lazy(() => import('./business')),
15
15
  };
16
16
 
@@ -30,7 +30,7 @@ export default function SettingsIndex() {
30
30
  const TabComponent = pages[page] || pages.paymentMethods;
31
31
  const tabs = [
32
32
  { label: t('admin.paymentMethods'), value: 'payment-methods' },
33
- // { label: t('admin.branding'), value: 'branding' },
33
+ { label: t('admin.vaultConfig.title'), value: 'vault-config' },
34
34
  // { label: t('admin.business'), value: 'business' },
35
35
  ];
36
36
 
@@ -25,6 +25,7 @@ import {
25
25
  ListItem,
26
26
  ListItemAvatar,
27
27
  ListItemText,
28
+ Skeleton,
28
29
  Stack,
29
30
  TextField,
30
31
  Tooltip,
@@ -296,6 +297,37 @@ function Balance({
296
297
  );
297
298
  }
298
299
 
300
+ function PaymentMethodSkeleton() {
301
+ return (
302
+ <>
303
+ {[1].map((group) => (
304
+ <Box key={group} mt={3}>
305
+ <Stack direction="row" alignItems="center" mb={1} flexWrap="wrap" gap={1}>
306
+ <Skeleton variant="text" width={120} height={32} />
307
+ </Stack>
308
+ <Box
309
+ sx={{
310
+ py: 1,
311
+ borderTop: '1px solid #eee',
312
+ borderBottom: '1px solid #eee',
313
+ mb: 1,
314
+ }}>
315
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
316
+ <Stack direction="row" spacing={2} alignItems="center" sx={{ flex: 1 }}>
317
+ <Skeleton variant="rectangular" width={40} height={40} />
318
+ <Box sx={{ flex: 1 }}>
319
+ <Skeleton variant="text" width="20%" height={24} />
320
+ <Skeleton variant="text" width="40%" height={20} />
321
+ </Box>
322
+ </Stack>
323
+ </Stack>
324
+ </Box>
325
+ </Box>
326
+ ))}
327
+ </>
328
+ );
329
+ }
330
+
299
331
  export default function PaymentMethods() {
300
332
  const { t } = useLocaleContext();
301
333
  const [expandedId, setExpandedId] = useSessionStorageState('payment-method-expanded-id', {
@@ -362,7 +394,7 @@ export default function PaymentMethods() {
362
394
  }
363
395
 
364
396
  if (loading || !data || methods?.length === 0) {
365
- return <CircularProgress />;
397
+ return <PaymentMethodSkeleton />;
366
398
  }
367
399
 
368
400
  const groups = groupByType(methods);