payment-kit 1.21.13 → 1.21.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 (55) hide show
  1. package/api/src/crons/payment-stat.ts +31 -23
  2. package/api/src/libs/invoice.ts +29 -4
  3. package/api/src/libs/product.ts +28 -4
  4. package/api/src/routes/checkout-sessions.ts +46 -1
  5. package/api/src/routes/index.ts +2 -0
  6. package/api/src/routes/invoices.ts +63 -2
  7. package/api/src/routes/payment-stats.ts +244 -22
  8. package/api/src/routes/products.ts +3 -0
  9. package/api/src/routes/tax-rates.ts +220 -0
  10. package/api/src/store/migrations/20251001-add-tax-code-to-products.ts +20 -0
  11. package/api/src/store/migrations/20251001-create-tax-rates.ts +17 -0
  12. package/api/src/store/migrations/20251007-relate-tax-rate-to-invoice.ts +24 -0
  13. package/api/src/store/migrations/20251009-add-tax-behavior.ts +21 -0
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/invoice-item.ts +10 -0
  16. package/api/src/store/models/price.ts +7 -0
  17. package/api/src/store/models/product.ts +7 -0
  18. package/api/src/store/models/tax-rate.ts +352 -0
  19. package/api/tests/models/tax-rate.spec.ts +777 -0
  20. package/blocklet.yml +2 -2
  21. package/package.json +6 -6
  22. package/public/currencies/dollar.png +0 -0
  23. package/src/components/collapse.tsx +3 -2
  24. package/src/components/drawer-form.tsx +2 -1
  25. package/src/components/invoice/list.tsx +38 -1
  26. package/src/components/invoice/table.tsx +48 -2
  27. package/src/components/metadata/form.tsx +2 -2
  28. package/src/components/payment-intent/list.tsx +19 -1
  29. package/src/components/payouts/list.tsx +19 -1
  30. package/src/components/price/currency-select.tsx +105 -48
  31. package/src/components/price/form.tsx +3 -1
  32. package/src/components/product/form.tsx +79 -5
  33. package/src/components/refund/list.tsx +20 -1
  34. package/src/components/subscription/items/actions.tsx +25 -15
  35. package/src/components/subscription/list.tsx +16 -1
  36. package/src/components/tax/actions.tsx +140 -0
  37. package/src/components/tax/filter-toolbar.tsx +230 -0
  38. package/src/components/tax/tax-code-select.tsx +633 -0
  39. package/src/components/tax/tax-rate-form.tsx +177 -0
  40. package/src/components/tax/tax-utils.ts +38 -0
  41. package/src/components/tax/taxCodes.json +10882 -0
  42. package/src/components/uploader.tsx +3 -0
  43. package/src/locales/en.tsx +152 -0
  44. package/src/locales/zh.tsx +149 -0
  45. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  46. package/src/pages/admin/index.tsx +2 -0
  47. package/src/pages/admin/overview.tsx +1114 -322
  48. package/src/pages/admin/products/vendors/index.tsx +4 -2
  49. package/src/pages/admin/tax/create.tsx +104 -0
  50. package/src/pages/admin/tax/detail.tsx +476 -0
  51. package/src/pages/admin/tax/edit.tsx +126 -0
  52. package/src/pages/admin/tax/index.tsx +86 -0
  53. package/src/pages/admin/tax/list.tsx +334 -0
  54. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  55. package/src/pages/home.tsx +6 -3
package/blocklet.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  name: z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk
2
2
  title: Payment Kit
3
- description: The decentralized stripe alike payment solution for blocklets.
3
+ description: A decentralized Stripe-like payment solution for Blocklets.
4
4
  keywords:
5
5
  - payment
6
6
  - stripe
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.21.13
17
+ version: 1.21.14
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.21.13",
3
+ "version": "1.21.14",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -56,9 +56,9 @@
56
56
  "@blocklet/error": "^0.2.5",
57
57
  "@blocklet/js-sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
58
58
  "@blocklet/logger": "^1.16.53-beta-20251011-054719-4ed2f6b7",
59
- "@blocklet/payment-broker-client": "1.21.13",
60
- "@blocklet/payment-react": "1.21.13",
61
- "@blocklet/payment-vendor": "1.21.13",
59
+ "@blocklet/payment-broker-client": "1.21.14",
60
+ "@blocklet/payment-react": "1.21.14",
61
+ "@blocklet/payment-vendor": "1.21.14",
62
62
  "@blocklet/sdk": "^1.16.53-beta-20251011-054719-4ed2f6b7",
63
63
  "@blocklet/ui-react": "^3.1.46",
64
64
  "@blocklet/uploader": "^0.2.15",
@@ -128,7 +128,7 @@
128
128
  "devDependencies": {
129
129
  "@abtnode/types": "^1.16.53-beta-20251011-054719-4ed2f6b7",
130
130
  "@arcblock/eslint-config-ts": "^0.3.3",
131
- "@blocklet/payment-types": "1.21.13",
131
+ "@blocklet/payment-types": "1.21.14",
132
132
  "@types/cookie-parser": "^1.4.9",
133
133
  "@types/cors": "^2.8.19",
134
134
  "@types/debug": "^4.1.12",
@@ -175,5 +175,5 @@
175
175
  "parser": "typescript"
176
176
  }
177
177
  },
178
- "gitHead": "b101cbdeded522328ac7c5ccd2ba44930467fe25"
178
+ "gitHead": "31f93a8310fe5184be8dd4ff23b362906c8a66cf"
179
179
  }
Binary file
@@ -13,6 +13,7 @@ type Props = {
13
13
  onChange?: (value: string, expanded: boolean) => void;
14
14
  lazy?: boolean;
15
15
  card?: boolean;
16
+ timeout?: number;
16
17
  };
17
18
 
18
19
  export default function IconCollapse(rawProps: Props) {
@@ -75,8 +76,8 @@ export default function IconCollapse(rawProps: Props) {
75
76
  </Stack>
76
77
  </Stack>
77
78
 
78
- <Collapse in={expanded} sx={{ width: '100%' }}>
79
- {expanded || props.lazy ? props.children : null}
79
+ <Collapse in={expanded} timeout={props.timeout || 'auto'} sx={{ width: '100%' }}>
80
+ {expanded || !props.lazy ? props.children : null}
80
81
  </Collapse>
81
82
  </>
82
83
  );
@@ -58,7 +58,8 @@ export default function DrawerForm(rawProps: Props) {
58
58
  onClose={handleClose}
59
59
  sx={props.style || {}}
60
60
  width={props.width}
61
- disableEscapeKeyDown>
61
+ disableEscapeKeyDown
62
+ disableScrollLock>
62
63
  <Stack
63
64
  direction="row"
64
65
  className="drawer-form-header-wrapper"
@@ -15,7 +15,7 @@ import type { TInvoiceExpanded } from '@blocklet/payment-types';
15
15
  import { Avatar, CircularProgress, Typography } from '@mui/material';
16
16
  import { useLocalStorageState } from 'ahooks';
17
17
  import { useEffect, useState } from 'react';
18
- import { Link } from 'react-router-dom';
18
+ import { Link, useSearchParams } from 'react-router-dom';
19
19
  import CustomerLink from '../customer/link';
20
20
  import FilterToolbar from '../filter-toolbar';
21
21
  import InvoiceActions from './action';
@@ -24,6 +24,9 @@ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TInvoiceEx
24
24
  const search = new URLSearchParams();
25
25
  Object.keys(params).forEach((key) => {
26
26
  let v = params[key];
27
+ if (v === undefined || v === null || v === '') {
28
+ return;
29
+ }
27
30
  if (key === 'q') {
28
31
  v = Object.entries(v)
29
32
  .map((x) => x.join(':'))
@@ -40,9 +43,11 @@ type SearchProps = {
40
43
  pageSize: number;
41
44
  page: number;
42
45
  customer_id?: string;
46
+ currency_id?: string;
43
47
  subscription_id?: string;
44
48
  q?: any;
45
49
  o?: string;
50
+ tax_rate_id?: string;
46
51
  };
47
52
 
48
53
  type ListProps = {
@@ -58,6 +63,7 @@ type ListProps = {
58
63
  ignore_zero?: boolean;
59
64
  include_staking?: boolean;
60
65
  include_return_staking?: boolean;
66
+ tax_rate_id?: string;
61
67
 
62
68
  mode?: 'admin' | 'customer';
63
69
  };
@@ -69,6 +75,9 @@ const getListKey = (props: ListProps) => {
69
75
  if (props.subscription_id) {
70
76
  return `subscription-invoices-${props.subscription_id}`;
71
77
  }
78
+ if (props.tax_rate_id) {
79
+ return `tax-rate-invoices-${props.tax_rate_id}`;
80
+ }
72
81
 
73
82
  return 'invoices';
74
83
  };
@@ -103,6 +112,7 @@ function InvoiceLink({ invoice, children }: { invoice: TInvoiceExpanded; childre
103
112
  export default function InvoiceList({
104
113
  customer_id = '',
105
114
  subscription_id = '',
115
+ tax_rate_id = '',
106
116
  features = {
107
117
  customer: true,
108
118
  filter: true,
@@ -113,10 +123,16 @@ export default function InvoiceList({
113
123
  ignore_zero = false,
114
124
  mode = 'admin',
115
125
  }: ListProps) {
126
+ const [searchParams] = useSearchParams();
116
127
  const listKey = getListKey({ customer_id, subscription_id });
117
128
 
118
129
  const { t, locale } = useLocaleContext();
119
130
  const defaultPageSize = useDefaultPageSize(20);
131
+
132
+ const urlStatus = searchParams.get('status');
133
+ const urlCurrencyId = searchParams.get('currency_id');
134
+ const urlCustomerId = searchParams.get('customer_id');
135
+
120
136
  const [search, setSearch] = useLocalStorageState<
121
137
  SearchProps & { ignore_zero?: boolean; include_staking?: boolean; include_return_staking?: boolean }
122
138
  >(listKey, {
@@ -124,6 +140,7 @@ export default function InvoiceList({
124
140
  status: status as string,
125
141
  customer_id,
126
142
  subscription_id,
143
+ tax_rate_id,
127
144
  pageSize: defaultPageSize,
128
145
  page: 1,
129
146
  ignore_zero: !!ignore_zero,
@@ -132,6 +149,17 @@ export default function InvoiceList({
132
149
  },
133
150
  });
134
151
 
152
+ useEffect(() => {
153
+ if (urlStatus || urlCurrencyId || urlCustomerId) {
154
+ setSearch((prev) => ({
155
+ ...prev!,
156
+ ...(urlStatus && { status: urlStatus }),
157
+ ...(urlCurrencyId && { currency_id: urlCurrencyId }),
158
+ ...(urlCustomerId && { customer_id: urlCustomerId }),
159
+ }));
160
+ }
161
+ }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
162
+
135
163
  const [data, setData] = useState({}) as any;
136
164
 
137
165
  const refresh = () =>
@@ -140,6 +168,7 @@ export default function InvoiceList({
140
168
  include_staking: !!include_staking,
141
169
  ignore_zero: !!ignore_zero,
142
170
  include_return_staking: !!include_return_staking,
171
+ tax_rate_id,
143
172
  }).then((res: any) => {
144
173
  setData(res);
145
174
  });
@@ -148,6 +177,14 @@ export default function InvoiceList({
148
177
  refresh();
149
178
  }, [search]);
150
179
 
180
+ useEffect(() => {
181
+ if (tax_rate_id && search?.tax_rate_id !== tax_rate_id) {
182
+ // @ts-ignore
183
+ setSearch((prev) => ({ ...(prev || {}), tax_rate_id, page: 1 }));
184
+ }
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps
186
+ }, [tax_rate_id]);
187
+
151
188
  if (!data.list) {
152
189
  return <CircularProgress />;
153
190
  }
@@ -9,6 +9,7 @@ import { useSetState } from 'ahooks';
9
9
 
10
10
  import { styled } from '@mui/system';
11
11
  import { isEmpty } from 'lodash';
12
+ import { useNavigate } from 'react-router-dom';
12
13
  import LineItemActions from '../subscription/items/actions';
13
14
  import { UsageRecordDialog } from '../subscription/items/usage-records';
14
15
  import { getInvoiceUsageReportStartEnd } from '../../libs/util';
@@ -17,6 +18,7 @@ type Props = {
17
18
  invoice: TInvoiceExpanded;
18
19
  simple?: boolean;
19
20
  emptyNodeText?: string;
21
+ mode?: 'admin' | 'portal';
20
22
  };
21
23
 
22
24
  type InvoiceDetailItem = {
@@ -182,8 +184,10 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
182
184
  };
183
185
  }
184
186
 
185
- export default function InvoiceTable({ invoice, simple = false, emptyNodeText = '' }: Props) {
187
+ export default function InvoiceTable({ invoice, simple = false, emptyNodeText = '', mode = 'portal' }: Props) {
186
188
  const { t, locale } = useLocaleContext();
189
+ const isAdmin = mode === 'admin';
190
+ const navigate = useNavigate();
187
191
  const { detail, summary } = getInvoiceRows(invoice, t);
188
192
  const [state, setState] = useSetState({
189
193
  subscriptionId: '',
@@ -293,6 +297,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
293
297
  width: 200,
294
298
  align: 'right',
295
299
  },
300
+
296
301
  {
297
302
  label: t('common.discount'),
298
303
  name: 'discount',
@@ -321,6 +326,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
321
326
  },
322
327
  },
323
328
  },
329
+
324
330
  {
325
331
  label: t('common.amount'),
326
332
  name: 'amount',
@@ -333,6 +339,46 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
333
339
  },
334
340
  },
335
341
  },
342
+ ...(isAdmin
343
+ ? [
344
+ {
345
+ label: t('admin.taxRate.label'),
346
+ name: 'tax_rate',
347
+ width: 100,
348
+ align: 'right',
349
+ options: {
350
+ customBodyRenderLite: (_: string, index: number) => {
351
+ const item = detail[index] as InvoiceDetailItem;
352
+ const taxRate = (item.raw as any).tax_rate;
353
+ if (!taxRate) {
354
+ return (
355
+ <Typography component="span" sx={{ color: 'text.secondary' }}>
356
+
357
+ </Typography>
358
+ );
359
+ }
360
+ return (
361
+ <Tooltip
362
+ title={`${taxRate.display_name || taxRate.id} (${taxRate.percentage}%)`}
363
+ arrow
364
+ placement="top">
365
+ <Typography
366
+ component="span"
367
+ sx={{
368
+ color: 'text.link',
369
+ fontWeight: 500,
370
+ cursor: 'default',
371
+ }}
372
+ onClick={() => navigate(`/admin/tax/${taxRate.id}`)}>
373
+ {taxRate.percentage}%
374
+ </Typography>
375
+ </Tooltip>
376
+ );
377
+ },
378
+ },
379
+ },
380
+ ]
381
+ : []),
336
382
  ...(simple
337
383
  ? []
338
384
  : [
@@ -343,7 +389,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
343
389
  options: {
344
390
  customBodyRenderLite: (_: string, index: number) => {
345
391
  const item = detail[index] as InvoiceDetailItem;
346
- return <LineItemActions data={item as any} />;
392
+ return <LineItemActions data={item.raw as any} mode={mode} />;
347
393
  },
348
394
  },
349
395
  },
@@ -219,7 +219,7 @@ export default function MetadataForm({
219
219
  message: t('common.maxLength', { len: 40 }),
220
220
  },
221
221
  }}
222
- label="Key *"
222
+ label="Key"
223
223
  placeholder="Key"
224
224
  // @ts-ignore
225
225
  ref={errors?.metadata?.[index]?.key ? errorRef : null}
@@ -229,7 +229,7 @@ export default function MetadataForm({
229
229
  size="small"
230
230
  errorPosition="right"
231
231
  name={`metadata.${index}.value`}
232
- label="Value *"
232
+ label="Value"
233
233
  placeholder="Value"
234
234
  rules={{
235
235
  validate: (value: any) => {
@@ -13,7 +13,7 @@ import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
14
  import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
16
+ import { Link, useSearchParams } from 'react-router-dom';
17
17
 
18
18
  import { debounce } from '../../libs/util';
19
19
  import CustomerLink from '../customer/link';
@@ -39,6 +39,7 @@ type SearchProps = {
39
39
  pageSize: number;
40
40
  page: number;
41
41
  customer_id?: string;
42
+ currency_id?: string;
42
43
  invoice_id?: string;
43
44
  q?: any;
44
45
  o?: any;
@@ -75,9 +76,15 @@ export default function PaymentList({
75
76
  },
76
77
  }: ListProps) {
77
78
  const { t } = useLocaleContext();
79
+ const [searchParams] = useSearchParams();
78
80
 
79
81
  const listKey = getListKey({ customer_id, invoice_id });
80
82
  const defaultPageSize = useDefaultPageSize(20);
83
+
84
+ const urlStatus = searchParams.get('status');
85
+ const urlCurrencyId = searchParams.get('currency_id');
86
+ const urlCustomerId = searchParams.get('customer_id');
87
+
81
88
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
82
89
  defaultValue: {
83
90
  status: '',
@@ -88,6 +95,17 @@ export default function PaymentList({
88
95
  },
89
96
  });
90
97
 
98
+ useEffect(() => {
99
+ if (urlStatus || urlCurrencyId || urlCustomerId) {
100
+ setSearch((prev) => ({
101
+ ...prev!,
102
+ ...(urlStatus && { status: urlStatus }),
103
+ ...(urlCurrencyId && { currency_id: urlCurrencyId }),
104
+ ...(urlCustomerId && { customer_id: urlCustomerId }),
105
+ }));
106
+ }
107
+ }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
108
+
91
109
  const [data, setData] = useState({}) as any;
92
110
 
93
111
  const fetchListData = () => {
@@ -13,7 +13,7 @@ import type { TPayoutExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
14
  import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
16
+ import { Link, useSearchParams } from 'react-router-dom';
17
17
 
18
18
  import DID from '@arcblock/ux/lib/DID';
19
19
  import ShortenLabel from '@arcblock/ux/lib/UserCard/Content/shorten-label';
@@ -42,6 +42,7 @@ type SearchProps = {
42
42
  pageSize: number;
43
43
  page: number;
44
44
  customer_id?: string;
45
+ currency_id?: string;
45
46
  payment_intent_id?: string;
46
47
  q?: any;
47
48
  o?: any;
@@ -80,9 +81,15 @@ export default function PayoutList({
80
81
  },
81
82
  }: ListProps) {
82
83
  const { t } = useLocaleContext();
84
+ const [searchParams] = useSearchParams();
83
85
 
84
86
  const listKey = getListKey({ customer_id, payment_intent_id });
85
87
  const defaultPageSize = useDefaultPageSize(20);
88
+
89
+ const urlStatus = searchParams.get('status');
90
+ const urlCurrencyId = searchParams.get('currency_id');
91
+ const urlCustomerId = searchParams.get('customer_id');
92
+
86
93
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
87
94
  defaultValue: {
88
95
  status: status as string,
@@ -93,6 +100,17 @@ export default function PayoutList({
93
100
  },
94
101
  });
95
102
 
103
+ useEffect(() => {
104
+ if (urlStatus || urlCurrencyId || urlCustomerId) {
105
+ setSearch((prev) => ({
106
+ ...prev!,
107
+ ...(urlStatus && { status: urlStatus }),
108
+ ...(urlCurrencyId && { currency_id: urlCurrencyId }),
109
+ ...(urlCustomerId && { customer_id: urlCustomerId }),
110
+ }));
111
+ }
112
+ }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
113
+
96
114
  const [data, setData] = useState({}) as any;
97
115
 
98
116
  useEffect(() => {
@@ -2,11 +2,9 @@ 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, useEffect } from 'react';
5
+ import { useState, useEffect, useMemo } from 'react';
6
6
  import type { LiteralUnion } from 'type-fest';
7
7
 
8
- import { flatten } from 'lodash';
9
- import { getSupportedPaymentMethods } from '../../libs/util';
10
8
  import Currency from '../currency';
11
9
 
12
10
  type Props = {
@@ -19,6 +17,12 @@ type Props = {
19
17
  selectSX?: SxProps;
20
18
  currencyFilter?: (currency: any) => boolean;
21
19
  hideMethod?: boolean;
20
+ includeAllOption?: boolean;
21
+ allOption?: {
22
+ value: string;
23
+ label: string;
24
+ };
25
+ selectedSx?: SxProps;
22
26
  };
23
27
 
24
28
  export default function CurrencySelect({
@@ -31,16 +35,29 @@ export default function CurrencySelect({
31
35
  selectSX = {},
32
36
  currencyFilter = () => true,
33
37
  hideMethod = false,
38
+ includeAllOption = false,
39
+ allOption = { value: 'all', label: 'All' },
40
+ selectedSx = {},
34
41
  }: Props) {
35
42
  const { t } = useLocaleContext();
36
43
  const { settings } = usePaymentContext();
37
44
  const [mode, setMode] = useState(initialMode);
38
45
 
39
- const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
46
+ const currencyGroups = useMemo(() => {
47
+ return (settings.paymentMethods || []).map((method) => ({
48
+ method,
49
+ currencies: (method.payment_currencies || []).map((currency: any) => ({
50
+ ...currency,
51
+ method,
52
+ })),
53
+ }));
54
+ }, [settings.paymentMethods]);
55
+
56
+ const allCurrencies = useMemo(() => currencyGroups.flatMap((group) => group.currencies), [currencyGroups]);
40
57
 
41
58
  useEffect(() => {
42
- if (value && initialMode === 'selected' && currencies.length > 0) {
43
- const currency = currencies.find((c) => c.id === value);
59
+ if (value && initialMode === 'selected' && allCurrencies.length > 0) {
60
+ const currency = allCurrencies.find((c) => c.id === value);
44
61
  if (currency && !hasSelected(currency)) {
45
62
  const timer = setTimeout(() => {
46
63
  onSelect(value);
@@ -49,7 +66,7 @@ export default function CurrencySelect({
49
66
  }
50
67
  }
51
68
  return undefined;
52
- }, [value, initialMode]);
69
+ }, [value, initialMode, allCurrencies, hasSelected]);
53
70
 
54
71
  const handleSelect = (e: any) => {
55
72
  if (disabled) {
@@ -59,15 +76,36 @@ export default function CurrencySelect({
59
76
  onSelect(e.target.value);
60
77
  };
61
78
 
62
- const selectedCurrency = currencies.find((x) => x.id === value);
79
+ const selectedCurrency = allCurrencies.find((x) => x.id === value);
63
80
 
64
- const selectedPaymentMethod = settings.paymentMethods.find((x) => x.payment_currencies.some((c) => c.id === value));
81
+ const selectedPaymentMethod = currencyGroups.find((group) =>
82
+ group.currencies.some((currency) => currency.id === value)
83
+ )?.method;
65
84
 
66
- const extraCurrencies = getSupportedPaymentMethods(settings.paymentMethods, (x) => !hasSelected(x));
85
+ const availableGroups = useMemo(() => {
86
+ return currencyGroups
87
+ .map((group) => ({
88
+ method: group.method,
89
+ currencies: group.currencies.filter((currency) => currencyFilter(currency) && !hasSelected(currency)),
90
+ }))
91
+ .filter((group) => group.currencies.length > 0);
92
+ }, [currencyGroups, hasSelected]);
67
93
 
68
- const canSelect = extraCurrencies.length > 0 && !disabled;
94
+ const canSelect = availableGroups.length > 0 && !disabled;
69
95
 
70
96
  if (mode === 'selected') {
97
+ const isAllSelected = includeAllOption && value === allOption.value;
98
+ let displayLabel = '';
99
+ if (isAllSelected) {
100
+ displayLabel = allOption.label;
101
+ } else if (selectedCurrency?.symbol) {
102
+ displayLabel = hideMethod
103
+ ? selectedCurrency.symbol
104
+ : `${selectedCurrency.symbol} (${selectedPaymentMethod?.name || ''})`;
105
+ } else if (!hideMethod && selectedPaymentMethod?.name) {
106
+ displayLabel = `(${selectedPaymentMethod.name})`;
107
+ }
108
+
71
109
  return (
72
110
  <Typography
73
111
  onClick={() => {
@@ -81,8 +119,10 @@ export default function CurrencySelect({
81
119
  minWidth: '120px',
82
120
  justifyContent: 'flex-end',
83
121
  textAlign: 'right',
122
+ width: '100%',
123
+ ...selectedSx,
84
124
  }}>
85
- {selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
125
+ {displayLabel}
86
126
  {canSelect && <ArrowDropDown sx={{ color: 'text.secondary', fontSize: 21 }} />}
87
127
  </Typography>
88
128
  );
@@ -97,47 +137,64 @@ export default function CurrencySelect({
97
137
  <Select
98
138
  size="small"
99
139
  value={value}
100
- renderValue={() => (
101
- <Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
102
- {selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
103
- </Typography>
104
- )}
140
+ renderValue={() => {
141
+ if (includeAllOption && value === allOption.value) {
142
+ return (
143
+ <Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
144
+ {allOption.label}
145
+ </Typography>
146
+ );
147
+ }
148
+
149
+ const symbolText = selectedCurrency?.symbol || '';
150
+ const methodText = !hideMethod && selectedPaymentMethod?.name ? ` (${selectedPaymentMethod.name})` : '';
151
+
152
+ return (
153
+ <Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
154
+ {`${symbolText}${methodText}`.trim()}
155
+ </Typography>
156
+ );
157
+ }}
105
158
  onChange={handleSelect}
106
159
  open
107
160
  sx={{ width, ...selectSX }}
108
161
  disabled={disabled}
109
162
  onClose={() => setMode(initialMode)}>
110
- {extraCurrencies
111
- .map((method) => {
112
- const filteredCurrencies = method.payment_currencies.filter(currencyFilter);
113
- if (filteredCurrencies.length === 0) {
114
- return null;
115
- }
116
-
117
- return [
118
- hideMethod ? null : (
119
- <ListSubheader
120
- key={method.id}
121
- sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
122
- {method.name}
123
- </ListSubheader>
124
- ),
125
- ...filteredCurrencies.map((currency) => (
126
- <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
127
- <Stack direction="row" sx={{ width: '100%', justifyContent: 'space-between', gap: 2 }}>
128
- {hideMethod ? null : <Currency logo={currency.logo} name={currency.name} />}
129
- <Typography
130
- sx={{
131
- fontWeight: hideMethod ? 'normal' : 'bold',
132
- }}>
133
- {currency.symbol}
134
- </Typography>
135
- </Stack>
136
- </MenuItem>
137
- )),
138
- ];
139
- })
140
- .filter(Boolean)}
163
+ {includeAllOption ? (
164
+ <MenuItem value={allOption.value}>
165
+ <Typography
166
+ sx={{
167
+ fontWeight: 'bold',
168
+ }}>
169
+ {allOption.label}
170
+ </Typography>
171
+ </MenuItem>
172
+ ) : null}
173
+ {availableGroups.flatMap((group) => {
174
+ const header = hideMethod ? null : (
175
+ <ListSubheader
176
+ key={group.method.id}
177
+ sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
178
+ {group.method.name}
179
+ </ListSubheader>
180
+ );
181
+
182
+ const items = group.currencies.map((currency) => (
183
+ <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
184
+ <Stack direction="row" sx={{ width: '100%', justifyContent: 'space-between', gap: 2 }}>
185
+ {hideMethod ? null : <Currency logo={currency.logo} name={currency.name} />}
186
+ <Typography
187
+ sx={{
188
+ fontWeight: hideMethod ? 'normal' : 'bold',
189
+ }}>
190
+ {currency.symbol}
191
+ </Typography>
192
+ </Stack>
193
+ </MenuItem>
194
+ ));
195
+
196
+ return header ? [header, ...items] : items;
197
+ })}
141
198
  </Select>
142
199
  );
143
200
  }
@@ -380,7 +380,9 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
380
380
  }}>
381
381
  <Stack spacing={2} sx={{ flex: 1, minWidth: { xs: 'auto', sm: '300px' }, '>div': { width: '100%' } }}>
382
382
  <Box sx={{ width: '100%' }}>
383
- <FormLabel tooltip={t('admin.price.amountTip')}>{t('admin.price.amount')}</FormLabel>
383
+ <FormLabel tooltip={t('admin.price.amountTip')} description={t('admin.price.amountDescription')}>
384
+ {t('admin.price.amount')}
385
+ </FormLabel>
384
386
  <Controller
385
387
  name={getFieldName('unit_amount')}
386
388
  control={control}