payment-kit 1.13.23 → 1.13.25
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/README.md +4 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/payment-intent.ts +2 -2
- package/api/src/integrations/stripe/handlers/setup-intent.ts +1 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +2 -2
- package/api/src/jobs/event.ts +10 -4
- package/api/src/jobs/webhook.ts +17 -8
- package/api/src/libs/audit.ts +3 -3
- package/api/src/libs/event.ts +3 -0
- package/api/src/libs/util.ts +5 -0
- package/api/src/routes/checkout-sessions.ts +3 -3
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/payment-links.ts +0 -1
- package/api/src/routes/pricing-table.ts +342 -0
- package/api/src/routes/subscriptions.ts +15 -0
- package/api/src/store/migrations/20231017-pricing-table.ts +10 -0
- package/api/src/store/models/index.ts +14 -1
- package/api/src/store/models/pricing-table.ts +107 -0
- package/api/src/store/models/types.ts +53 -0
- package/blocklet.yml +2 -2
- package/package.json +4 -3
- package/src/app.tsx +1 -1
- package/src/components/blockchain/tx.tsx +8 -0
- package/src/components/payment-link/actions.tsx +20 -9
- package/src/components/payment-link/chrome.tsx +5 -3
- package/src/components/payment-link/preview.tsx +8 -5
- package/src/components/payment-link/rename.tsx +3 -3
- package/src/components/price/form.tsx +4 -1
- package/src/components/pricing-table/actions.tsx +126 -0
- package/src/components/pricing-table/customer-settings.tsx +17 -0
- package/src/components/pricing-table/payment-settings.tsx +179 -0
- package/src/components/pricing-table/preview.tsx +34 -0
- package/src/components/pricing-table/price-item.tsx +64 -0
- package/src/components/pricing-table/product-item.tsx +86 -0
- package/src/components/pricing-table/product-settings.tsx +195 -0
- package/src/components/pricing-table/rename.tsx +67 -0
- package/src/libs/util.ts +54 -5
- package/src/locales/en.tsx +28 -0
- package/src/pages/admin/payments/links/create.tsx +1 -1
- package/src/pages/admin/products/index.tsx +8 -13
- package/src/pages/admin/products/pricing-tables/create.tsx +140 -0
- package/src/pages/admin/products/pricing-tables/detail.tsx +237 -0
- package/src/pages/admin/products/pricing-tables/index.tsx +154 -0
- package/src/pages/admin/products/products/create.tsx +8 -4
- package/src/pages/checkout/index.tsx +2 -1
- package/src/pages/checkout/pricing-table.tsx +195 -0
- package/src/pages/admin/products/pricing-tables.tsx +0 -3
package/src/libs/util.ts
CHANGED
|
@@ -132,11 +132,17 @@ export const formatProductPrice = (
|
|
|
132
132
|
return 'No price';
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
export const formatPrice = (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
export const formatPrice = (
|
|
136
|
+
price: TPrice,
|
|
137
|
+
currency: TPaymentCurrency,
|
|
138
|
+
unit_label?: string,
|
|
139
|
+
quantity: number = 1,
|
|
140
|
+
bn: boolean = true
|
|
141
|
+
) => {
|
|
142
|
+
const unit = getPriceUintAmountByCurrency(price, currency);
|
|
143
|
+
const amount = bn
|
|
144
|
+
? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
|
|
145
|
+
: +unit * quantity;
|
|
140
146
|
if (price?.type === 'recurring' && price.recurring) {
|
|
141
147
|
const recurring = formatRecurring(price.recurring, false, '/');
|
|
142
148
|
|
|
@@ -153,6 +159,31 @@ export const formatPrice = (price: TPrice, currency: TPaymentCurrency, unit_labe
|
|
|
153
159
|
return `${amount} ${currency.symbol}`;
|
|
154
160
|
};
|
|
155
161
|
|
|
162
|
+
export const formatPriceAmount = (
|
|
163
|
+
price: TPrice,
|
|
164
|
+
currency: TPaymentCurrency,
|
|
165
|
+
unit_label?: string,
|
|
166
|
+
quantity: number = 1,
|
|
167
|
+
bn: boolean = true
|
|
168
|
+
) => {
|
|
169
|
+
const unit = getPriceUintAmountByCurrency(price, currency);
|
|
170
|
+
const amount = bn
|
|
171
|
+
? fromUnitToToken(new BN(unit).mul(new BN(quantity)), currency.decimal).toString()
|
|
172
|
+
: +unit * quantity;
|
|
173
|
+
if (price?.type === 'recurring' && price.recurring) {
|
|
174
|
+
if (unit_label) {
|
|
175
|
+
return `${amount} ${currency.symbol} / ${unit_label}`;
|
|
176
|
+
}
|
|
177
|
+
if (price.recurring.usage_type === 'metered') {
|
|
178
|
+
return `${amount} ${currency.symbol} / unit`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `${amount} ${currency.symbol}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return `${amount} ${currency.symbol}`;
|
|
185
|
+
};
|
|
186
|
+
|
|
156
187
|
export function getPricingModel(price: TPrice) {
|
|
157
188
|
if (price.billing_scheme === 'tiered') {
|
|
158
189
|
return price.tiers_mode;
|
|
@@ -173,6 +204,11 @@ export function getProductByPriceId(products: TProductExpanded[], priceId: strin
|
|
|
173
204
|
return product;
|
|
174
205
|
}
|
|
175
206
|
|
|
207
|
+
export function getPriceFromProducts(products: TProductExpanded[], priceId: string) {
|
|
208
|
+
const product = products.find((x) => x.prices.some((p) => p.id === priceId));
|
|
209
|
+
return product?.prices.find((x) => x.id === priceId);
|
|
210
|
+
}
|
|
211
|
+
|
|
176
212
|
export function getStatementDescriptor(items: any[]) {
|
|
177
213
|
for (const item of items) {
|
|
178
214
|
if (item.price?.product?.statement_descriptor) {
|
|
@@ -582,3 +618,16 @@ export function stopEvent(e: React.SyntheticEvent<any>) {
|
|
|
582
618
|
// Do nothing
|
|
583
619
|
}
|
|
584
620
|
}
|
|
621
|
+
|
|
622
|
+
export function groupPricingTableItems(items: any[]) {
|
|
623
|
+
const grouped: { [key: string]: any[] } = {};
|
|
624
|
+
items.forEach((x: any, index) => {
|
|
625
|
+
x.index = index;
|
|
626
|
+
if (!grouped[x.product_id]) {
|
|
627
|
+
grouped[x.product_id] = [];
|
|
628
|
+
}
|
|
629
|
+
grouped[x.product_id]?.push(x);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
return Object.entries(grouped);
|
|
633
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -44,6 +44,9 @@ export default flat({
|
|
|
44
44
|
loadMore: 'View more {resource}',
|
|
45
45
|
loadingMore: 'Loading more {resource}...',
|
|
46
46
|
noMore: 'No more {resource}',
|
|
47
|
+
copied: 'Copied',
|
|
48
|
+
previous: 'Back',
|
|
49
|
+
continue: 'Continue',
|
|
47
50
|
metadata: {
|
|
48
51
|
label: 'Metadata',
|
|
49
52
|
add: 'Add more metadata',
|
|
@@ -173,6 +176,7 @@ export default flat({
|
|
|
173
176
|
info: 'Payment link information',
|
|
174
177
|
add: 'Create payment link',
|
|
175
178
|
save: 'Create link',
|
|
179
|
+
copyLink: 'Copy URL',
|
|
176
180
|
saved: 'Payment link successfully saved',
|
|
177
181
|
additional: 'Additional options',
|
|
178
182
|
beforePay: 'Payment page',
|
|
@@ -210,6 +214,28 @@ export default flat({
|
|
|
210
214
|
placeholder: 'Not consumer facing',
|
|
211
215
|
},
|
|
212
216
|
},
|
|
217
|
+
pricingTable: {
|
|
218
|
+
view: 'View pricing table',
|
|
219
|
+
add: 'Create pricing table',
|
|
220
|
+
save: 'Create',
|
|
221
|
+
copyLink: 'Copy URL',
|
|
222
|
+
saved: 'Pricing table successfully saved',
|
|
223
|
+
edit: 'Edit pricing table',
|
|
224
|
+
rename: 'Change name',
|
|
225
|
+
archive: 'Archive pricing table',
|
|
226
|
+
archiveTip:
|
|
227
|
+
'Archiving will hide this pricing table from new purchases. Are you sure you want to archive this pricing table?',
|
|
228
|
+
remove: 'Remove pricing table',
|
|
229
|
+
removeTip:
|
|
230
|
+
'Removing will hide this pricing table from new purchases. Are you sure you want to remove this pricing table?',
|
|
231
|
+
name: {
|
|
232
|
+
label: 'Name',
|
|
233
|
+
placeholder: 'Not consumer facing',
|
|
234
|
+
},
|
|
235
|
+
display: 'Display Settings',
|
|
236
|
+
highlight: 'Highlight product',
|
|
237
|
+
customer: 'Customer portal',
|
|
238
|
+
},
|
|
213
239
|
paymentIntent: {
|
|
214
240
|
name: 'Payment',
|
|
215
241
|
view: 'View payment detail',
|
|
@@ -396,6 +422,8 @@ export default flat({
|
|
|
396
422
|
method: 'Payment method',
|
|
397
423
|
processing: 'Processing',
|
|
398
424
|
payment: 'Pay',
|
|
425
|
+
try: 'Try for free',
|
|
426
|
+
include: 'This includes:',
|
|
399
427
|
subscription: 'Subscribe',
|
|
400
428
|
setup: 'Subscribe',
|
|
401
429
|
continue: 'Confirm {action}',
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import Button from '@arcblock/ux/lib/Button';
|
|
2
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
2
|
import Tabs from '@arcblock/ux/lib/Tabs';
|
|
4
|
-
import { AddOutlined } from '@mui/icons-material';
|
|
5
3
|
import { Stack, Typography } from '@mui/material';
|
|
6
4
|
import React, { isValidElement } from 'react';
|
|
7
5
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
@@ -9,6 +7,8 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|
|
9
7
|
const ProductCreate = React.lazy(() => import('./products/create'));
|
|
10
8
|
const ProductDetail = React.lazy(() => import('./products/detail'));
|
|
11
9
|
const PriceDetail = React.lazy(() => import('./prices/detail'));
|
|
10
|
+
const PricingTableCreate = React.lazy(() => import('./pricing-tables/create'));
|
|
11
|
+
const PricingTableDetail = React.lazy(() => import('./pricing-tables/detail'));
|
|
12
12
|
|
|
13
13
|
const pages = {
|
|
14
14
|
products: React.lazy(() => import('./products')),
|
|
@@ -29,6 +29,10 @@ export default function Products() {
|
|
|
29
29
|
return <PriceDetail id={page} />;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
if (page.startsWith('prctbl_')) {
|
|
33
|
+
return <PricingTableDetail id={page} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
// @ts-ignore
|
|
33
37
|
const TabComponent = pages[page] || pages.products;
|
|
34
38
|
const tabs = [
|
|
@@ -40,17 +44,8 @@ export default function Products() {
|
|
|
40
44
|
let extra = null;
|
|
41
45
|
if (page === 'products') {
|
|
42
46
|
extra = <ProductCreate />;
|
|
43
|
-
} else if (page === '
|
|
44
|
-
extra =
|
|
45
|
-
<Button
|
|
46
|
-
variant="contained"
|
|
47
|
-
size="small"
|
|
48
|
-
color="primary"
|
|
49
|
-
onClick={() => navigate('/admin/products/coupons/create')}>
|
|
50
|
-
<AddOutlined />
|
|
51
|
-
{t('admin.coupon.create')}
|
|
52
|
-
</Button>
|
|
53
|
-
);
|
|
47
|
+
} else if (page === 'pricing-tables') {
|
|
48
|
+
extra = <PricingTableCreate />;
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
return (
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* eslint-disable no-nested-ternary */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
+
import type { TPricingTable } from '@did-pay/types';
|
|
5
|
+
import { AddOutlined } from '@mui/icons-material';
|
|
6
|
+
import { Box, Button, Stack, Typography } from '@mui/material';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
9
|
+
import { dispatch } from 'use-bus';
|
|
10
|
+
|
|
11
|
+
import DrawerForm from '../../../../components/drawer-form';
|
|
12
|
+
import PricingTableCustomerSettings from '../../../../components/pricing-table/customer-settings';
|
|
13
|
+
import PricingTablePaymentSettings from '../../../../components/pricing-table/payment-settings';
|
|
14
|
+
import PricingTablePreview from '../../../../components/pricing-table/preview';
|
|
15
|
+
import PricingTableProductSettings from '../../../../components/pricing-table/product-settings';
|
|
16
|
+
import { ProductsProvider } from '../../../../contexts/products';
|
|
17
|
+
import { useSessionContext } from '../../../../contexts/session';
|
|
18
|
+
import api from '../../../../libs/api';
|
|
19
|
+
import { formatError } from '../../../../libs/util';
|
|
20
|
+
|
|
21
|
+
export default function CreatePricingTable() {
|
|
22
|
+
const { t } = useLocaleContext();
|
|
23
|
+
const { session } = useSessionContext();
|
|
24
|
+
const [step, setStep] = useState(0); // ['products', 'payment', 'portal']
|
|
25
|
+
const [stashed, setStashed] = useState(0);
|
|
26
|
+
|
|
27
|
+
const methods = useForm<TPricingTable & any>({
|
|
28
|
+
shouldUnregister: false,
|
|
29
|
+
defaultValues: {
|
|
30
|
+
name: '',
|
|
31
|
+
branding_settings: {
|
|
32
|
+
background_color: '#ffffff',
|
|
33
|
+
border_style: 'default',
|
|
34
|
+
button_color: '#0074d4',
|
|
35
|
+
font_family: 'default',
|
|
36
|
+
},
|
|
37
|
+
highlight: false,
|
|
38
|
+
highlight_product_id: '',
|
|
39
|
+
highlight_text: 'popular',
|
|
40
|
+
items: [],
|
|
41
|
+
metadata: [], // FIXME:
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const changes = methods.watch(['items', 'branding_settings']);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
api.post('/api/pricing-tables/stash', methods.getValues()).then(() => {
|
|
49
|
+
setStashed(stashed + 1);
|
|
50
|
+
});
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, [JSON.stringify(changes)]);
|
|
53
|
+
|
|
54
|
+
const onSubmit = (data: TPricingTable) => {
|
|
55
|
+
if (data.items.length === 0) {
|
|
56
|
+
Toast.error(t('admin.paymentLink.noProducts'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
api
|
|
61
|
+
.post('/api/pricing-tables', data)
|
|
62
|
+
.then(() => {
|
|
63
|
+
Toast.success(t('admin.pricingTable.saved'));
|
|
64
|
+
methods.reset();
|
|
65
|
+
dispatch('drawer.submitted');
|
|
66
|
+
dispatch('pricingTable.created');
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
console.error(err);
|
|
70
|
+
Toast.error(formatError(err));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onPrevious = () => {
|
|
75
|
+
if (step > 0) {
|
|
76
|
+
setStep(step - 1);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onContinue = () => {
|
|
81
|
+
if (step < 2) {
|
|
82
|
+
setStep(step + 1);
|
|
83
|
+
} else {
|
|
84
|
+
methods.handleSubmit(async (formData: any) => {
|
|
85
|
+
await onSubmit(formData);
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<DrawerForm
|
|
92
|
+
icon={<AddOutlined />}
|
|
93
|
+
text={t('admin.pricingTable.add')}
|
|
94
|
+
width={1280}
|
|
95
|
+
addons={
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
<Button variant="contained" size="small" onClick={methods.handleSubmit(onSubmit)}>
|
|
98
|
+
{t('admin.pricingTable.save')}
|
|
99
|
+
</Button>
|
|
100
|
+
}>
|
|
101
|
+
<FormProvider {...methods}>
|
|
102
|
+
<ProductsProvider>
|
|
103
|
+
<Stack height="92vh" spacing={2} direction="row">
|
|
104
|
+
<Box flex={1} sx={{ borderRight: '1px solid #eee' }} position="relative">
|
|
105
|
+
<Stack height="100%" spacing={2}>
|
|
106
|
+
<Box overflow="auto" sx={{ pr: 2 }}>
|
|
107
|
+
{step === 0 && <PricingTableProductSettings />}
|
|
108
|
+
{step === 1 && <PricingTablePaymentSettings />}
|
|
109
|
+
{step === 2 && <PricingTableCustomerSettings />}
|
|
110
|
+
</Box>
|
|
111
|
+
<Stack
|
|
112
|
+
padding={2}
|
|
113
|
+
spacing={2}
|
|
114
|
+
width="100%"
|
|
115
|
+
direction="row"
|
|
116
|
+
alignItems="center"
|
|
117
|
+
justifyContent="flex-end"
|
|
118
|
+
position="absolute"
|
|
119
|
+
sx={{ borderTop: '1px solid #eee', left: 0, bottom: 0 }}>
|
|
120
|
+
<Button variant="text" color="inherit" disabled={step === 0} onClick={onPrevious}>
|
|
121
|
+
{t('common.previous')}
|
|
122
|
+
</Button>
|
|
123
|
+
<Button variant="contained" color="primary" onClick={onContinue}>
|
|
124
|
+
{step === 2 ? t('common.save') : t('common.continue')}
|
|
125
|
+
</Button>
|
|
126
|
+
</Stack>
|
|
127
|
+
</Stack>
|
|
128
|
+
</Box>
|
|
129
|
+
<Box flex={2}>
|
|
130
|
+
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
|
|
131
|
+
{t('common.preview')}
|
|
132
|
+
</Typography>
|
|
133
|
+
{stashed && <PricingTablePreview id={`prctbl_${session.user.did}`} version={stashed} />}
|
|
134
|
+
</Box>
|
|
135
|
+
</Stack>
|
|
136
|
+
</ProductsProvider>
|
|
137
|
+
</FormProvider>
|
|
138
|
+
</DrawerForm>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
+
import type { TPricingTableExpanded, TProduct } from '@did-pay/types';
|
|
5
|
+
import { ArrowBackOutlined, Edit } from '@mui/icons-material';
|
|
6
|
+
import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
|
|
7
|
+
import { styled } from '@mui/system';
|
|
8
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
9
|
+
import { isEmpty } from 'lodash';
|
|
10
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
11
|
+
import { joinURL } from 'ufo';
|
|
12
|
+
|
|
13
|
+
import Copyable from '../../../../components/copyable';
|
|
14
|
+
import EventList from '../../../../components/event/list';
|
|
15
|
+
import InfoCard from '../../../../components/info-card';
|
|
16
|
+
import InfoRow from '../../../../components/info-row';
|
|
17
|
+
import MetadataEditor from '../../../../components/metadata/editor';
|
|
18
|
+
import PricingTableActions from '../../../../components/pricing-table/actions';
|
|
19
|
+
import PricingTablePreview from '../../../../components/pricing-table/preview';
|
|
20
|
+
import SectionHeader from '../../../../components/section/header';
|
|
21
|
+
import Table from '../../../../components/table';
|
|
22
|
+
import { useSettingsContext } from '../../../../contexts/settings';
|
|
23
|
+
import api from '../../../../libs/api';
|
|
24
|
+
import { formatError, formatProductPrice, formatTime } from '../../../../libs/util';
|
|
25
|
+
|
|
26
|
+
const fetchData = (id: string): Promise<TPricingTableExpanded> => {
|
|
27
|
+
return api.get(`/api/pricing-tables/${id}`).then((res) => res.data);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default function PricingTableDetail(props: { id: string }) {
|
|
31
|
+
const { t, locale } = useLocaleContext();
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const { settings } = useSettingsContext();
|
|
34
|
+
const [state, setState] = useSetState({
|
|
35
|
+
adding: {
|
|
36
|
+
price: false,
|
|
37
|
+
},
|
|
38
|
+
editing: {
|
|
39
|
+
metadata: false,
|
|
40
|
+
product: false,
|
|
41
|
+
},
|
|
42
|
+
loading: {
|
|
43
|
+
metadata: false,
|
|
44
|
+
price: false,
|
|
45
|
+
product: false,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
return <Alert severity="error">{error.message}</Alert>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (loading || !data) {
|
|
56
|
+
return <CircularProgress />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const createUpdater = (key: string) => async (updates: TProduct) => {
|
|
60
|
+
try {
|
|
61
|
+
setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
|
|
62
|
+
await api.put(`/api/payment-links/${props.id}`, updates).then((res) => res.data);
|
|
63
|
+
Toast.success(t('common.saved'));
|
|
64
|
+
runAsync();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(err);
|
|
67
|
+
Toast.error(formatError(err));
|
|
68
|
+
} finally {
|
|
69
|
+
setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onUpdateMetadata = createUpdater('metadata');
|
|
74
|
+
const onChange = (action: string) => {
|
|
75
|
+
if (action === 'remove') {
|
|
76
|
+
navigate('/admin/products/pricing-tables');
|
|
77
|
+
} else {
|
|
78
|
+
runAsync();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Grid container spacing={4} sx={{ mb: 4 }}>
|
|
84
|
+
<Grid item md={12}>
|
|
85
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
86
|
+
<Link to="/admin/products/pricing-tables">
|
|
87
|
+
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
|
|
88
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
89
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
90
|
+
{t('admin.pricingTables')}
|
|
91
|
+
</Typography>
|
|
92
|
+
</Stack>
|
|
93
|
+
</Link>
|
|
94
|
+
<Copyable text={props.id} style={{ marginLeft: 4 }} />
|
|
95
|
+
</Stack>
|
|
96
|
+
<Box mt={2}>
|
|
97
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
98
|
+
<Stack direction="column" alignItems="flex-start">
|
|
99
|
+
<Stack direction="row" alignItems="center" mb={1}>
|
|
100
|
+
<Typography variant="h5" sx={{ color: 'text.primary', fontWeight: 600 }}>
|
|
101
|
+
{data.name}
|
|
102
|
+
</Typography>
|
|
103
|
+
</Stack>
|
|
104
|
+
<Copyable
|
|
105
|
+
text={joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pricing-table/${data.id}`)}>
|
|
106
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', mr: 1 }}>
|
|
107
|
+
{joinURL(window.blocklet.prefix, `/checkout/pricing-table/${data.id}`)}
|
|
108
|
+
</Typography>
|
|
109
|
+
</Copyable>
|
|
110
|
+
</Stack>
|
|
111
|
+
<PricingTableActions data={data} onChange={onChange} variant="normal" />
|
|
112
|
+
</Stack>
|
|
113
|
+
</Box>
|
|
114
|
+
</Grid>
|
|
115
|
+
<Grid item xs={12} md={4}>
|
|
116
|
+
<Div direction="column" spacing={4}>
|
|
117
|
+
<Box className="section">
|
|
118
|
+
<SectionHeader title={t('admin.products')} />
|
|
119
|
+
<Box className="section-body">
|
|
120
|
+
<Table
|
|
121
|
+
className="link-products-table"
|
|
122
|
+
toolbar={false}
|
|
123
|
+
footer={false}
|
|
124
|
+
locale={locale}
|
|
125
|
+
data={data.items}
|
|
126
|
+
columns={[
|
|
127
|
+
{
|
|
128
|
+
label: t('common.name'),
|
|
129
|
+
name: 'product_id',
|
|
130
|
+
options: {
|
|
131
|
+
sort: false,
|
|
132
|
+
customBodyRenderLite: (_: any, index: number) => {
|
|
133
|
+
const item = data.items[index] as any;
|
|
134
|
+
return (
|
|
135
|
+
<Link to={`/admin/products/${item.product_id}`}>
|
|
136
|
+
<InfoCard
|
|
137
|
+
name={item.product.name}
|
|
138
|
+
description={formatProductPrice(
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
{ ...item.product, prices: [item.price] },
|
|
141
|
+
settings.baseCurrency
|
|
142
|
+
)}
|
|
143
|
+
logo={item.product.images[0]}
|
|
144
|
+
/>
|
|
145
|
+
</Link>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
label: t('common.actions'),
|
|
152
|
+
name: 'quantity',
|
|
153
|
+
},
|
|
154
|
+
]}
|
|
155
|
+
options={{
|
|
156
|
+
count: data.items.length,
|
|
157
|
+
page: 0,
|
|
158
|
+
rowsPerPage: 20,
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
</Box>
|
|
162
|
+
</Box>
|
|
163
|
+
<Box className="section">
|
|
164
|
+
<SectionHeader title={t('admin.details')}>
|
|
165
|
+
<Button
|
|
166
|
+
variant="outlined"
|
|
167
|
+
color="inherit"
|
|
168
|
+
size="small"
|
|
169
|
+
onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
|
|
170
|
+
<Edit fontSize="small" sx={{ mr: 0.5 }} />
|
|
171
|
+
{t('common.edit')}
|
|
172
|
+
</Button>
|
|
173
|
+
</SectionHeader>
|
|
174
|
+
<Stack>
|
|
175
|
+
<InfoRow sizes={[1, 1]} label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
176
|
+
<InfoRow sizes={[1, 1]} label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
|
|
177
|
+
</Stack>
|
|
178
|
+
</Box>
|
|
179
|
+
<Box className="section">
|
|
180
|
+
<SectionHeader title={t('common.metadata.label')}>
|
|
181
|
+
<Button
|
|
182
|
+
variant="outlined"
|
|
183
|
+
color="inherit"
|
|
184
|
+
size="small"
|
|
185
|
+
disabled={state.editing.metadata}
|
|
186
|
+
onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
|
|
187
|
+
<Edit fontSize="small" sx={{ mr: 0.5 }} />
|
|
188
|
+
{t('common.metadata.edit')}
|
|
189
|
+
</Button>
|
|
190
|
+
</SectionHeader>
|
|
191
|
+
<Box className="section-body">
|
|
192
|
+
{!state.editing.metadata &&
|
|
193
|
+
(isEmpty(data.metadata) ? (
|
|
194
|
+
<Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
|
|
195
|
+
) : (
|
|
196
|
+
<Grid container>
|
|
197
|
+
<Grid item xs={12} md={6}>
|
|
198
|
+
{Object.keys(data.metadata || {}).map((key) => (
|
|
199
|
+
// @ts-ignore
|
|
200
|
+
<InfoRow key={key} label={key} value={data.metadata[key]} />
|
|
201
|
+
))}
|
|
202
|
+
</Grid>
|
|
203
|
+
</Grid>
|
|
204
|
+
))}
|
|
205
|
+
{state.editing.metadata && (
|
|
206
|
+
<MetadataEditor
|
|
207
|
+
data={data}
|
|
208
|
+
loading={state.loading.metadata}
|
|
209
|
+
onSave={onUpdateMetadata}
|
|
210
|
+
onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
|
|
211
|
+
/>
|
|
212
|
+
)}
|
|
213
|
+
</Box>
|
|
214
|
+
</Box>
|
|
215
|
+
<Box className="section">
|
|
216
|
+
<SectionHeader title={t('admin.events')} />
|
|
217
|
+
<Box className="section-body">
|
|
218
|
+
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
219
|
+
</Box>
|
|
220
|
+
</Box>
|
|
221
|
+
</Div>
|
|
222
|
+
</Grid>
|
|
223
|
+
<Grid item xs={12} md={8}>
|
|
224
|
+
<Div>
|
|
225
|
+
<Box className="section">
|
|
226
|
+
<SectionHeader title={t('common.preview')} />
|
|
227
|
+
<Box className="section-body">
|
|
228
|
+
<PricingTablePreview id={data.id} />
|
|
229
|
+
</Box>
|
|
230
|
+
</Box>
|
|
231
|
+
</Div>
|
|
232
|
+
</Grid>
|
|
233
|
+
</Grid>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const Div = styled(Stack)``;
|