payment-kit 1.19.0 → 1.19.1

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 (133) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/ws.ts +23 -1
  24. package/api/src/locales/en.ts +33 -0
  25. package/api/src/locales/zh.ts +31 -0
  26. package/api/src/queues/credit-consume.ts +715 -0
  27. package/api/src/queues/credit-grant.ts +572 -0
  28. package/api/src/queues/notification.ts +173 -128
  29. package/api/src/queues/payment.ts +210 -122
  30. package/api/src/queues/subscription.ts +179 -0
  31. package/api/src/routes/checkout-sessions.ts +157 -9
  32. package/api/src/routes/connect/shared.ts +3 -2
  33. package/api/src/routes/credit-grants.ts +241 -0
  34. package/api/src/routes/credit-transactions.ts +208 -0
  35. package/api/src/routes/index.ts +8 -0
  36. package/api/src/routes/meter-events.ts +347 -0
  37. package/api/src/routes/meters.ts +219 -0
  38. package/api/src/routes/payment-currencies.ts +14 -2
  39. package/api/src/routes/payment-links.ts +1 -1
  40. package/api/src/routes/payment-methods.ts +14 -2
  41. package/api/src/routes/prices.ts +43 -0
  42. package/api/src/routes/pricing-table.ts +13 -7
  43. package/api/src/routes/products.ts +63 -4
  44. package/api/src/routes/settings.ts +1 -1
  45. package/api/src/routes/subscriptions.ts +4 -0
  46. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  47. package/api/src/store/models/credit-grant.ts +486 -0
  48. package/api/src/store/models/credit-transaction.ts +268 -0
  49. package/api/src/store/models/customer.ts +8 -0
  50. package/api/src/store/models/index.ts +52 -1
  51. package/api/src/store/models/meter-event.ts +423 -0
  52. package/api/src/store/models/meter.ts +176 -0
  53. package/api/src/store/models/payment-currency.ts +66 -14
  54. package/api/src/store/models/price.ts +6 -0
  55. package/api/src/store/models/product.ts +2 -2
  56. package/api/src/store/models/subscription.ts +24 -0
  57. package/api/src/store/models/types.ts +28 -2
  58. package/api/tests/libs/subscription.spec.ts +53 -0
  59. package/blocklet.yml +9 -1
  60. package/package.json +4 -4
  61. package/scripts/sdk.js +233 -1
  62. package/src/app.tsx +10 -0
  63. package/src/components/collapse.tsx +11 -1
  64. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  65. package/src/components/customer/credit-overview.tsx +233 -0
  66. package/src/components/customer/form.tsx +5 -2
  67. package/src/components/invoice/list.tsx +19 -1
  68. package/src/components/metadata/form.tsx +286 -90
  69. package/src/components/meter/actions.tsx +101 -0
  70. package/src/components/meter/add-usage-dialog.tsx +239 -0
  71. package/src/components/meter/events-list.tsx +657 -0
  72. package/src/components/meter/form.tsx +245 -0
  73. package/src/components/meter/products.tsx +264 -0
  74. package/src/components/meter/usage-guide.tsx +174 -0
  75. package/src/components/payment-currency/form.tsx +2 -0
  76. package/src/components/payment-intent/list.tsx +19 -1
  77. package/src/components/payment-link/preview.tsx +1 -1
  78. package/src/components/payment-link/product-select.tsx +52 -12
  79. package/src/components/payment-method/arcblock.tsx +2 -0
  80. package/src/components/payment-method/base.tsx +2 -0
  81. package/src/components/payment-method/bitcoin.tsx +2 -0
  82. package/src/components/payment-method/ethereum.tsx +2 -0
  83. package/src/components/payment-method/stripe.tsx +2 -0
  84. package/src/components/payouts/list.tsx +19 -1
  85. package/src/components/price/currency-select.tsx +51 -31
  86. package/src/components/price/form.tsx +881 -407
  87. package/src/components/pricing-table/preview.tsx +1 -1
  88. package/src/components/product/add-price.tsx +9 -7
  89. package/src/components/product/create.tsx +7 -4
  90. package/src/components/product/edit-price.tsx +21 -12
  91. package/src/components/product/features.tsx +17 -7
  92. package/src/components/product/form.tsx +104 -89
  93. package/src/components/refund/list.tsx +19 -1
  94. package/src/components/section/header.tsx +5 -18
  95. package/src/components/subscription/items/index.tsx +1 -1
  96. package/src/components/subscription/metrics.tsx +37 -5
  97. package/src/components/subscription/portal/actions.tsx +2 -1
  98. package/src/contexts/products.tsx +26 -9
  99. package/src/hooks/subscription.ts +34 -0
  100. package/src/libs/meter-utils.ts +196 -0
  101. package/src/libs/util.ts +4 -0
  102. package/src/locales/en.tsx +385 -4
  103. package/src/locales/zh.tsx +364 -0
  104. package/src/pages/admin/billing/index.tsx +61 -33
  105. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  106. package/src/pages/admin/billing/meters/create.tsx +60 -0
  107. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  108. package/src/pages/admin/billing/meters/index.tsx +210 -0
  109. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  110. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  111. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  112. package/src/pages/admin/customers/customers/detail.tsx +22 -10
  113. package/src/pages/admin/customers/index.tsx +5 -0
  114. package/src/pages/admin/developers/events/detail.tsx +1 -1
  115. package/src/pages/admin/developers/index.tsx +1 -1
  116. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  117. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  118. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  119. package/src/pages/admin/products/index.tsx +3 -2
  120. package/src/pages/admin/products/links/detail.tsx +1 -1
  121. package/src/pages/admin/products/prices/actions.tsx +16 -4
  122. package/src/pages/admin/products/prices/detail.tsx +30 -3
  123. package/src/pages/admin/products/prices/list.tsx +8 -1
  124. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  125. package/src/pages/admin/products/products/create.tsx +233 -57
  126. package/src/pages/admin/products/products/detail.tsx +2 -1
  127. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  128. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  129. package/src/pages/customer/index.tsx +35 -2
  130. package/src/pages/customer/recharge/account.tsx +5 -5
  131. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  132. package/src/pages/customer/subscription/detail.tsx +48 -14
  133. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -10,7 +10,7 @@ import {
10
10
  useDefaultPageSize,
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
13
- import { CircularProgress, Typography } from '@mui/material';
13
+ import { Avatar, CircularProgress, Typography } from '@mui/material';
14
14
  import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
16
  import { Link } from 'react-router-dom';
@@ -136,6 +136,24 @@ export default function PaymentList({
136
136
  },
137
137
  },
138
138
  },
139
+ {
140
+ label: t('common.paymentMethod'),
141
+ name: 'paymentMethod',
142
+ width: 120,
143
+ options: {
144
+ customBodyRenderLite: (_: string, index: number) => {
145
+ const item = data.list[index] as TPaymentIntentExpanded;
146
+ return (
147
+ <Link to={`/admin/payments/${item.id}`}>
148
+ <Typography sx={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
149
+ <Avatar src={item.paymentMethod.logo} sx={{ width: 18, height: 18, mr: 1 }} />
150
+ {item.paymentMethod.name}
151
+ </Typography>
152
+ </Link>
153
+ );
154
+ },
155
+ },
156
+ },
139
157
  {
140
158
  label: t('common.status'),
141
159
  name: 'status',
@@ -9,7 +9,7 @@ export default function PaymentLinkPreview({
9
9
  id,
10
10
  version = 1,
11
11
  }: { id: string; version?: number } & {
12
- ref?: React.RefObject<unknown>;
12
+ ref?: React.RefObject<unknown | null>;
13
13
  }) {
14
14
  const theme = useTheme();
15
15
  const innerRef = useRef(null);
@@ -7,38 +7,68 @@ import cloneDeep from 'lodash/cloneDeep';
7
7
  import { useState } from 'react';
8
8
  import type { LiteralUnion } from 'type-fest';
9
9
 
10
+ import Empty from '@arcblock/ux/lib/Empty';
10
11
  import { useProductsContext } from '../../contexts/products';
11
12
 
12
13
  type Props = {
13
14
  mode: LiteralUnion<'waiting' | 'selecting' | 'inline', string>;
14
15
  hasSelected: (price: any) => boolean;
15
16
  onSelect: (priceId: string) => void;
17
+ addProduct?: boolean;
18
+ filterPrice?: (price: any) => boolean;
16
19
  };
17
20
 
18
- const filterPrices = (product: TProductExpanded, hasSelected: (price: any) => boolean) => {
19
- product.prices = product.prices.filter((x) => x.active && !hasSelected(x));
21
+ const filterPrices = (
22
+ product: TProductExpanded,
23
+ hasSelected: (price: any) => boolean,
24
+ filterPrice?: (price: any) => boolean
25
+ ) => {
26
+ product.prices = product.prices.filter((x) => {
27
+ const isActive = x.active;
28
+ const notSelected = !hasSelected(x);
29
+ const customFilter = filterPrice ? filterPrice(x) : true;
30
+ return isActive && notSelected && customFilter;
31
+ });
20
32
  return product;
21
33
  };
22
34
 
23
- const filterProducts = (products: TProductExpanded[], hasSelected: (price: any) => boolean) => {
24
- const filtered = cloneDeep(products).map((x) => filterPrices(x, hasSelected));
35
+ const filterProducts = (
36
+ products: TProductExpanded[],
37
+ hasSelected: (price: any) => boolean,
38
+ filterPrice?: (price: any) => boolean
39
+ ) => {
40
+ const filtered = cloneDeep(products).map((x) => filterPrices(x, hasSelected, filterPrice));
25
41
  return filtered.filter((x) => x.prices.length);
26
42
  };
27
43
 
28
- export default function ProductSelect({ mode: initialMode, hasSelected, onSelect }: Props) {
44
+ export default function ProductSelect({
45
+ mode: initialMode,
46
+ hasSelected,
47
+ onSelect,
48
+ addProduct = true,
49
+ filterPrice = () => true,
50
+ }: Props) {
29
51
  const { t } = useLocaleContext();
30
52
  const [mode, setMode] = useState(initialMode);
31
53
  const { products } = useProductsContext();
32
54
  const { settings } = usePaymentContext();
55
+ const [value, setValue] = useState('');
33
56
  const size = { width: 16, height: 16 };
34
57
 
35
58
  const handleSelect = (e: any) => {
59
+ setValue(e.target.value);
36
60
  setMode('waiting');
37
61
  onSelect(e.target.value);
38
62
  };
39
63
 
40
- const items = (callback?: any) =>
41
- filterProducts(products, hasSelected).map((product) => [
64
+ const items = (callback?: any) => {
65
+ const filteredProducts = filterProducts(products, hasSelected, filterPrice);
66
+
67
+ if (filteredProducts.length === 0) {
68
+ return <Empty>{t('admin.product.empty')}</Empty>;
69
+ }
70
+
71
+ return filteredProducts.map((product) => [
42
72
  <ListSubheader key={product.id} sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
43
73
  <Stack
44
74
  direction="row"
@@ -89,6 +119,7 @@ export default function ProductSelect({ mode: initialMode, hasSelected, onSelect
89
119
  );
90
120
  }),
91
121
  ]);
122
+ };
92
123
 
93
124
  if (mode === 'inline') {
94
125
  return <>{items(onSelect)}</>;
@@ -96,11 +127,20 @@ export default function ProductSelect({ mode: initialMode, hasSelected, onSelect
96
127
 
97
128
  if (mode === 'selecting') {
98
129
  return (
99
- <Select value="" fullWidth size="small" onChange={handleSelect} MenuProps={{ style: { maxHeight: 480 } }}>
100
- <MenuItem value="add">
101
- <AddOutlined />
102
- {t('admin.product.add')}
103
- </MenuItem>
130
+ <Select
131
+ fullWidth
132
+ size="small"
133
+ onChange={handleSelect}
134
+ value={value}
135
+ MenuProps={{
136
+ style: { maxHeight: 480 },
137
+ }}>
138
+ {addProduct && (
139
+ <MenuItem value="add">
140
+ <AddOutlined />
141
+ {t('admin.product.add')}
142
+ </MenuItem>
143
+ )}
104
144
  {items()}
105
145
  </Select>
106
146
  );
@@ -30,6 +30,7 @@ export default function ArcBlockMethodForm({ checkDisabled }: { checkDisabled: (
30
30
  label={t('admin.paymentMethod.name.label')}
31
31
  placeholder={t('admin.paymentMethod.name.tip')}
32
32
  disabled={checkDisabled('name')}
33
+ inputProps={{ maxLength: 32 }}
33
34
  />
34
35
  <FormInput
35
36
  key="description"
@@ -39,6 +40,7 @@ export default function ArcBlockMethodForm({ checkDisabled }: { checkDisabled: (
39
40
  label={t('admin.paymentMethod.description.label')}
40
41
  placeholder={t('admin.paymentMethod.description.tip')}
41
42
  disabled={checkDisabled('description')}
43
+ inputProps={{ maxLength: 255 }}
42
44
  />
43
45
  <FormInput
44
46
  key="secret_key"
@@ -31,6 +31,7 @@ export default function BaseMethodForm({ checkDisabled }: { checkDisabled: (key:
31
31
  label={t('admin.paymentMethod.name.label')}
32
32
  placeholder={t('admin.paymentMethod.name.tip')}
33
33
  disabled={checkDisabled('name')}
34
+ inputProps={{ maxLength: 32 }}
34
35
  />
35
36
  <FormInput
36
37
  key="description"
@@ -40,6 +41,7 @@ export default function BaseMethodForm({ checkDisabled }: { checkDisabled: (key:
40
41
  label={t('admin.paymentMethod.description.label')}
41
42
  placeholder={t('admin.paymentMethod.description.tip')}
42
43
  disabled={checkDisabled('description')}
44
+ inputProps={{ maxLength: 255 }}
43
45
  />
44
46
  <EvmRpcInput
45
47
  name="settings.base.api_host"
@@ -30,6 +30,7 @@ export default function BitcoinMethodForm({ checkDisabled }: { checkDisabled: (k
30
30
  label={t('admin.paymentMethod.name.label')}
31
31
  placeholder={t('admin.paymentMethod.name.tip')}
32
32
  disabled={checkDisabled('name')}
33
+ inputProps={{ maxLength: 32 }}
33
34
  />
34
35
  <FormInput
35
36
  key="description"
@@ -39,6 +40,7 @@ export default function BitcoinMethodForm({ checkDisabled }: { checkDisabled: (k
39
40
  label={t('admin.paymentMethod.description.label')}
40
41
  placeholder={t('admin.paymentMethod.description.tip')}
41
42
  disabled={checkDisabled('description')}
43
+ inputProps={{ maxLength: 255 }}
42
44
  />
43
45
  <FormInput
44
46
  key="chain_id"
@@ -31,6 +31,7 @@ export default function EthereumMethodForm({ checkDisabled }: { checkDisabled: (
31
31
  label={t('admin.paymentMethod.name.label')}
32
32
  placeholder={t('admin.paymentMethod.name.tip')}
33
33
  disabled={checkDisabled('name')}
34
+ inputProps={{ maxLength: 32 }}
34
35
  />
35
36
  <FormInput
36
37
  key="description"
@@ -40,6 +41,7 @@ export default function EthereumMethodForm({ checkDisabled }: { checkDisabled: (
40
41
  label={t('admin.paymentMethod.description.label')}
41
42
  placeholder={t('admin.paymentMethod.description.tip')}
42
43
  disabled={checkDisabled('description')}
44
+ inputProps={{ maxLength: 255 }}
43
45
  />
44
46
  <EvmRpcInput
45
47
  name="settings.ethereum.api_host"
@@ -14,6 +14,7 @@ export default function StripeMethodForm({ checkDisabled }: { checkDisabled: (ke
14
14
  label={t('admin.paymentMethod.name.label')}
15
15
  placeholder={t('admin.paymentMethod.name.tip')}
16
16
  disabled={checkDisabled('name')}
17
+ inputProps={{ maxLength: 32 }}
17
18
  />
18
19
  <FormInput
19
20
  name="description"
@@ -21,6 +22,7 @@ export default function StripeMethodForm({ checkDisabled }: { checkDisabled: (ke
21
22
  rules={{ required: true }}
22
23
  label={t('admin.paymentMethod.description.label')}
23
24
  placeholder={t('admin.paymentMethod.description.tip')}
25
+ inputProps={{ maxLength: 255 }}
24
26
  />
25
27
  <FormInput
26
28
  name="settings.stripe.dashboard"
@@ -10,7 +10,7 @@ import {
10
10
  useDefaultPageSize,
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPayoutExpanded } from '@blocklet/payment-types';
13
- import { CircularProgress, Typography } from '@mui/material';
13
+ import { Avatar, CircularProgress, Typography } from '@mui/material';
14
14
  import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
16
  import { Link } from 'react-router-dom';
@@ -132,6 +132,24 @@ export default function PayoutList({
132
132
  },
133
133
  },
134
134
  },
135
+ {
136
+ label: t('common.paymentMethod'),
137
+ name: 'paymentMethod',
138
+ width: 120,
139
+ options: {
140
+ customBodyRenderLite: (_: string, index: number) => {
141
+ const item = data.list[index] as TPayoutExpanded;
142
+ return (
143
+ <Link to={`/admin/payments/${item.id}`}>
144
+ <Typography sx={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap' }}>
145
+ <Avatar src={item.paymentMethod.logo} sx={{ width: 18, height: 18, mr: 1 }} />
146
+ {item.paymentMethod.name}
147
+ </Typography>
148
+ </Link>
149
+ );
150
+ },
151
+ },
152
+ },
135
153
  {
136
154
  label: t('common.status'),
137
155
  name: 'status',
@@ -2,7 +2,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { usePaymentContext } from '@blocklet/payment-react';
3
3
  import { AddOutlined, ArrowDropDown } from '@mui/icons-material';
4
4
  import { ListSubheader, MenuItem, Select, Stack, SxProps, Typography } from '@mui/material';
5
- import { useState } from 'react';
5
+ import { useState, useEffect } from 'react';
6
6
  import type { LiteralUnion } from 'type-fest';
7
7
 
8
8
  import { flatten } from 'lodash';
@@ -17,6 +17,7 @@ type Props = {
17
17
  width?: string;
18
18
  disabled?: boolean;
19
19
  selectSX?: SxProps;
20
+ currencyFilter?: (currency: any) => boolean;
20
21
  };
21
22
 
22
23
  export default function CurrencySelect({
@@ -27,11 +28,27 @@ export default function CurrencySelect({
27
28
  width = '100%',
28
29
  disabled = false,
29
30
  selectSX = {},
31
+ currencyFilter = () => true,
30
32
  }: Props) {
31
33
  const { t } = useLocaleContext();
32
34
  const { settings } = usePaymentContext();
33
35
  const [mode, setMode] = useState(initialMode);
34
36
 
37
+ const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
38
+
39
+ useEffect(() => {
40
+ if (value && initialMode === 'selected' && currencies.length > 0) {
41
+ const currency = currencies.find((c) => c.id === value);
42
+ if (currency && !hasSelected(currency)) {
43
+ const timer = setTimeout(() => {
44
+ onSelect(value);
45
+ }, 0);
46
+ return () => clearTimeout(timer);
47
+ }
48
+ }
49
+ return undefined;
50
+ }, [value, initialMode]);
51
+
35
52
  const handleSelect = (e: any) => {
36
53
  if (disabled) {
37
54
  return;
@@ -40,8 +57,6 @@ export default function CurrencySelect({
40
57
  onSelect(e.target.value);
41
58
  };
42
59
 
43
- const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
44
-
45
60
  const selectedCurrency = currencies.find((x) => x.id === value);
46
61
 
47
62
  const selectedPaymentMethod = settings.paymentMethods.find((x) => x.payment_currencies.some((c) => c.id === value));
@@ -59,11 +74,13 @@ export default function CurrencySelect({
59
74
  }
60
75
  }}
61
76
  sx={{
62
- fontSize: '12px',
63
77
  cursor: canSelect ? 'pointer' : 'default',
64
78
  display: 'inline-flex',
79
+ minWidth: '120px',
80
+ justifyContent: 'flex-end',
81
+ textAlign: 'right',
65
82
  }}>
66
- {selectedCurrency?.symbol}({selectedPaymentMethod?.name})
83
+ {selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
67
84
  {canSelect && <ArrowDropDown sx={{ color: 'text.secondary', fontSize: 21 }} />}
68
85
  </Typography>
69
86
  );
@@ -88,32 +105,35 @@ export default function CurrencySelect({
88
105
  sx={{ width, ...selectSX }}
89
106
  disabled={disabled}
90
107
  onClose={() => setMode(initialMode)}>
91
- {extraCurrencies.map((method) => [
92
- <ListSubheader
93
- key={method.id}
94
- sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
95
- {method.name}
96
- </ListSubheader>,
97
- ...method.payment_currencies.map((currency) => (
98
- <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
99
- <Stack
100
- direction="row"
101
- sx={{
102
- justifyContent: 'space-between',
103
- gap: 2,
104
- width: '100%',
105
- }}>
106
- <Currency logo={currency.logo} name={currency.name} />
107
- <Typography
108
- sx={{
109
- fontWeight: 'bold',
110
- }}>
111
- {currency.symbol}
112
- </Typography>
113
- </Stack>
114
- </MenuItem>
115
- )),
116
- ])}
108
+ {extraCurrencies
109
+ .map((method) => {
110
+ const filteredCurrencies = method.payment_currencies.filter(currencyFilter);
111
+ if (filteredCurrencies.length === 0) {
112
+ return null;
113
+ }
114
+
115
+ return [
116
+ <ListSubheader
117
+ key={method.id}
118
+ sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
119
+ {method.name}
120
+ </ListSubheader>,
121
+ ...filteredCurrencies.map((currency) => (
122
+ <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
123
+ <Stack direction="row" sx={{ width: '100%', justifyContent: 'space-between', gap: 2 }}>
124
+ <Currency logo={currency.logo} name={currency.name} />
125
+ <Typography
126
+ sx={{
127
+ fontWeight: 'bold',
128
+ }}>
129
+ {currency.symbol}
130
+ </Typography>
131
+ </Stack>
132
+ </MenuItem>
133
+ )),
134
+ ];
135
+ })
136
+ .filter(Boolean)}
117
137
  </Select>
118
138
  );
119
139
  }