payment-kit 1.16.4 → 1.16.5

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 (34) hide show
  1. package/api/src/libs/api.ts +5 -0
  2. package/api/src/libs/session.ts +7 -1
  3. package/api/src/routes/prices.ts +10 -7
  4. package/api/src/routes/pricing-table.ts +14 -2
  5. package/api/src/routes/subscriptions.ts +2 -0
  6. package/api/src/store/models/price.ts +1 -0
  7. package/blocklet.yml +1 -1
  8. package/package.json +17 -17
  9. package/src/components/filter-toolbar.tsx +41 -17
  10. package/src/components/layout/admin.tsx +2 -1
  11. package/src/components/payment-link/after-pay.tsx +1 -1
  12. package/src/components/payment-link/before-pay.tsx +6 -0
  13. package/src/components/payment-link/item.tsx +5 -2
  14. package/src/components/payment-link/product-select.tsx +4 -3
  15. package/src/components/price/currency-select.tsx +59 -6
  16. package/src/components/price/form.tsx +71 -14
  17. package/src/components/price/upsell-select.tsx +1 -1
  18. package/src/components/price/upsell.tsx +4 -2
  19. package/src/components/pricing-table/payment-settings.tsx +10 -7
  20. package/src/components/pricing-table/price-item.tsx +3 -2
  21. package/src/components/pricing-table/product-settings.tsx +2 -0
  22. package/src/components/product/cross-sell-select.tsx +7 -4
  23. package/src/components/product/cross-sell.tsx +5 -2
  24. package/src/components/section/header.tsx +3 -2
  25. package/src/components/subscription/list.tsx +4 -0
  26. package/src/pages/admin/products/links/create.tsx +1 -0
  27. package/src/pages/admin/products/links/detail.tsx +10 -4
  28. package/src/pages/admin/products/links/index.tsx +3 -2
  29. package/src/pages/admin/products/prices/list.tsx +19 -4
  30. package/src/pages/admin/products/pricing-tables/create.tsx +13 -4
  31. package/src/pages/admin/products/pricing-tables/detail.tsx +4 -2
  32. package/src/pages/admin/products/products/create.tsx +5 -2
  33. package/src/pages/admin/products/products/detail.tsx +26 -4
  34. package/src/pages/admin/products/products/index.tsx +4 -2
@@ -81,6 +81,11 @@ export const getWhereFromKvQuery = (query?: string) => {
81
81
  const likes: any = [];
82
82
  const fn = (kv: string) => {
83
83
  const [k, v] = kv.split(':');
84
+ if (k && !v && !k.includes('like')) {
85
+ out[k as string] = {
86
+ [Op.in]: [],
87
+ };
88
+ }
84
89
  if (v) {
85
90
  let value = decodeURIComponent(v).replace('+', ' ');
86
91
  if (value.includes(',')) {
@@ -113,7 +113,10 @@ export function getCheckoutAmount(items: TLineItemExpanded[], currencyId: string
113
113
  }
114
114
 
115
115
  export function getRecurringPeriod(recurring: PriceRecurring) {
116
- const { interval } = recurring;
116
+ const { interval } = recurring || {};
117
+ if (!interval) {
118
+ return 0;
119
+ }
117
120
  const count = +recurring.interval_count || 1;
118
121
  const dayInMs = 24 * 60 * 60 * 1000;
119
122
 
@@ -240,6 +243,9 @@ export function canUpsell(from: TPrice, to: TPrice) {
240
243
  // longer periods
241
244
  const fromPeriod = getRecurringPeriod(from.recurring as PriceRecurring);
242
245
  const toPeriod = getRecurringPeriod(to.recurring as PriceRecurring);
246
+ if (!fromPeriod || !toPeriod) {
247
+ return false;
248
+ }
243
249
  if (fromPeriod >= toPeriod) {
244
250
  return false;
245
251
  }
@@ -306,8 +306,8 @@ router.put('/:id', auth, async (req, res) => {
306
306
  pick(
307
307
  req.body,
308
308
  locked
309
- ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell', 'lookup_key', ...quantityKeys]
310
- : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell', ...quantityKeys] // prettier-ignore
309
+ ? ['nickname', 'description', 'metadata', 'upsell', 'lookup_key', ...quantityKeys]
310
+ : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'currency_id', 'lookup_key', 'currency_options', 'upsell', ...quantityKeys] // prettier-ignore
311
311
  )
312
312
  );
313
313
 
@@ -319,14 +319,17 @@ router.put('/:id', auth, async (req, res) => {
319
319
  }
320
320
 
321
321
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
322
- const currency = currencies.find((x) => x.id === doc.currency_id);
322
+ const currency =
323
+ currencies.find((x) => x.id === updates?.currency_id || '') || currencies.find((x) => x.id === doc.currency_id);
323
324
  if (!currency) {
324
- return res.status(400).json({ error: `currency used in price not found or not active: ${doc.currency_id}` });
325
+ return res
326
+ .status(400)
327
+ .json({ error: `currency used in price not found or not active: ${updates?.currency_id || doc.currency_id}` });
325
328
  }
326
329
  if (updates.unit_amount) {
327
330
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
328
331
  if (updates.currency_options) {
329
- const exist = updates.currency_options.find((x) => x.currency_id === doc.currency_id);
332
+ const exist = updates.currency_options.find((x) => x.currency_id === currency.id);
330
333
  if (exist) {
331
334
  exist.unit_amount = fromUnitToToken(updates.unit_amount as string, currency.decimal);
332
335
  }
@@ -334,12 +337,12 @@ router.put('/:id', auth, async (req, res) => {
334
337
  }
335
338
  if (updates.currency_options) {
336
339
  updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
337
- const index = updates.currency_options.findIndex((x) => x.currency_id === doc.currency_id);
340
+ const index = updates.currency_options.findIndex((x) => x.currency_id === currency.id);
338
341
  if (index > -1) {
339
342
  updates.unit_amount = updates.currency_options[index]?.unit_amount;
340
343
  } else {
341
344
  updates.currency_options.unshift({
342
- currency_id: doc.currency_id,
345
+ currency_id: currency.id,
343
346
  unit_amount: doc.unit_amount,
344
347
  tiers: null,
345
348
  custom_unit_amount: null,
@@ -141,8 +141,20 @@ router.get('/:id', async (req, res) => {
141
141
  return res.status(404).json({ error: 'pricing table not found' });
142
142
  }
143
143
 
144
- const currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
144
+ let currency = await PaymentCurrency.findOne({ where: { livemode: doc.livemode, is_base_currency: true } });
145
145
  const expanded = await doc.expand();
146
+ if (expanded.items.length > 0) {
147
+ const index = expanded.items.findIndex((x: any) => x.price?.currency_id === currency?.id);
148
+ if (index === -1) {
149
+ currency = await PaymentCurrency.findOne({
150
+ where: {
151
+ livemode: doc.livemode,
152
+ // @ts-ignore
153
+ id: expanded?.items?.[0]?.price?.currency_id,
154
+ },
155
+ });
156
+ }
157
+ }
146
158
  res.json({ ...expanded, currency });
147
159
  });
148
160
 
@@ -317,7 +329,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
317
329
 
318
330
  raw.livemode = doc.livemode;
319
331
  raw.created_via = 'portal';
320
- raw.currency_id = currency?.id;
332
+ raw.currency_id = (req.query?.currencyId as string) ?? currency?.id;
321
333
 
322
334
  if (req.query.redirect) {
323
335
  raw.success_url = req.query.redirect as string;
@@ -83,12 +83,14 @@ const schema = createListParamSchema<{
83
83
  customer_id?: string;
84
84
  customer_did?: string;
85
85
  activeFirst?: boolean;
86
+ price_id?: string;
86
87
  order?: string | string[] | OrderItem | OrderItem[];
87
88
  }>({
88
89
  status: Joi.string().empty(''),
89
90
  customer_id: Joi.string().empty(''),
90
91
  customer_did: Joi.string().empty(''),
91
92
  activeFirst: Joi.boolean().optional(),
93
+ price_id: Joi.string().empty(''),
92
94
  order: Joi.alternatives()
93
95
  .try(
94
96
  Joi.string(),
@@ -350,6 +350,7 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
350
350
  {
351
351
  ...x,
352
352
  unit_amount: fromTokenToUnit(x.unit_amount, currency.decimal).toString(),
353
+ currency,
353
354
  },
354
355
  ['currency']
355
356
  ) as any;
package/blocklet.yml CHANGED
@@ -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.16.4
17
+ version: 1.16.5
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.16.4",
3
+ "version": "1.16.5",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -44,29 +44,29 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@abtnode/cron": "1.16.33-beta-20241031-073543-49b1ff9b",
47
- "@arcblock/did": "^1.18.147",
47
+ "@arcblock/did": "^1.18.150",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.10.67",
50
- "@arcblock/did-util": "^1.18.147",
51
- "@arcblock/jwt": "^1.18.147",
52
- "@arcblock/ux": "^2.10.67",
53
- "@arcblock/validator": "^1.18.147",
49
+ "@arcblock/did-connect": "^2.10.74",
50
+ "@arcblock/did-util": "^1.18.150",
51
+ "@arcblock/jwt": "^1.18.150",
52
+ "@arcblock/ux": "^2.10.74",
53
+ "@arcblock/validator": "^1.18.150",
54
54
  "@blocklet/js-sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
55
55
  "@blocklet/logger": "1.16.33-beta-20241031-073543-49b1ff9b",
56
- "@blocklet/payment-react": "1.16.4",
56
+ "@blocklet/payment-react": "1.16.5",
57
57
  "@blocklet/sdk": "1.16.33-beta-20241031-073543-49b1ff9b",
58
- "@blocklet/ui-react": "^2.10.67",
59
- "@blocklet/uploader": "^0.1.52",
58
+ "@blocklet/ui-react": "^2.10.74",
59
+ "@blocklet/uploader": "^0.1.53",
60
60
  "@blocklet/xss": "^0.1.12",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
63
63
  "@mui/material": "^5.16.6",
64
64
  "@mui/system": "^5.16.6",
65
- "@ocap/asset": "^1.18.147",
66
- "@ocap/client": "^1.18.147",
67
- "@ocap/mcrypto": "^1.18.147",
68
- "@ocap/util": "^1.18.147",
69
- "@ocap/wallet": "^1.18.147",
65
+ "@ocap/asset": "^1.18.150",
66
+ "@ocap/client": "^1.18.150",
67
+ "@ocap/mcrypto": "^1.18.150",
68
+ "@ocap/util": "^1.18.150",
69
+ "@ocap/wallet": "^1.18.150",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
@@ -120,7 +120,7 @@
120
120
  "devDependencies": {
121
121
  "@abtnode/types": "1.16.33-beta-20241031-073543-49b1ff9b",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.16.4",
123
+ "@blocklet/payment-types": "1.16.5",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -166,5 +166,5 @@
166
166
  "parser": "typescript"
167
167
  }
168
168
  },
169
- "gitHead": "d2c53c8bec3a4a34d0ef3a011ec215b69bc9a5ab"
169
+ "gitHead": "74e38987168230f01af52d1aff14bfa6a18c0578"
170
170
  }
@@ -1,12 +1,20 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { api, getCustomerAvatar, useMobile, usePaymentContext } from '@blocklet/payment-react';
2
+ import {
3
+ api,
4
+ findCurrency,
5
+ formatPrice,
6
+ getCustomerAvatar,
7
+ useMobile,
8
+ usePaymentContext,
9
+ } from '@blocklet/payment-react';
3
10
  import type { TCustomer } from '@blocklet/payment-types';
4
11
  import { Add, Close } from '@mui/icons-material';
5
12
  import { Button, Menu, MenuItem } from '@mui/material';
6
13
  import { Box, styled } from '@mui/system';
7
14
  import { useEffect, useState } from 'react';
8
15
 
9
- import { ProductsProvider } from '../contexts/products';
16
+ import { flatten } from 'lodash';
17
+ import { ProductsProvider, useProductsContext } from '../contexts/products';
10
18
  import InfoCard from './info-card';
11
19
  import ProductSelect from './payment-link/product-select';
12
20
 
@@ -50,6 +58,7 @@ export default function FilterToolbar(props: Props) {
50
58
  setSearch({
51
59
  ...search,
52
60
  ...obj,
61
+ page: 1,
53
62
  });
54
63
  };
55
64
 
@@ -73,7 +82,9 @@ export default function FilterToolbar(props: Props) {
73
82
  {currency ? (
74
83
  <SearchCurrency search={search} setSearch={handleSearch} />
75
84
  ) : (
76
- <SearchProducts search={search} setSearch={handleSearch} />
85
+ <ProductsProvider>
86
+ <SearchProducts search={search} setSearch={handleSearch} />
87
+ </ProductsProvider>
77
88
  )}
78
89
  </>
79
90
  )}
@@ -312,10 +323,23 @@ function SearchCustomers({ setSearch, search }: Pick<Props, 'setSearch' | 'searc
312
323
  );
313
324
  }
314
325
 
315
- function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
326
+ function SearchProducts({ setSearch, search }: Pick<Props, 'setSearch' | 'search'>) {
316
327
  const [show, setShow] = useState(null);
317
- const [price, setPrice] = useState({} as any);
318
- const [display, setDisplay] = useState('');
328
+ const { products } = useProductsContext();
329
+ const { settings } = usePaymentContext();
330
+ const defaultPrice =
331
+ search?.price_id && products
332
+ ? flatten(products.map((x) => x.prices)).find((x) => x.id === search?.price_id)
333
+ : ({} as any);
334
+ const [price, setPrice] = useState(defaultPrice);
335
+ const [display, setDisplay] = useState(() => {
336
+ if (search?.price_id && defaultPrice && defaultPrice.id) {
337
+ const product = products.find((x) => x.prices.some((y) => y.id === defaultPrice.id));
338
+ const currency = findCurrency(settings.paymentMethods, defaultPrice.currency_id ?? '') || settings.baseCurrency;
339
+ return `${product?.name}(${formatPrice(price, currency!)})`;
340
+ }
341
+ return '';
342
+ });
319
343
  const isSubscription = window.location.pathname.includes('subscriptions');
320
344
  const { t } = useLocaleContext();
321
345
 
@@ -334,6 +358,7 @@ function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
334
358
  },
335
359
  page: 1,
336
360
  pageSize: 10,
361
+ price_id: price.id,
337
362
  });
338
363
  });
339
364
  }, [price]);
@@ -353,6 +378,7 @@ function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
353
378
  e.stopPropagation();
354
379
  setSearch({
355
380
  q: '',
381
+ price_id: '',
356
382
  });
357
383
  setDisplay('');
358
384
  setShow(null);
@@ -373,17 +399,15 @@ function SearchProducts({ setSearch }: Pick<Props, 'setSearch'>) {
373
399
  e.stopPropagation();
374
400
  setShow(null);
375
401
  }}>
376
- <ProductsProvider>
377
- <ProductSelect
378
- mode="inline"
379
- hasSelected={() => false}
380
- onSelect={(p: any) => {
381
- setPrice(p);
382
- setDisplay(`${p.productName}(${p.displayPrice})`);
383
- setShow(null);
384
- }}
385
- />
386
- </ProductsProvider>
402
+ <ProductSelect
403
+ mode="inline"
404
+ hasSelected={() => false}
405
+ onSelect={(p: any) => {
406
+ setPrice(p);
407
+ setDisplay(`${p.productName}(${p.displayPrice})`);
408
+ setShow(null);
409
+ }}
410
+ />
387
411
  </Menu>
388
412
  </section>
389
413
  );
@@ -5,6 +5,7 @@ import Dashboard from '@blocklet/ui-react/lib/Dashboard';
5
5
  import { styled } from '@mui/system';
6
6
  import { useEffect } from 'react';
7
7
 
8
+ import { Typography } from '@mui/material';
8
9
  import { useSessionContext } from '../../contexts/session';
9
10
 
10
11
  const Root = styled(Dashboard)<{ padding: string }>`
@@ -82,5 +83,5 @@ export default function Layout(props: any) {
82
83
  );
83
84
  }
84
85
 
85
- return t('common.redirecting');
86
+ return <Typography>{t('common.redirecting')}</Typography>;
86
87
  }
@@ -50,7 +50,7 @@ export default function AfterPay() {
50
50
  <TextField
51
51
  {...field}
52
52
  placeholder={t('admin.paymentLink.customMessage')}
53
- helperText={get(errors, field.name)?.message || t('admin.paymentLink.customMessageTip')}
53
+ helperText={(get(errors, field.name)?.message as string) || t('admin.paymentLink.customMessageTip')}
54
54
  error={!!get(errors, field.name)}
55
55
  fullWidth
56
56
  size="small"
@@ -6,6 +6,7 @@ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-
6
6
  import { useSearchParams } from 'react-router-dom';
7
7
 
8
8
  import { get } from 'lodash';
9
+ import { findCurrency, usePaymentContext } from '@blocklet/payment-react';
9
10
  import { useProductsContext } from '../../contexts/products';
10
11
  import { getProductByPriceId, isPriceAligned } from '../../libs/util';
11
12
  import CreateProduct from '../product/create';
@@ -19,6 +20,7 @@ export default function BeforePay({
19
20
  }) {
20
21
  const { t } = useLocaleContext();
21
22
  const [params, setParams] = useSearchParams();
23
+ const { settings } = usePaymentContext();
22
24
  const { products, refresh } = useProductsContext();
23
25
  const {
24
26
  control,
@@ -53,6 +55,10 @@ export default function BeforePay({
53
55
  const product = getProductByPriceId(products, priceId);
54
56
  if (product) {
55
57
  const price = product.prices.find((x) => x.id === priceId);
58
+ const currency = findCurrency(settings.paymentMethods, price?.currency_id ?? '');
59
+ if (currency) {
60
+ setValue('currency_id', currency.id);
61
+ }
56
62
  if (price && price.type === 'recurring') {
57
63
  setValue('invoice_creation.enabled', true);
58
64
  }
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { api, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
3
+ import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TPrice, TProduct, TProductExpanded } from '@blocklet/payment-types';
5
5
  import { Box, Checkbox, FormControlLabel, FormLabel, Stack, TextField, Typography } from '@mui/material';
6
6
  import { useSetState } from 'ahooks';
@@ -45,6 +45,9 @@ export default function LineItem({ prefix, product, valid, onUpdate, onRemove }:
45
45
  }
46
46
  };
47
47
 
48
+ const productCurrency =
49
+ findCurrency(settings.paymentMethods, product.prices[0]?.currency_id ?? '') || settings.baseCurrency;
50
+
48
51
  return (
49
52
  <Box
50
53
  sx={{
@@ -76,7 +79,7 @@ export default function LineItem({ prefix, product, valid, onUpdate, onRemove }:
76
79
  <InfoCard
77
80
  logo={product.images[0]}
78
81
  name={product.name}
79
- description={formatPrice(product.prices[0] as TPrice, settings.baseCurrency)}
82
+ description={formatPrice(product.prices[0] as TPrice, productCurrency!)}
80
83
  />
81
84
  <Controller
82
85
  name={getFieldName('quantity')}
@@ -1,5 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { formatPrice, getPriceCurrencyOptions, usePaymentContext } from '@blocklet/payment-react';
2
+ import { findCurrency, formatPrice, getPriceCurrencyOptions, usePaymentContext } from '@blocklet/payment-react';
3
3
  import type { TProductExpanded } from '@blocklet/payment-types';
4
4
  import { AddOutlined } from '@mui/icons-material';
5
5
  import { Avatar, Box, ListSubheader, MenuItem, Select, Stack, Typography } from '@mui/material';
@@ -52,6 +52,7 @@ export default function ProductSelect({ mode: initialMode, hasSelected, onSelect
52
52
  </Stack>
53
53
  </ListSubheader>,
54
54
  ...product.prices.map((price: any) => {
55
+ const currency = findCurrency(settings.paymentMethods, price.currency_id ?? '') || settings.baseCurrency;
55
56
  return (
56
57
  <MenuItem
57
58
  key={price.id}
@@ -60,11 +61,11 @@ export default function ProductSelect({ mode: initialMode, hasSelected, onSelect
60
61
  onClick={() => {
61
62
  if (callback) {
62
63
  price.productName = product.name;
63
- price.displayPrice = formatPrice(price, settings.baseCurrency);
64
+ price.displayPrice = formatPrice(price, currency!);
64
65
  callback(price);
65
66
  }
66
67
  }}>
67
- <Typography color="text.primary">{formatPrice(price, settings.baseCurrency)}</Typography>
68
+ <Typography color="text.primary">{formatPrice(price, currency!)}</Typography>
68
69
  <Typography color="text.secondary" sx={{ ml: 2 }}>
69
70
  {getPriceCurrencyOptions(price).length > 1
70
71
  ? ` +${getPriceCurrencyOptions(price).length - 1} more currencies`
@@ -5,35 +5,88 @@ import { ListSubheader, MenuItem, Select, Stack, Typography } from '@mui/materia
5
5
  import { useState } from 'react';
6
6
  import type { LiteralUnion } from 'type-fest';
7
7
 
8
+ import { flatten } from 'lodash';
8
9
  import { getSupportedPaymentMethods } from '../../libs/util';
9
10
  import Currency from '../currency';
10
11
 
11
12
  type Props = {
12
- mode: LiteralUnion<'waiting' | 'selecting', string>;
13
+ mode: LiteralUnion<'waiting' | 'selecting' | 'selected', string>;
13
14
  hasSelected: (currency: any) => boolean;
14
15
  onSelect: (currencyId: string) => void;
16
+ value: string;
17
+ width?: string;
18
+ disabled?: boolean;
15
19
  };
16
20
 
17
- export default function CurrencySelect({ mode: initialMode, hasSelected, onSelect }: Props) {
21
+ CurrencySelect.defaultProps = {
22
+ width: '100%',
23
+ disabled: false,
24
+ };
25
+
26
+ export default function CurrencySelect({ mode: initialMode, hasSelected, onSelect, value, width, disabled }: Props) {
18
27
  const { t } = useLocaleContext();
19
28
  const { settings } = usePaymentContext();
20
29
  const [mode, setMode] = useState(initialMode);
21
30
 
22
31
  const handleSelect = (e: any) => {
23
- setMode('waiting');
32
+ if (disabled) {
33
+ return;
34
+ }
35
+ setMode(initialMode);
24
36
  onSelect(e.target.value);
25
37
  };
26
38
 
39
+ const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
40
+
41
+ const selectedCurrency = currencies.find((x) => x.id === value);
42
+
43
+ const selectedPaymentMethod = settings.paymentMethods.find((x) => x.payment_currencies.some((c) => c.id === value));
44
+
45
+ const extraCurrencies = getSupportedPaymentMethods(settings.paymentMethods, (x) => !hasSelected(x));
46
+
47
+ const canSelect = extraCurrencies.length > 0 && !disabled;
48
+
49
+ if (mode === 'selected') {
50
+ return (
51
+ <Typography
52
+ fontSize="12px"
53
+ onClick={() => {
54
+ if (canSelect) {
55
+ setMode('selecting');
56
+ }
57
+ }}
58
+ sx={{ cursor: canSelect ? 'pointer' : 'default', display: 'inline-flex' }}>
59
+ {selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
60
+ </Typography>
61
+ );
62
+ }
63
+
64
+ if (!canSelect) {
65
+ return null;
66
+ }
67
+
27
68
  if (mode === 'selecting') {
28
69
  return (
29
- <Select value="" sx={{ width: 260 }} size="small" onChange={handleSelect}>
30
- {getSupportedPaymentMethods(settings.paymentMethods, (x) => !hasSelected(x)).map((method) => [
70
+ <Select
71
+ size="small"
72
+ value={value}
73
+ renderValue={() => (
74
+ <Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
75
+ {selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
76
+ </Typography>
77
+ )}
78
+ onChange={handleSelect}
79
+ open
80
+ sx={{ width }}
81
+ disabled={disabled}
82
+ onClose={() => setMode(initialMode)}>
83
+ {extraCurrencies.map((method) => [
31
84
  <ListSubheader key={method.id} sx={{ fontSize: '1rem', color: 'text.secondary', lineHeight: '2.5rem' }}>
32
85
  {method.name}
33
86
  </ListSubheader>,
34
87
  ...method.payment_currencies.map((currency) => (
35
88
  <MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
36
- <Stack direction="row" justifyContent="space-between" sx={{ width: '100%' }}>
89
+ <Stack direction="row" justifyContent="space-between" sx={{ width: '100%' }} gap={2}>
37
90
  <Currency logo={currency.logo} name={currency.name} />
38
91
  <Typography fontWeight="bold">{currency.symbol}</Typography>
39
92
  </Stack>
@@ -118,13 +118,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
118
118
  const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
119
119
  const model = useWatch({ control, name: getFieldName('model') });
120
120
  const intervalSelectValue = useWatch({ control, name: getFieldName('recurring.interval') });
121
+ const defaultCurrencyId = useWatch({ control, name: getFieldName('currency_id') });
122
+ const defaultCurrency = findCurrency(settings.paymentMethods, defaultCurrencyId);
121
123
  const quantityPositive = (v: number | undefined) => !v || v.toString().match(/^(0|[1-9]\d*)$/);
122
124
  const intervalCountPositive = (v: number) => Number.isInteger(Number(v)) && v > 0;
123
125
 
124
- const basePaymentMethod = settings.paymentMethods.find((x) =>
125
- x.payment_currencies.some((c) => c.id === settings.baseCurrency.id)
126
- );
127
-
128
126
  const isLocked = priceLocked && window.blocklet?.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
129
127
 
130
128
  const validateAmount = (v: number, currency: { maximum_precision?: number }) => {
@@ -143,6 +141,20 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
143
141
  trigger(getFieldName('recurring.interval_config'));
144
142
  };
145
143
 
144
+ const handleCurrencyChange = (index: number, currencyId: string) => {
145
+ const update = {
146
+ currency_id: currencyId,
147
+ };
148
+ // @ts-ignore
149
+ if (currencies?.fields?.[index]?.currency) {
150
+ // @ts-ignore
151
+ update.currency = findCurrency(settings.paymentMethods, currencyId);
152
+ }
153
+ currencies.update(index, {
154
+ ...currencies.fields[index],
155
+ ...update,
156
+ });
157
+ };
146
158
  return (
147
159
  <Root direction="column" alignItems="flex-start" spacing={2}>
148
160
  {isLocked && <Alert severity="info">{t('admin.price.locked')}</Alert>}
@@ -176,7 +188,13 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
176
188
  control={control}
177
189
  rules={{
178
190
  required: t('admin.price.unit_amount.required'),
179
- validate: (v) => validateAmount(v, settings.baseCurrency),
191
+ validate: (v) => {
192
+ const hasStripError = !stripeCurrencyValidate(v, defaultCurrency);
193
+ if (hasStripError) {
194
+ return t('admin.price.unit_amount.stripeTip');
195
+ }
196
+ return validateAmount(v, defaultCurrency ?? {});
197
+ },
180
198
  }}
181
199
  disabled={isLocked}
182
200
  render={({ field }) => (
@@ -201,14 +219,33 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
201
219
  InputProps={{
202
220
  endAdornment: (
203
221
  <InputAdornment position="end">
204
- {settings.baseCurrency.symbol} ({basePaymentMethod?.name})
222
+ <CurrencySelect
223
+ mode="selected"
224
+ hasSelected={(currency) =>
225
+ currencies.fields.some((x: any) => x.currency_id === currency.id) ||
226
+ currency.id === defaultCurrencyId
227
+ }
228
+ onSelect={(currencyId) => {
229
+ const index = currencies.fields.findIndex((x: any) => x.currency_id === defaultCurrencyId);
230
+ if (index > -1) {
231
+ // @ts-ignore
232
+ handleCurrencyChange(index, currencyId);
233
+ }
234
+ setValue(getFieldName('currency'), findCurrency(settings.paymentMethods, currencyId), {
235
+ shouldValidate: true,
236
+ });
237
+ setValue(getFieldName('currency_id'), currencyId, { shouldValidate: true });
238
+ }}
239
+ value={defaultCurrencyId}
240
+ disabled={isLocked}
241
+ />
205
242
  </InputAdornment>
206
243
  ),
207
244
  }}
208
245
  onChange={(e) => {
209
246
  const { value } = e.target;
210
247
  field.onChange(value);
211
- const index = currencies.fields.findIndex((x: any) => x.currency_id === settings.baseCurrency.id);
248
+ const index = currencies.fields.findIndex((x: any) => x.currency_id === defaultCurrencyId);
212
249
  if (index === -1) {
213
250
  return;
214
251
  }
@@ -244,7 +281,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
244
281
  {hasMoreCurrency(settings.paymentMethods) && (
245
282
  <Stack direction="column" spacing={2}>
246
283
  {currencies.fields.map((item: any, index: number) => {
247
- if (item.currency_id === settings.baseCurrency.id) {
284
+ if (item.currency_id === defaultCurrencyId) {
248
285
  return null;
249
286
  }
250
287
  const fieldName = getFieldName(`currency_options.${index}.unit_amount`);
@@ -277,7 +314,24 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
277
314
  InputProps={{
278
315
  endAdornment: (
279
316
  <InputAdornment position="end">
280
- {currency?.symbol} ({currency?.paymentMethod?.name})
317
+ <CurrencySelect
318
+ mode="selected"
319
+ hasSelected={(c) =>
320
+ currencies.fields.some((x: any) => x.currency_id === c.id) ||
321
+ c.id === defaultCurrencyId
322
+ }
323
+ onSelect={(currencyId) => {
324
+ const cIndex = currencies.fields.findIndex(
325
+ (x: any) => x.currency_id === currency?.id
326
+ );
327
+ if (cIndex > -1) {
328
+ // @ts-ignore
329
+ handleCurrencyChange(cIndex, currencyId);
330
+ }
331
+ }}
332
+ value={currency?.id!}
333
+ disabled={isLocked}
334
+ />
281
335
  </InputAdornment>
282
336
  ),
283
337
  }}
@@ -285,9 +339,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
285
339
  );
286
340
  }}
287
341
  />
288
- <IconButton size="small" disabled={isLocked} onClick={() => handleRemoveCurrency(index)}>
289
- <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
290
- </IconButton>
342
+ {!isLocked && (
343
+ <IconButton size="small" disabled={isLocked} onClick={() => handleRemoveCurrency(index)}>
344
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
345
+ </IconButton>
346
+ )}
291
347
  </Stack>
292
348
  );
293
349
  })}
@@ -295,10 +351,11 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
295
351
  <CurrencySelect
296
352
  mode="waiting"
297
353
  hasSelected={(currency) =>
298
- currencies.fields.some((x: any) => x.currency_id === currency.id) ||
299
- currency.id === settings.baseCurrency.id
354
+ currencies.fields.some((x: any) => x.currency_id === currency.id) || currency.id === defaultCurrencyId
300
355
  }
301
356
  onSelect={(currencyId) => currencies.append({ currency_id: currencyId, unit_amount: 0 })}
357
+ value=""
358
+ width="260px"
302
359
  />
303
360
  )}
304
361
  </Stack>
@@ -92,7 +92,7 @@ export default function UpsellSelect({ price, onSelect, onAdd }: Props) {
92
92
  </MenuItem>
93
93
  {filteredData.map((x) => (
94
94
  <MenuItem key={x.id} value={x.id}>
95
- {formatPrice(x, x.currency)}
95
+ {formatPrice(x, price.currency || x.currency)}
96
96
  </MenuItem>
97
97
  ))}
98
98
  </Select>
@@ -1,5 +1,5 @@
1
1
  import Toast from '@arcblock/ux/lib/Toast';
2
- import { api, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
2
+ import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
3
3
  import type { TPriceExpanded } from '@blocklet/payment-types';
4
4
  import { DeleteOutlineOutlined } from '@mui/icons-material';
5
5
  import { CircularProgress, Grid, IconButton, Stack, Typography } from '@mui/material';
@@ -44,10 +44,12 @@ export function UpsellForm({ data, onChange }: { data: TPriceExpanded; onChange:
44
44
  return <CircularProgress />;
45
45
  }
46
46
 
47
+ const defaultCurrency = findCurrency(settings.paymentMethods, data?.currency_id || '') || settings.baseCurrency;
48
+
47
49
  if (data.upsell?.upsells_to_id) {
48
50
  return (
49
51
  <Stack spacing={1} direction="row" alignItems="center">
50
- <Typography>{formatPrice(data.upsell.upsells_to, settings.baseCurrency)}</Typography>
52
+ <Typography>{formatPrice(data.upsell.upsells_to, defaultCurrency)}</Typography>
51
53
  <IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>
52
54
  <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
53
55
  </IconButton>
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Tabs from '@arcblock/ux/lib/Tabs';
3
- import { formatPrice, usePaymentContext } from '@blocklet/payment-react';
3
+ import { findCurrency, formatPrice, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TPrice, TProduct } from '@blocklet/payment-types';
5
5
  import { Box, Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
6
6
  import get from 'lodash/get';
@@ -125,7 +125,7 @@ export function PricePaymentSettings({ index }: { index: number }) {
125
125
  fullWidth
126
126
  size="small"
127
127
  error={!!get(errors, field.name)}
128
- helperText={get(errors, field.name)?.message || t('admin.paymentLink.customMessageTip')}
128
+ helperText={(get(errors, field.name)?.message as string) || t('admin.paymentLink.customMessageTip')}
129
129
  inputProps={{
130
130
  maxLength: 200,
131
131
  }}
@@ -217,11 +217,14 @@ export function ProductPaymentSettings({ product, prices }: { product: TProduct;
217
217
  const { settings } = usePaymentContext();
218
218
  const { products } = useProductsContext();
219
219
  const [current, setCurrent] = useState(prices[0]?.id);
220
- const tabs = prices.map((x: any) => ({
221
- value: x.id,
222
- label: formatPrice(getPriceFromProducts(products, x.price_id) as TPrice, settings.baseCurrency),
223
- component: <PricePaymentSettings index={x.index} />,
224
- }));
220
+ const tabs = prices.map((x: any) => {
221
+ const currency = findCurrency(settings.paymentMethods, x.currency_id);
222
+ return {
223
+ value: x.id,
224
+ label: formatPrice(getPriceFromProducts(products, x.price_id) as TPrice, currency || settings.baseCurrency),
225
+ component: <PricePaymentSettings index={x.index} />,
226
+ };
227
+ });
225
228
 
226
229
  return (
227
230
  <Box sx={{ px: 2, py: 0, border: '1px solid #eee', borderRadius: 2 }}>
@@ -1,5 +1,5 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { FormInput, formatPrice, usePaymentContext } from '@blocklet/payment-react';
2
+ import { FormInput, findCurrency, formatPrice, usePaymentContext } from '@blocklet/payment-react';
3
3
  import type { TPrice } from '@blocklet/payment-types';
4
4
  import { DeleteOutlineOutlined } from '@mui/icons-material';
5
5
  import { Box, Checkbox, FormControlLabel, IconButton, InputAdornment, Stack, Typography } from '@mui/material';
@@ -23,10 +23,11 @@ export default function PriceItem({ prefix, price, onRemove }: Props) {
23
23
  } = useFormContext();
24
24
  const includeFreeTrial = useWatch({ control, name: getFieldName('include_free_trial') });
25
25
 
26
+ const currency = findCurrency(settings.paymentMethods, price?.currency_id ?? '') || settings.baseCurrency;
26
27
  return (
27
28
  <Box sx={{ width: '100%' }}>
28
29
  <Stack direction="row" alignItems="center" justifyContent="space-between">
29
- <Typography>{formatPrice(price, settings.baseCurrency)}</Typography>
30
+ <Typography>{formatPrice(price, currency!)}</Typography>
30
31
  <IconButton size="small" onClick={onRemove}>
31
32
  <DeleteOutlineOutlined color="inherit" sx={{ opacity: 0.75 }} />
32
33
  </IconButton>
@@ -41,9 +41,11 @@ export default function PricingTableProductSettings({
41
41
  } else if (priceId) {
42
42
  const product = getProductByPriceId(products, priceId);
43
43
  if (product) {
44
+ const price = product?.prices.find((x) => x.id === priceId);
44
45
  items.append({
45
46
  price_id: priceId,
46
47
  product_id: product.id,
48
+ currency_id: price?.currency_id,
47
49
  is_highlight: false,
48
50
  highlight_text: 'popular',
49
51
  adjustable_quantity: {
@@ -1,11 +1,11 @@
1
1
  import { useState } from 'react';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { usePaymentContext } from '@blocklet/payment-react';
3
+ import { findCurrency, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TProductExpanded } from '@blocklet/payment-types';
5
5
  import { FormControl, MenuItem, Select, Stack, Typography } from '@mui/material';
6
6
 
7
7
  import { useProductsContext } from '../../contexts/products';
8
- import { formatProductPrice } from '../../libs/util';
8
+ import { formatProductPrice, isProductCurrenciesMatched } from '../../libs/util';
9
9
  import InfoCard from '../info-card';
10
10
 
11
11
  type Props = {
@@ -27,6 +27,9 @@ export default function CrossSellSelect({ data, onSelect, valid }: Props) {
27
27
  }
28
28
  };
29
29
 
30
+ const defaultCurrency =
31
+ findCurrency(settings.paymentMethods, data?.default_price?.currency_id || '') || settings.baseCurrency;
32
+
30
33
  return (
31
34
  <FormControl error={!valid} sx={{ width: '100%' }}>
32
35
  <Select
@@ -41,12 +44,12 @@ export default function CrossSellSelect({ data, onSelect, valid }: Props) {
41
44
  </Stack>
42
45
  </MenuItem>
43
46
  {products
44
- .filter((x) => x.id !== data.id)
47
+ .filter((x) => x.id !== data.id && isProductCurrenciesMatched(x, data))
45
48
  .map((x: TProductExpanded) => (
46
49
  <MenuItem key={x.id} value={x.id}>
47
50
  <InfoCard
48
51
  name={x.name}
49
- description={formatProductPrice(x as any, settings.baseCurrency, locale)}
52
+ description={formatProductPrice(x as any, defaultCurrency, locale)}
50
53
  logo={x.images[0]}
51
54
  />
52
55
  </MenuItem>
@@ -1,6 +1,6 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { api, formatError, usePaymentContext } from '@blocklet/payment-react';
3
+ import { api, findCurrency, formatError, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TProductExpanded } from '@blocklet/payment-types';
5
5
  import { DeleteOutlineOutlined } from '@mui/icons-material';
6
6
  import { CircularProgress, Grid, IconButton, Stack, Typography } from '@mui/material';
@@ -58,13 +58,16 @@ export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onCh
58
58
  return <CircularProgress />;
59
59
  }
60
60
 
61
+ const defaultCurrency =
62
+ findCurrency(settings.paymentMethods, data?.default_price?.currency_id || '') || settings.baseCurrency;
63
+
61
64
  if (data.cross_sell?.cross_sells_to_id) {
62
65
  const to = data.cross_sell.cross_sells_to;
63
66
  return (
64
67
  <Stack spacing={1} direction="row" alignItems="center">
65
68
  <InfoCard
66
69
  name={to.name}
67
- description={formatProductPrice(to as any, settings.baseCurrency, locale)}
70
+ description={formatProductPrice(to as any, defaultCurrency, locale)}
68
71
  logo={to.images[0]}
69
72
  />
70
73
  <IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>
@@ -2,7 +2,7 @@ import { Stack, Typography } from '@mui/material';
2
2
  import type { ReactNode } from 'react';
3
3
 
4
4
  type Props = {
5
- title: string;
5
+ title: string | ReactNode;
6
6
  children?: ReactNode;
7
7
  mb?: number;
8
8
  mt?: number;
@@ -25,7 +25,8 @@ export default function SectionHeader(props: Props) {
25
25
  xs: '18px',
26
26
  md: '1.25rem',
27
27
  },
28
- }}>
28
+ }}
29
+ component="div">
29
30
  {props.title}
30
31
  </Typography>
31
32
  {props.children}
@@ -29,6 +29,7 @@ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TSubscript
29
29
 
30
30
  type SearchProps = {
31
31
  status?: string;
32
+ price_id?: string;
32
33
  pageSize: number;
33
34
  page: number;
34
35
  customer_id?: string;
@@ -75,6 +76,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
75
76
  customer_id,
76
77
  pageSize: defaultPageSize,
77
78
  page: 1,
79
+ price_id: '',
78
80
  },
79
81
  });
80
82
 
@@ -213,6 +215,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
213
215
  onSearchChange: (text: string) => {
214
216
  if (text) {
215
217
  setSearch({
218
+ ...search!,
216
219
  q: {
217
220
  'like-metadata': text,
218
221
  'like-description': text,
@@ -222,6 +225,7 @@ export default function SubscriptionList({ customer_id, features, status }: List
222
225
  });
223
226
  } else {
224
227
  setSearch({
228
+ ...search!,
225
229
  status: '',
226
230
  customer_id,
227
231
  pageSize: 100,
@@ -85,6 +85,7 @@ export default function CreatePaymentLink() {
85
85
  'subscription_data',
86
86
  'billing_address_collection',
87
87
  'phone_number_collection',
88
+ 'currency_id',
88
89
  ]);
89
90
 
90
91
  useEffect(() => {
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { api, formatError, formatTime, usePaymentContext, Table } from '@blocklet/payment-react';
4
+ import { api, formatError, formatTime, usePaymentContext, Table, findCurrency } from '@blocklet/payment-react';
5
5
  import type { TLineItemExpanded, TPaymentLinkExpanded, TPrice, TProduct } from '@blocklet/payment-types';
6
6
  import { ArrowBackOutlined } from '@mui/icons-material';
7
7
  import { Alert, Box, Button, CircularProgress, Divider, Grid, Stack, Typography } from '@mui/material';
@@ -96,7 +96,8 @@ export default function PaymentLinkDetail(props: { id: string }) {
96
96
  }
97
97
  };
98
98
 
99
- const result = formatPaymentLinkPricing(data, settings.baseCurrency, locale);
99
+ const currency = findCurrency(settings.paymentMethods, data?.currency_id ?? '');
100
+ const result = formatPaymentLinkPricing(data, currency!, locale);
100
101
 
101
102
  const handleEditMetadata = () => {
102
103
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
@@ -233,14 +234,17 @@ export default function PaymentLinkDetail(props: { id: string }) {
233
234
  sort: false,
234
235
  customBodyRenderLite: (_: any, index: number) => {
235
236
  const item = data.line_items[index] as TLineItemExpanded;
237
+ const itemCurrency =
238
+ currency || findCurrency(settings.paymentMethods, item?.price?.currency_id ?? '');
236
239
  return (
237
- <Link to={`/admin/products/${item.price.product_id}`}>
240
+ <Link
241
+ to={`/admin/products/${item.price.product_id}?currency_id=${itemCurrency?.id}&price_id=${item.price.id}`}>
238
242
  <InfoCard
239
243
  name={item.price.product.name}
240
244
  description={formatProductPrice(
241
245
  // @ts-ignore
242
246
  { ...item.price.product, prices: [item.price] },
243
- settings.baseCurrency,
247
+ itemCurrency,
244
248
  locale
245
249
  )}
246
250
  logo={item.price.product.images[0]}
@@ -253,10 +257,12 @@ export default function PaymentLinkDetail(props: { id: string }) {
253
257
  {
254
258
  label: t('common.quantity'),
255
259
  name: 'quantity',
260
+ align: 'right',
256
261
  },
257
262
  {
258
263
  label: t('admin.paymentLink.adjustable'),
259
264
  name: 'price_id',
265
+ align: 'right',
260
266
  options: {
261
267
  customBodyRenderLite: (_: any, index: number) => {
262
268
  const item = data.line_items[index];
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
- import { Status, api, formatTime, usePaymentContext, Table } from '@blocklet/payment-react';
4
+ import { Status, api, formatTime, usePaymentContext, Table, findCurrency } from '@blocklet/payment-react';
5
5
  import type { TPaymentLinkExpanded } from '@blocklet/payment-types';
6
6
  import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
7
7
  import { useRequest } from 'ahooks';
@@ -97,7 +97,8 @@ function PaymentLinks() {
97
97
  sort: false,
98
98
  customBodyRenderLite: (_: string, index: number) => {
99
99
  const item = data.list[index] as TPaymentLinkExpanded;
100
- const result = formatPaymentLinkPricing(item, settings.baseCurrency, locale);
100
+ const currency = findCurrency(settings.paymentMethods, item?.currency_id ?? '') || settings.baseCurrency;
101
+ const result = formatPaymentLinkPricing(item, currency!, locale);
101
102
  return <Link to={`/admin/products/${item.id}`}>{result.priceDisplay}</Link>;
102
103
  },
103
104
  },
@@ -1,6 +1,14 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { Status, formatPrice, formatTime, usePaymentContext, Table } from '@blocklet/payment-react';
3
+ import {
4
+ Status,
5
+ formatPrice,
6
+ formatTime,
7
+ Table,
8
+ getQueryParams,
9
+ usePaymentContext,
10
+ findCurrency,
11
+ } from '@blocklet/payment-react';
4
12
  import { LockOutlined } from '@mui/icons-material';
5
13
  import { Stack, Tooltip, Typography } from '@mui/material';
6
14
  import { Link } from 'react-router-dom';
@@ -12,6 +20,7 @@ import PriceActions from './actions';
12
20
  export default function PricesList({ product, onChange }: { product: Product; onChange: Function }) {
13
21
  const { t, locale } = useLocaleContext();
14
22
  const { settings } = usePaymentContext();
23
+ const query = getQueryParams(window.location.href);
15
24
 
16
25
  const columns = [
17
26
  {
@@ -22,16 +31,22 @@ export default function PricesList({ product, onChange }: { product: Product; on
22
31
  sort: false,
23
32
  customBodyRenderLite: (_: any, index: number) => {
24
33
  const price = product.prices[index] as any;
34
+ const showHighlight = query.price_id === price.id && query.currency_id;
35
+ const priceCurrency = showHighlight
36
+ ? findCurrency(settings.paymentMethods, query.currency_id || '')
37
+ : price.currency;
25
38
  return (
26
39
  <Link to={`/admin/products/${price.id}`} color="text.primary">
27
- <Stack direction="row" alignItems="center" spacing={1} flexWrap="wrap">
40
+ <Stack direction="row" alignItems="center" spacing={1}>
28
41
  {price.locked && (
29
42
  <Tooltip title={t('admin.price.locked')}>
30
43
  <LockOutlined sx={{ color: 'text.secondary' }} />
31
44
  </Tooltip>
32
45
  )}
33
- <Typography component="span">
34
- {formatPrice(price, settings.baseCurrency, '', 1, true, locale)}
46
+ <Typography
47
+ component="span"
48
+ sx={{ backgroundColor: showHighlight ? '#FFF9C4' : 'transparent', whiteSpace: 'nowrap' }}>
49
+ {formatPrice(price, priceCurrency || price.currency, '', 1, true, locale)}
35
50
  </Typography>
36
51
  <Typography component="span">
37
52
  {price.id === product.default_price_id && <Status label="default" color="info" sx={{ height: 18 }} />}
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { api, formatError } from '@blocklet/payment-react';
4
+ import { api, formatError, usePaymentContext } from '@blocklet/payment-react';
5
5
  import type { TPricingTable } from '@blocklet/payment-types';
6
6
  import { AddOutlined, Fullscreen, FullscreenExit } from '@mui/icons-material';
7
7
  import { Box, Button, Stack, Typography } from '@mui/material';
@@ -27,6 +27,7 @@ export default function CreatePricingTable() {
27
27
  const [stashed, setStashed] = useState(0);
28
28
  const fullScreenRef = useRef(null);
29
29
  const [errors, triggerError] = useSetState({});
30
+ const { settings } = usePaymentContext();
30
31
 
31
32
  const methods = useForm<TPricingTable & any>({
32
33
  shouldUnregister: false,
@@ -42,11 +43,19 @@ export default function CreatePricingTable() {
42
43
  highlight_product_id: '',
43
44
  highlight_text: 'popular',
44
45
  items: [],
45
- metadata: [], // FIXME:
46
+ metadata: [],
47
+ currency_id: settings.baseCurrency.id,
46
48
  },
47
49
  });
48
50
 
49
- const changes = methods.watch(['items', 'branding_settings', 'highlight', 'highlight_product_id', 'highlight_text']);
51
+ const changes = methods.watch([
52
+ 'items',
53
+ 'branding_settings',
54
+ 'highlight',
55
+ 'highlight_product_id',
56
+ 'highlight_text',
57
+ 'currency_id',
58
+ ]);
50
59
 
51
60
  useEffect(() => {
52
61
  api.post('/api/pricing-tables/stash', methods.getValues()).then(() => {
@@ -115,7 +124,7 @@ export default function CreatePricingTable() {
115
124
  <Stack height="92vh" spacing={2} direction="row">
116
125
  <Box flex={2} sx={{ borderRight: '1px solid #eee' }} position="relative">
117
126
  <Stack height="100%" spacing={2}>
118
- <Box overflow="auto" sx={{ pr: 2 }}>
127
+ <Box overflow="auto" sx={{ pr: 2, pb: 8 }}>
119
128
  {step === 0 && <PricingTableProductSettings triggerError={triggerError} />}
120
129
  {step === 1 && <PricingTablePaymentSettings />}
121
130
  {/* {step === 2 && <PricingTableCustomerSettings />} */}
@@ -220,14 +220,16 @@ export default function PricingTableDetail(props: { id: string }) {
220
220
  sort: false,
221
221
  customBodyRenderLite: (_: any, index: number) => {
222
222
  const item = data.items[index] as any;
223
+ const itemCurrency = data?.currency || settings.baseCurrency;
223
224
  return (
224
- <Link to={`/admin/products/${item.product_id}`}>
225
+ <Link
226
+ to={`/admin/products/${item.product_id}?currency_id=${itemCurrency?.id}&price_id=${item.price.id}`}>
225
227
  <InfoCard
226
228
  name={item.product.name}
227
229
  description={formatProductPrice(
228
230
  // @ts-ignore
229
231
  { ...item.product, prices: [item.price] },
230
- settings.baseCurrency,
232
+ itemCurrency,
231
233
  locale
232
234
  )}
233
235
  logo={item.product.images[0]}
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { api, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
4
+ import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
5
5
  import { AddOutlined } from '@mui/icons-material';
6
6
  import { Box, Button, Divider, Typography } from '@mui/material';
7
7
  import { cloneDeep } from 'lodash';
@@ -83,9 +83,12 @@ export default function ProductsCreate() {
83
83
  if (expanded) {
84
84
  return t('admin.price.detail');
85
85
  }
86
+ const priceItem = getPrice(index);
87
+ const currency =
88
+ findCurrency(settings.paymentMethods, priceItem?.currency_id || '') || settings.baseCurrency;
86
89
 
87
90
  // @ts-ignore
88
- return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false, locale);
91
+ return formatPrice(priceItem as any, currency, getValues().unit_label, 1, false, locale);
89
92
  }}>
90
93
  <PriceForm prefix={`prices.${index}`} />
91
94
  </Collapse>
@@ -1,8 +1,8 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
- import { api, formatError, formatTime, useMobile, usePaymentContext } from '@blocklet/payment-react';
3
+ import { api, findCurrency, formatError, formatTime, useMobile, usePaymentContext } from '@blocklet/payment-react';
4
4
  import type { TPrice, TProduct, TProductExpanded } from '@blocklet/payment-types';
5
- import { ArrowBackOutlined } from '@mui/icons-material';
5
+ import { ArrowBackOutlined, InfoOutlined } from '@mui/icons-material';
6
6
  import {
7
7
  Alert,
8
8
  AlertTitle,
@@ -13,6 +13,7 @@ import {
13
13
  Divider,
14
14
  Grid,
15
15
  Stack,
16
+ Tooltip,
16
17
  Typography,
17
18
  } from '@mui/material';
18
19
  import { styled } from '@mui/system';
@@ -114,6 +115,9 @@ export default function ProductDetail(props: { id: string }) {
114
115
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
115
116
  };
116
117
 
118
+ const defaultCurrency =
119
+ findCurrency(settings.paymentMethods, data?.default_price?.currency_id) || settings.baseCurrency;
120
+
117
121
  return (
118
122
  <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
119
123
  <Box>
@@ -173,7 +177,7 @@ export default function ProductDetail(props: { id: string }) {
173
177
  {data.name}
174
178
  </Typography>
175
179
  <Typography variant="subtitle1" color="text.lighter">
176
- {formatProductPrice(data as any, settings.baseCurrency, locale)}
180
+ {formatProductPrice(data as any, defaultCurrency, locale)}
177
181
  </Typography>
178
182
  </Stack>
179
183
  </Stack>
@@ -372,7 +376,25 @@ export default function ProductDetail(props: { id: string }) {
372
376
  {isMobile && <Divider />}
373
377
  <Box className="payment-link-column-2" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
374
378
  <Box className="section">
375
- <SectionHeader title={t('admin.product.cross_sell.title')} />
379
+ <SectionHeader
380
+ title={
381
+ <Stack direction="row" alignItems="center" spacing={1}>
382
+ <Typography
383
+ variant="h3"
384
+ sx={{
385
+ fontSize: {
386
+ xs: '18px',
387
+ md: '1.25rem',
388
+ },
389
+ }}>
390
+ {t('admin.product.cross_sell.title')}
391
+ </Typography>
392
+ <Tooltip title={t('admin.product.currencyNotAligned')}>
393
+ <InfoOutlined sx={{ color: 'text.lighter' }} fontSize="small" />
394
+ </Tooltip>
395
+ </Stack>
396
+ }
397
+ />
376
398
  <Box className="section-body">
377
399
  <ProductCrossSell data={data} onChange={runAsync} />
378
400
  </Box>
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
- import { Status, api, formatTime, usePaymentContext, Table } from '@blocklet/payment-react';
4
+ import { Status, api, formatTime, usePaymentContext, Table, findCurrency } from '@blocklet/payment-react';
5
5
  import type { TProductExpanded } from '@blocklet/payment-types';
6
6
  import { CircularProgress } from '@mui/material';
7
7
  import { useEffect, useState } from 'react';
@@ -75,11 +75,13 @@ export default function ProductsList() {
75
75
  filter: true,
76
76
  customBodyRenderLite: (_: string, index: number) => {
77
77
  const item = data.list[index] as TProductExpanded;
78
+ const currency =
79
+ findCurrency(settings.paymentMethods, item.prices[0]?.currency_id ?? '') || settings.baseCurrency;
78
80
  return (
79
81
  <Link to={`/admin/products/${item.id}`}>
80
82
  <InfoCard
81
83
  name={item.name}
82
- description={formatProductPrice(item as any, settings.baseCurrency, locale)}
84
+ description={formatProductPrice(item as any, currency!, locale)}
83
85
  logo={item.images[0]}
84
86
  />
85
87
  </Link>