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.
Files changed (48) hide show
  1. package/README.md +4 -0
  2. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  3. package/api/src/integrations/stripe/handlers/payment-intent.ts +2 -2
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +1 -1
  5. package/api/src/integrations/stripe/handlers/subscription.ts +2 -2
  6. package/api/src/jobs/event.ts +10 -4
  7. package/api/src/jobs/webhook.ts +17 -8
  8. package/api/src/libs/audit.ts +3 -3
  9. package/api/src/libs/event.ts +3 -0
  10. package/api/src/libs/util.ts +5 -0
  11. package/api/src/routes/checkout-sessions.ts +3 -3
  12. package/api/src/routes/connect/pay.ts +1 -1
  13. package/api/src/routes/index.ts +2 -0
  14. package/api/src/routes/payment-links.ts +0 -1
  15. package/api/src/routes/pricing-table.ts +342 -0
  16. package/api/src/routes/subscriptions.ts +15 -0
  17. package/api/src/store/migrations/20231017-pricing-table.ts +10 -0
  18. package/api/src/store/models/index.ts +14 -1
  19. package/api/src/store/models/pricing-table.ts +107 -0
  20. package/api/src/store/models/types.ts +53 -0
  21. package/blocklet.yml +2 -2
  22. package/package.json +4 -3
  23. package/src/app.tsx +1 -1
  24. package/src/components/blockchain/tx.tsx +8 -0
  25. package/src/components/payment-link/actions.tsx +20 -9
  26. package/src/components/payment-link/chrome.tsx +5 -3
  27. package/src/components/payment-link/preview.tsx +8 -5
  28. package/src/components/payment-link/rename.tsx +3 -3
  29. package/src/components/price/form.tsx +4 -1
  30. package/src/components/pricing-table/actions.tsx +126 -0
  31. package/src/components/pricing-table/customer-settings.tsx +17 -0
  32. package/src/components/pricing-table/payment-settings.tsx +179 -0
  33. package/src/components/pricing-table/preview.tsx +34 -0
  34. package/src/components/pricing-table/price-item.tsx +64 -0
  35. package/src/components/pricing-table/product-item.tsx +86 -0
  36. package/src/components/pricing-table/product-settings.tsx +195 -0
  37. package/src/components/pricing-table/rename.tsx +67 -0
  38. package/src/libs/util.ts +54 -5
  39. package/src/locales/en.tsx +28 -0
  40. package/src/pages/admin/payments/links/create.tsx +1 -1
  41. package/src/pages/admin/products/index.tsx +8 -13
  42. package/src/pages/admin/products/pricing-tables/create.tsx +140 -0
  43. package/src/pages/admin/products/pricing-tables/detail.tsx +237 -0
  44. package/src/pages/admin/products/pricing-tables/index.tsx +154 -0
  45. package/src/pages/admin/products/products/create.tsx +8 -4
  46. package/src/pages/checkout/index.tsx +2 -1
  47. package/src/pages/checkout/pricing-table.tsx +195 -0
  48. 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 = (price: TPrice, currency: TPaymentCurrency, unit_label?: string, quantity: number = 1) => {
136
- const amount = fromUnitToToken(
137
- new BN(getPriceUintAmountByCurrency(price, currency)).mul(new BN(quantity)),
138
- currency.decimal
139
- ).toString();
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
+ }
@@ -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}',
@@ -65,7 +65,7 @@ export default function CreatePaymentLink() {
65
65
  },
66
66
  metadata: [], // FIXME:
67
67
  custom_fields: [], // FIXME:
68
- submit_type: 'pay', // FIXME:
68
+ submit_type: 'auto', // FIXME:
69
69
  },
70
70
  });
71
71
 
@@ -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 === 'coupons') {
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)``;