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.
- package/api/src/libs/api.ts +5 -0
- package/api/src/libs/session.ts +7 -1
- package/api/src/libs/util.ts +20 -0
- package/api/src/routes/connect/collect-batch.ts +65 -24
- package/api/src/routes/prices.ts +10 -7
- package/api/src/routes/pricing-table.ts +14 -2
- package/api/src/routes/subscriptions.ts +60 -1
- package/api/src/store/models/price.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -17
- package/src/components/filter-toolbar.tsx +41 -17
- package/src/components/layout/admin.tsx +2 -1
- package/src/components/payment-link/after-pay.tsx +1 -1
- package/src/components/payment-link/before-pay.tsx +6 -0
- package/src/components/payment-link/item.tsx +5 -2
- package/src/components/payment-link/product-select.tsx +4 -3
- package/src/components/price/currency-select.tsx +59 -6
- package/src/components/price/form.tsx +71 -14
- package/src/components/price/upsell-select.tsx +1 -1
- package/src/components/price/upsell.tsx +4 -2
- package/src/components/pricing-table/payment-settings.tsx +10 -7
- package/src/components/pricing-table/price-item.tsx +3 -2
- package/src/components/pricing-table/product-settings.tsx +2 -0
- package/src/components/product/cross-sell-select.tsx +7 -4
- package/src/components/product/cross-sell.tsx +5 -2
- package/src/components/section/header.tsx +3 -2
- package/src/components/subscription/list.tsx +4 -0
- package/src/pages/admin/products/links/create.tsx +1 -0
- package/src/pages/admin/products/links/detail.tsx +10 -4
- package/src/pages/admin/products/links/index.tsx +3 -2
- package/src/pages/admin/products/prices/list.tsx +19 -4
- package/src/pages/admin/products/pricing-tables/create.tsx +13 -4
- package/src/pages/admin/products/pricing-tables/detail.tsx +4 -2
- package/src/pages/admin/products/products/create.tsx +5 -2
- package/src/pages/admin/products/products/detail.tsx +26 -4
- 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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
<
|
|
290
|
-
|
|
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,
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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}
|
|
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
|
|
34
|
-
|
|
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: [],
|
|
46
|
+
metadata: [],
|
|
47
|
+
currency_id: settings.baseCurrency.id,
|
|
46
48
|
},
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
const changes = methods.watch([
|
|
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 />} */}
|