payment-kit 1.16.3 → 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 (36) hide show
  1. package/api/src/libs/api.ts +5 -0
  2. package/api/src/libs/session.ts +7 -1
  3. package/api/src/libs/util.ts +20 -0
  4. package/api/src/routes/connect/collect-batch.ts +65 -24
  5. package/api/src/routes/prices.ts +10 -7
  6. package/api/src/routes/pricing-table.ts +14 -2
  7. package/api/src/routes/subscriptions.ts +60 -1
  8. package/api/src/store/models/price.ts +1 -0
  9. package/blocklet.yml +1 -1
  10. package/package.json +17 -17
  11. package/src/components/filter-toolbar.tsx +41 -17
  12. package/src/components/layout/admin.tsx +2 -1
  13. package/src/components/payment-link/after-pay.tsx +1 -1
  14. package/src/components/payment-link/before-pay.tsx +6 -0
  15. package/src/components/payment-link/item.tsx +5 -2
  16. package/src/components/payment-link/product-select.tsx +4 -3
  17. package/src/components/price/currency-select.tsx +59 -6
  18. package/src/components/price/form.tsx +71 -14
  19. package/src/components/price/upsell-select.tsx +1 -1
  20. package/src/components/price/upsell.tsx +4 -2
  21. package/src/components/pricing-table/payment-settings.tsx +10 -7
  22. package/src/components/pricing-table/price-item.tsx +3 -2
  23. package/src/components/pricing-table/product-settings.tsx +2 -0
  24. package/src/components/product/cross-sell-select.tsx +7 -4
  25. package/src/components/product/cross-sell.tsx +5 -2
  26. package/src/components/section/header.tsx +3 -2
  27. package/src/components/subscription/list.tsx +4 -0
  28. package/src/pages/admin/products/links/create.tsx +1 -0
  29. package/src/pages/admin/products/links/detail.tsx +10 -4
  30. package/src/pages/admin/products/links/index.tsx +3 -2
  31. package/src/pages/admin/products/prices/list.tsx +19 -4
  32. package/src/pages/admin/products/pricing-tables/create.tsx +13 -4
  33. package/src/pages/admin/products/pricing-tables/detail.tsx +4 -2
  34. package/src/pages/admin/products/products/create.tsx +5 -2
  35. package/src/pages/admin/products/products/detail.tsx +26 -4
  36. package/src/pages/admin/products/products/index.tsx +4 -2
@@ -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 />} */}