payment-kit 1.21.13 → 1.21.15

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 (56) 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 -3
  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 -3
  29. package/src/components/payouts/list.tsx +19 -3
  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 -3
  34. package/src/components/subscription/items/actions.tsx +25 -15
  35. package/src/components/subscription/list.tsx +15 -3
  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/hooks/cache-state.ts +84 -0
  44. package/src/locales/en.tsx +152 -0
  45. package/src/locales/zh.tsx +149 -0
  46. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  47. package/src/pages/admin/index.tsx +2 -0
  48. package/src/pages/admin/overview.tsx +1114 -322
  49. package/src/pages/admin/products/vendors/index.tsx +4 -2
  50. package/src/pages/admin/tax/create.tsx +104 -0
  51. package/src/pages/admin/tax/detail.tsx +476 -0
  52. package/src/pages/admin/tax/edit.tsx +126 -0
  53. package/src/pages/admin/tax/index.tsx +86 -0
  54. package/src/pages/admin/tax/list.tsx +334 -0
  55. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  56. 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.15
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.15",
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.15",
60
+ "@blocklet/payment-react": "1.21.15",
61
+ "@blocklet/payment-vendor": "1.21.15",
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.15",
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": "fd48f9233f19514b537e30b013e09a7d9f7a9f48"
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"
@@ -13,9 +13,9 @@ import {
13
13
  } from '@blocklet/payment-react';
14
14
  import type { TInvoiceExpanded } from '@blocklet/payment-types';
15
15
  import { Avatar, CircularProgress, Typography } from '@mui/material';
16
- import { useLocalStorageState } from 'ahooks';
17
16
  import { useEffect, useState } from 'react';
18
- import { Link } from 'react-router-dom';
17
+ import { Link, useSearchParams } from 'react-router-dom';
18
+ import { useCacheState } from '../../hooks/cache-state';
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,23 +123,39 @@ 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);
120
- const [search, setSearch] = useLocalStorageState<
131
+
132
+ const [search, setSearch] = useCacheState<
121
133
  SearchProps & { ignore_zero?: boolean; include_staking?: boolean; include_return_staking?: boolean }
122
134
  >(listKey, {
123
135
  defaultValue: {
124
136
  status: status as string,
125
137
  customer_id,
126
138
  subscription_id,
139
+ tax_rate_id,
127
140
  pageSize: defaultPageSize,
128
141
  page: 1,
129
142
  ignore_zero: !!ignore_zero,
130
143
  include_staking: !!include_staking,
131
144
  include_return_staking: !!include_return_staking,
132
145
  },
146
+ getUrlParams: () => {
147
+ const params: Record<string, any> = {};
148
+ if (searchParams.has('status')) {
149
+ params.status = searchParams.get('status');
150
+ }
151
+ if (searchParams.has('currency_id')) {
152
+ params.currency_id = searchParams.get('currency_id');
153
+ }
154
+ if (searchParams.has('customer_id')) {
155
+ params.customer_id = searchParams.get('customer_id');
156
+ }
157
+ return params;
158
+ },
133
159
  });
134
160
 
135
161
  const [data, setData] = useState({}) as any;
@@ -140,6 +166,7 @@ export default function InvoiceList({
140
166
  include_staking: !!include_staking,
141
167
  ignore_zero: !!ignore_zero,
142
168
  include_return_staking: !!include_return_staking,
169
+ tax_rate_id,
143
170
  }).then((res: any) => {
144
171
  setData(res);
145
172
  });
@@ -148,6 +175,14 @@ export default function InvoiceList({
148
175
  refresh();
149
176
  }, [search]);
150
177
 
178
+ useEffect(() => {
179
+ if (tax_rate_id && search?.tax_rate_id !== tax_rate_id) {
180
+ // @ts-ignore
181
+ setSearch((prev) => ({ ...(prev || {}), tax_rate_id, page: 1 }));
182
+ }
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ }, [tax_rate_id]);
185
+
151
186
  if (!data.list) {
152
187
  return <CircularProgress />;
153
188
  }
@@ -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) => {
@@ -11,11 +11,11 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPaymentIntentExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
15
+ import { Link, useSearchParams } from 'react-router-dom';
17
16
 
18
17
  import { debounce } from '../../libs/util';
18
+ import { useCacheState } from '../../hooks/cache-state';
19
19
  import CustomerLink from '../customer/link';
20
20
  import FilterToolbar from '../filter-toolbar';
21
21
  import PaymentIntentActions from './actions';
@@ -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,10 +76,12 @@ 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);
81
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
83
+
84
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
82
85
  defaultValue: {
83
86
  status: '',
84
87
  customer_id,
@@ -86,6 +89,19 @@ export default function PaymentList({
86
89
  pageSize: defaultPageSize,
87
90
  page: 1,
88
91
  },
92
+ getUrlParams: () => {
93
+ const params: Record<string, any> = {};
94
+ if (searchParams.has('status')) {
95
+ params.status = searchParams.get('status');
96
+ }
97
+ if (searchParams.has('currency_id')) {
98
+ params.currency_id = searchParams.get('currency_id');
99
+ }
100
+ if (searchParams.has('customer_id')) {
101
+ params.customer_id = searchParams.get('customer_id');
102
+ }
103
+ return params;
104
+ },
89
105
  });
90
106
 
91
107
  const [data, setData] = useState({}) as any;
@@ -11,13 +11,13 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TPayoutExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
15
+ import { Link, useSearchParams } from 'react-router-dom';
17
16
 
18
17
  import DID from '@arcblock/ux/lib/DID';
19
18
  import ShortenLabel from '@arcblock/ux/lib/UserCard/Content/shorten-label';
20
19
  import { debounce, getAppInfo } from '../../libs/util';
20
+ import { useCacheState } from '../../hooks/cache-state';
21
21
  import CustomerLink from '../customer/link';
22
22
  import FilterToolbar from '../filter-toolbar';
23
23
  import PayoutActions from './actions';
@@ -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,10 +81,12 @@ 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);
86
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
88
+
89
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
87
90
  defaultValue: {
88
91
  status: status as string,
89
92
  customer_id,
@@ -91,6 +94,19 @@ export default function PayoutList({
91
94
  pageSize: defaultPageSize,
92
95
  page: 1,
93
96
  },
97
+ getUrlParams: () => {
98
+ const params: Record<string, any> = {};
99
+ if (searchParams.has('status')) {
100
+ params.status = searchParams.get('status');
101
+ }
102
+ if (searchParams.has('currency_id')) {
103
+ params.currency_id = searchParams.get('currency_id');
104
+ }
105
+ if (searchParams.has('customer_id')) {
106
+ params.customer_id = searchParams.get('customer_id');
107
+ }
108
+ return params;
109
+ },
94
110
  });
95
111
 
96
112
  const [data, setData] = useState({}) as any;
@@ -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}