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
@@ -0,0 +1,179 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Tabs from '@arcblock/ux/lib/Tabs';
3
+ import type { TPrice, TProduct } from '@did-pay/types';
4
+ import { Box, Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
5
+ import get from 'lodash/get';
6
+ import { useState } from 'react';
7
+ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
8
+
9
+ import { useProductsContext } from '../../contexts/products';
10
+ import { useSettingsContext } from '../../contexts/settings';
11
+ import { formatPrice, getPriceFromProducts, groupPricingTableItems } from '../../libs/util';
12
+ import IconCollapse from '../collapse';
13
+
14
+ export function PricePaymentSettings({ index }: { index: number }) {
15
+ const getFieldName = (name: string) => `items.${index}.${name}`;
16
+
17
+ const { t } = useLocaleContext();
18
+ const { control, setValue, getValues } = useFormContext();
19
+ const type = useWatch({ control, name: getFieldName('after_completion.type') });
20
+
21
+ const values = getValues();
22
+
23
+ return (
24
+ <Stack spacing={2} mb={2}>
25
+ <Controller
26
+ name={getFieldName('billing_address_collection')}
27
+ control={control}
28
+ render={({ field }) => (
29
+ <FormControlLabel
30
+ control={
31
+ <Checkbox
32
+ checked={get(values, getFieldName('billing_address_collection')) === 'required'}
33
+ {...field}
34
+ onChange={(_, checked) => setValue(field.name, checked ? 'required' : 'auto')}
35
+ />
36
+ }
37
+ label={t('admin.paymentLink.requireBillingAddress')}
38
+ />
39
+ )}
40
+ />
41
+ <Controller
42
+ name={getFieldName('phone_number_collection.enabled')}
43
+ control={control}
44
+ render={({ field }) => (
45
+ <FormControlLabel
46
+ control={
47
+ <Checkbox
48
+ checked={get(values, getFieldName('phone_number_collection.enabled'))}
49
+ {...field}
50
+ onChange={(_, checked) => setValue(field.name, checked)}
51
+ />
52
+ }
53
+ label={t('admin.paymentLink.requirePhoneNumber')}
54
+ />
55
+ )}
56
+ />
57
+ <Controller
58
+ name={getFieldName('allow_promotion_codes')}
59
+ control={control}
60
+ render={({ field }) => (
61
+ <FormControlLabel
62
+ control={
63
+ <Checkbox
64
+ checked={get(values, getFieldName('allow_promotion_codes'))}
65
+ {...field}
66
+ onChange={(_, checked) => setValue(field.name, checked)}
67
+ />
68
+ }
69
+ label={t('admin.paymentLink.allowPromotionCodes')}
70
+ />
71
+ )}
72
+ />
73
+ <Controller
74
+ name={getFieldName('after_completion.type')}
75
+ control={control}
76
+ render={({ field }) => (
77
+ <FormControlLabel
78
+ control={
79
+ <Checkbox
80
+ checked={get(values, getFieldName('after_completion.type')) === 'hosted_confirmation'}
81
+ {...field}
82
+ onChange={(_, checked) => setValue(field.name, checked ? 'hosted_confirmation' : 'redirect')}
83
+ />
84
+ }
85
+ label={t('admin.paymentLink.showConfirmPage')}
86
+ />
87
+ )}
88
+ />
89
+ {type === 'hosted_confirmation' && (
90
+ <Controller
91
+ name={getFieldName('after_completion.hosted_confirmation.custom_message')}
92
+ control={control}
93
+ render={({ field }) => (
94
+ <TextField {...field} placeholder="Replace default success message" fullWidth size="small" />
95
+ )}
96
+ />
97
+ )}
98
+ <Controller
99
+ name={getFieldName('after_completion.type')}
100
+ control={control}
101
+ render={({ field }) => (
102
+ <FormControlLabel
103
+ control={
104
+ <Checkbox
105
+ checked={get(values, getFieldName('after_completion.type')) === 'redirect'}
106
+ {...field}
107
+ onChange={(_, checked) => setValue(field.name, checked ? 'redirect' : 'hosted_confirmation')}
108
+ />
109
+ }
110
+ label={t('admin.paymentLink.noConfirmPage')}
111
+ />
112
+ )}
113
+ />
114
+ {type === 'redirect' && (
115
+ <Controller
116
+ name={getFieldName('after_completion.redirect.url')}
117
+ control={control}
118
+ render={({ field }) => (
119
+ <TextField placeholder="Redirect customers to your site" {...field} fullWidth size="small" />
120
+ )}
121
+ />
122
+ )}
123
+ </Stack>
124
+ );
125
+ }
126
+
127
+ export function ProductPaymentSettings({ product, prices }: { product: TProduct; prices: TPrice[] }) {
128
+ const { settings } = useSettingsContext();
129
+ const { products } = useProductsContext();
130
+ const [current, setCurrent] = useState(prices[0]?.id);
131
+ const tabs = prices.map((x: any) => ({
132
+ value: x.id,
133
+ label: formatPrice(getPriceFromProducts(products, x.price_id) as TPrice, settings.baseCurrency),
134
+ component: <PricePaymentSettings index={x.index} />,
135
+ }));
136
+
137
+ return (
138
+ <Box sx={{ px: 2, py: 0, border: '1px solid #eee', borderRadius: 2 }}>
139
+ <IconCollapse
140
+ key={product.id}
141
+ expanded
142
+ trigger={<Typography variant="h6">{product.name}</Typography>}
143
+ style={{ py: 1 }}>
144
+ <Tabs
145
+ tabs={tabs}
146
+ current={current}
147
+ onChange={(v: string) => setCurrent(v)}
148
+ style={{ width: '100%' }}
149
+ scrollButtons="auto"
150
+ />
151
+ {tabs.find((x) => x.value === current)?.component}
152
+ </IconCollapse>
153
+ </Box>
154
+ );
155
+ }
156
+
157
+ export default function PricingTablePaymentSettings() {
158
+ const { t } = useLocaleContext();
159
+ const { products } = useProductsContext();
160
+ const { control } = useFormContext();
161
+ const items = useFieldArray({ control, name: 'items' });
162
+
163
+ const grouped = groupPricingTableItems(items.fields);
164
+
165
+ return (
166
+ <Stack spacing={2} alignItems="flex-start">
167
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
168
+ {t('admin.paymentLink.products')}
169
+ </Typography>
170
+ <Stack spacing={2} sx={{ width: '100%' }}>
171
+ {grouped.map((item) => {
172
+ const [productId, prices] = item;
173
+ const product = products.find((x) => x.id === productId);
174
+ return product ? <ProductPaymentSettings key={productId} product={product} prices={prices} /> : null;
175
+ })}
176
+ </Stack>
177
+ </Stack>
178
+ );
179
+ }
@@ -0,0 +1,34 @@
1
+ import { Box } from '@mui/material';
2
+ import { useSize } from 'ahooks';
3
+ import IframeResizer from 'iframe-resizer-react';
4
+ import { useRef } from 'react';
5
+
6
+ import Chrome from '../payment-link/chrome';
7
+
8
+ PricingTablePreview.defaultProps = {
9
+ version: 1,
10
+ };
11
+
12
+ export default function PricingTablePreview({ id, version }: { id: string; version?: number }) {
13
+ const ref = useRef(null);
14
+ const size = useSize(ref);
15
+ return (
16
+ <Chrome>
17
+ <Box ref={ref} sx={{ width: '100%' }}>
18
+ &nbsp;
19
+ </Box>
20
+ <IframeResizer
21
+ style={{
22
+ // @ts-ignore
23
+ // eslint-disable-next-line no-unsafe-optional-chaining
24
+ minWidth: size?.width / 0.8,
25
+ minHeight: '64vh',
26
+ transform: 'scale(0.8)',
27
+ transformOrigin: 'top left',
28
+ border: 'none',
29
+ }}
30
+ src={`${window.blocklet.prefix}checkout/pricing-table/${id}?preview=1&version=${version}`}
31
+ />
32
+ </Chrome>
33
+ );
34
+ }
@@ -0,0 +1,64 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type { TPrice } from '@did-pay/types';
3
+ import { DeleteOutlineOutlined } from '@mui/icons-material';
4
+ import { Box, Checkbox, FormControlLabel, IconButton, InputAdornment, Stack, Typography } from '@mui/material';
5
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
6
+
7
+ import { useSettingsContext } from '../../contexts/settings';
8
+ import { formatPrice } from '../../libs/util';
9
+ import FormInput from '../input';
10
+
11
+ type Props = {
12
+ prefix: string;
13
+ price: TPrice;
14
+ onRemove: () => void;
15
+ };
16
+
17
+ export default function PriceItem({ prefix, price, onRemove }: Props) {
18
+ const { t } = useLocaleContext();
19
+ const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
20
+ const { settings } = useSettingsContext();
21
+ const { control, setValue } = useFormContext();
22
+ const includeFreeTrail = useWatch({ control, name: getFieldName('include_free_trial') });
23
+
24
+ return (
25
+ <Box sx={{ width: '100%' }}>
26
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
27
+ <Typography>{formatPrice(price, settings.baseCurrency)}</Typography>
28
+ <IconButton size="small" onClick={onRemove}>
29
+ <DeleteOutlineOutlined color="inherit" sx={{ opacity: 0.75 }} />
30
+ </IconButton>
31
+ </Stack>
32
+ <Controller
33
+ name={getFieldName('include_free_trial')}
34
+ control={control}
35
+ render={({ field }) => (
36
+ <FormControlLabel
37
+ label={t('admin.paymentLink.includeFreeTrail')}
38
+ control={
39
+ <Checkbox
40
+ sx={{ ml: 1 }}
41
+ {...field}
42
+ onChange={(_, checked) => {
43
+ setValue(field.name, checked);
44
+ }}
45
+ />
46
+ }
47
+ />
48
+ )}
49
+ />
50
+ {includeFreeTrail && (
51
+ <FormInput
52
+ name={getFieldName('subscription_data.trial_period_days')}
53
+ rules={{ required: t('checkout.required') }}
54
+ errorPosition="right"
55
+ sx={{ mt: 0.5 }}
56
+ fullWidth={false}
57
+ InputProps={{
58
+ endAdornment: <InputAdornment position="end">days</InputAdornment>,
59
+ }}
60
+ />
61
+ )}
62
+ </Box>
63
+ );
64
+ }
@@ -0,0 +1,86 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { PricingTableItem, TProduct, TProductExpanded } from '@did-pay/types';
4
+ import { Box, Stack } from '@mui/material';
5
+ import { useSetState } from 'ahooks';
6
+
7
+ import { useProductsContext } from '../../contexts/products';
8
+ import api from '../../libs/api';
9
+ import { formatError, getPriceFromProducts } from '../../libs/util';
10
+ import Actions from '../actions';
11
+ import ClickBoundary from '../click-boundary';
12
+ import InfoCard from '../info-card';
13
+ import EditProduct from '../product/edit';
14
+ import PriceItem from './price-item';
15
+
16
+ type Props = {
17
+ product: TProductExpanded;
18
+ prices: PricingTableItem[];
19
+ valid: boolean;
20
+ onUpdate: () => void;
21
+ onRemove: (i: number) => void;
22
+ };
23
+
24
+ export default function ProductItem({ product, prices, valid, onUpdate, onRemove }: Props) {
25
+ const { t } = useLocaleContext();
26
+ const { products } = useProductsContext();
27
+ const [state, setState] = useSetState({ editing: false, loading: false });
28
+
29
+ const onSave = async (updates: TProduct) => {
30
+ try {
31
+ setState({ loading: true });
32
+ await api.put(`/api/products/${product.id}`, updates).then((res) => res.data);
33
+ Toast.success(t('common.saved'));
34
+ onUpdate();
35
+ } catch (err) {
36
+ console.error(err);
37
+ Toast.error(formatError(err));
38
+ } finally {
39
+ setState({ loading: false });
40
+ }
41
+ };
42
+
43
+ return (
44
+ <Box
45
+ sx={{
46
+ p: 2,
47
+ borderWidth: valid ? 1 : 2,
48
+ borderStyle: 'solid',
49
+ borderColor: valid ? '#eee' : 'error.main',
50
+ borderRadius: 2,
51
+ position: 'relative',
52
+ }}>
53
+ <ClickBoundary>
54
+ <Actions
55
+ sx={{ position: 'absolute', top: 8, right: 16 }}
56
+ actions={[
57
+ {
58
+ label: t('admin.product.edit'),
59
+ handler: () => setState({ editing: true }),
60
+ color: 'primary',
61
+ },
62
+ ]}
63
+ />
64
+ </ClickBoundary>
65
+ <InfoCard logo={product.images[0]} name={product.name} description={product.description} />
66
+ <Stack direction="column" spacing={2} alignItems="flex-start">
67
+ {prices.map((x: any) => (
68
+ <PriceItem
69
+ key={x.index}
70
+ prefix={`items.${x.index}`}
71
+ price={getPriceFromProducts(products, x.price_id) as any}
72
+ onRemove={() => onRemove(x.index)}
73
+ />
74
+ ))}
75
+ </Stack>
76
+ {state.editing && (
77
+ <EditProduct
78
+ product={product}
79
+ loading={state.loading}
80
+ onSave={onSave}
81
+ onCancel={() => setState({ editing: false })}
82
+ />
83
+ )}
84
+ </Box>
85
+ );
86
+ }
@@ -0,0 +1,195 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Checkbox, FormControlLabel, MenuItem, Select, Stack, Typography } from '@mui/material';
3
+ import { useSetState } from 'ahooks';
4
+ import { useEffect } from 'react';
5
+ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
6
+
7
+ import { useProductsContext } from '../../contexts/products';
8
+ import { getProductByPriceId, groupPricingTableItems, isPriceCurrencyAligned } from '../../libs/util';
9
+ import ProductSelect from '../payment-link/product-select';
10
+ import CreateProduct from '../product/create';
11
+ import ProductItem from './product-item';
12
+
13
+ export default function PricingTableProductSettings() {
14
+ const { t } = useLocaleContext();
15
+ const { products, refresh } = useProductsContext();
16
+ const { control, setValue, getValues } = useFormContext();
17
+ const items = useFieldArray({ control, name: 'items' });
18
+ const [state, setState] = useSetState({ creating: false });
19
+ const highlight = useWatch({ control, name: 'highlight' });
20
+
21
+ useEffect(() => {
22
+ if (items.fields.length) {
23
+ const selected: any[] = items.fields.map((x: any) => getProductByPriceId(products, x.price_id));
24
+ const name = selected.length > 1 ? `${selected[0].name} and ${selected.length - 1} more` : selected[0].name;
25
+ setValue('name', name);
26
+ } else {
27
+ setValue('name', '');
28
+ }
29
+ }, [items.fields, setValue, products]);
30
+
31
+ const onProductSelected = (priceId: string) => {
32
+ if (priceId === 'add') {
33
+ setState({ creating: true });
34
+ } else if (priceId) {
35
+ const product = getProductByPriceId(products, priceId);
36
+ if (product) {
37
+ items.append({
38
+ price_id: priceId,
39
+ product_id: product.id,
40
+ is_highlight: false,
41
+ highlight_text: 'popular',
42
+ adjustable_quantity: {
43
+ enabled: false,
44
+ maximum: 1,
45
+ minimum: 0,
46
+ },
47
+ after_completion: {
48
+ type: 'hosted_confirmation',
49
+ hosted_confirmation: {
50
+ custom_message: '',
51
+ },
52
+ redirect: {
53
+ url: '',
54
+ },
55
+ },
56
+ allow_promotion_codes: false,
57
+ customer_creation: 'always',
58
+ consent_collection: {
59
+ promotions: 'none',
60
+ terms_of_service: 'none',
61
+ },
62
+ phone_number_collection: {
63
+ enabled: false,
64
+ },
65
+ billing_address_collection: 'auto',
66
+ include_free_trial: false,
67
+ subscription_data: {
68
+ description: '',
69
+ trial_period_days: 0,
70
+ },
71
+ custom_fields: [],
72
+ submit_type: 'auto',
73
+ });
74
+ }
75
+ }
76
+ };
77
+
78
+ const onProductCreated = () => {
79
+ setState({ creating: false });
80
+ refresh();
81
+ };
82
+
83
+ const grouped = groupPricingTableItems(items.fields);
84
+
85
+ return (
86
+ <Stack spacing={2} alignItems="flex-start">
87
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
88
+ {t('admin.paymentLink.products')}
89
+ </Typography>
90
+ <Stack spacing={2} sx={{ width: '100%' }}>
91
+ {grouped.map((item) => {
92
+ const [productId, prices] = item;
93
+ // @ts-ignore
94
+ const product = products.find((x) => x.id === productId);
95
+ if (!product) {
96
+ return null;
97
+ }
98
+
99
+ return (
100
+ <ProductItem
101
+ key={productId}
102
+ // @ts-ignore
103
+ valid={prices.every((x) => isPriceCurrencyAligned(items.fields, products, x.index))}
104
+ product={product}
105
+ prices={prices}
106
+ onUpdate={refresh}
107
+ onRemove={(i: number) => items.remove(i)}
108
+ />
109
+ );
110
+ })}
111
+ {items.fields.some((_, index) => !isPriceCurrencyAligned(items.fields as any[], products, index)) && (
112
+ <Typography color="error" fontSize="small">
113
+ {t('admin.paymentLink.currencyNotAligned')}
114
+ </Typography>
115
+ )}
116
+ <ProductSelect
117
+ mode={items.fields.length ? 'waiting' : 'selecting'}
118
+ onSelect={onProductSelected}
119
+ hasSelected={(price) => price.type !== 'recurring' || items.fields.some((x: any) => x.price_id === price.id)}
120
+ />
121
+ {state.creating && <CreateProduct onCancel={() => setState({ creating: false })} onSave={onProductCreated} />}
122
+ </Stack>
123
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
124
+ {t('admin.pricingTable.display')}
125
+ </Typography>
126
+ {grouped.length > 0 && (
127
+ <Stack direction="column" spacing={2}>
128
+ <Controller
129
+ name="highlight"
130
+ control={control}
131
+ render={({ field }) => (
132
+ <FormControlLabel
133
+ label={t('admin.pricingTable.highlight')}
134
+ control={
135
+ <Checkbox
136
+ checked={getValues().highlight}
137
+ {...field}
138
+ onChange={(_, checked) => {
139
+ setValue(field.name, checked);
140
+ if (checked && !getValues().highlight_product_id && grouped[0]) {
141
+ setValue('highlight_product_id', grouped[0][0]);
142
+ }
143
+ }}
144
+ />
145
+ }
146
+ />
147
+ )}
148
+ />
149
+ {highlight && (
150
+ <Stack direction="row" alignItems="center" spacing={0.5}>
151
+ <Controller
152
+ name="highlight_product_id"
153
+ control={control}
154
+ render={({ field }) => (
155
+ <Select {...field} size="small">
156
+ {grouped.map(([productId]) => {
157
+ const product = products.find((x) => x.id === productId);
158
+ if (!product) {
159
+ return null;
160
+ }
161
+
162
+ return (
163
+ <MenuItem key={productId} value={productId}>
164
+ {product.name}
165
+ </MenuItem>
166
+ );
167
+ })}
168
+ </Select>
169
+ )}
170
+ />
171
+ <Typography>as</Typography>
172
+ <Controller
173
+ name="highlight_text"
174
+ control={control}
175
+ render={({ field }) => (
176
+ <Select {...field} size="small">
177
+ <MenuItem key="deal" value="deal">
178
+ Best deal
179
+ </MenuItem>
180
+ <MenuItem key="popular" value="popular">
181
+ Most popular
182
+ </MenuItem>
183
+ <MenuItem key="recommended" value="recommended">
184
+ Recommended
185
+ </MenuItem>
186
+ </Select>
187
+ )}
188
+ />
189
+ </Stack>
190
+ )}
191
+ </Stack>
192
+ )}
193
+ </Stack>
194
+ );
195
+ }
@@ -0,0 +1,67 @@
1
+ import Dialog from '@arcblock/ux/lib/Dialog';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import type { TPricingTable } from '@did-pay/types';
4
+ import { Button, CircularProgress, Stack } from '@mui/material';
5
+ import type { EventHandler } from 'react';
6
+ import { FormProvider, useForm } from 'react-hook-form';
7
+
8
+ import TextInput from '../input';
9
+
10
+ export default function RenamePricingTable({
11
+ data,
12
+ loading,
13
+ onSave,
14
+ onCancel,
15
+ }: {
16
+ data: TPricingTable;
17
+ loading: boolean;
18
+ onSave: EventHandler<any>;
19
+ onCancel: EventHandler<any>;
20
+ }) {
21
+ const { t } = useLocaleContext();
22
+ const methods = useForm<TPricingTable>({
23
+ defaultValues: {
24
+ name: data.name,
25
+ },
26
+ });
27
+
28
+ const { handleSubmit, reset } = methods;
29
+ const onSubmit = () => {
30
+ handleSubmit(async (formData: any) => {
31
+ await onSave(formData);
32
+ reset();
33
+ onCancel(null);
34
+ })();
35
+ };
36
+
37
+ return (
38
+ <Dialog
39
+ open
40
+ disableEscapeKeyDown
41
+ fullWidth
42
+ maxWidth="sm"
43
+ onClose={() => onCancel(null)}
44
+ showCloseButton={false}
45
+ title={t('admin.product.edit')}
46
+ actions={
47
+ <Stack direction="row">
48
+ <Button size="small" sx={{ mr: 2 }} onClick={onCancel}>
49
+ {t('common.cancel')}
50
+ </Button>
51
+ <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
52
+ {loading && <CircularProgress size="small" />} {t('common.save')}
53
+ </Button>
54
+ </Stack>
55
+ }>
56
+ <FormProvider {...methods}>
57
+ <TextInput
58
+ name="name"
59
+ rules={{ required: true }}
60
+ label={t('admin.paymentLink.name.label')}
61
+ placeholder={t('admin.paymentLink.name.placeholder')}
62
+ autoFocus
63
+ />
64
+ </FormProvider>
65
+ </Dialog>
66
+ );
67
+ }