payment-kit 1.13.24 → 1.13.26
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/routes/checkout-sessions.ts +3 -3
- 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/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 +1 -1
- package/package.json +5 -5
- package/src/app.tsx +1 -1
- package/src/components/payment-link/actions.tsx +1 -1
- 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/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 +43 -0
- package/src/locales/en.tsx +26 -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/checkout/index.tsx +2 -1
- package/src/pages/checkout/pricing-table.tsx +195 -0
- package/src/pages/admin/products/pricing-tables.tsx +0 -3
|
@@ -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
|
+
}
|
package/src/libs/util.ts
CHANGED
|
@@ -159,6 +159,31 @@ export const formatPrice = (
|
|
|
159
159
|
return `${amount} ${currency.symbol}`;
|
|
160
160
|
};
|
|
161
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
|
+
|
|
162
187
|
export function getPricingModel(price: TPrice) {
|
|
163
188
|
if (price.billing_scheme === 'tiered') {
|
|
164
189
|
return price.tiers_mode;
|
|
@@ -179,6 +204,11 @@ export function getProductByPriceId(products: TProductExpanded[], priceId: strin
|
|
|
179
204
|
return product;
|
|
180
205
|
}
|
|
181
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
|
+
|
|
182
212
|
export function getStatementDescriptor(items: any[]) {
|
|
183
213
|
for (const item of items) {
|
|
184
214
|
if (item.price?.product?.statement_descriptor) {
|
|
@@ -588,3 +618,16 @@ export function stopEvent(e: React.SyntheticEvent<any>) {
|
|
|
588
618
|
// Do nothing
|
|
589
619
|
}
|
|
590
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
|
@@ -45,6 +45,8 @@ export default flat({
|
|
|
45
45
|
loadingMore: 'Loading more {resource}...',
|
|
46
46
|
noMore: 'No more {resource}',
|
|
47
47
|
copied: 'Copied',
|
|
48
|
+
previous: 'Back',
|
|
49
|
+
continue: 'Continue',
|
|
48
50
|
metadata: {
|
|
49
51
|
label: 'Metadata',
|
|
50
52
|
add: 'Add more metadata',
|
|
@@ -212,6 +214,28 @@ export default flat({
|
|
|
212
214
|
placeholder: 'Not consumer facing',
|
|
213
215
|
},
|
|
214
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
|
+
},
|
|
215
239
|
paymentIntent: {
|
|
216
240
|
name: 'Payment',
|
|
217
241
|
view: 'View payment detail',
|
|
@@ -398,6 +422,8 @@ export default flat({
|
|
|
398
422
|
method: 'Payment method',
|
|
399
423
|
processing: 'Processing',
|
|
400
424
|
payment: 'Pay',
|
|
425
|
+
try: 'Try for free',
|
|
426
|
+
include: 'This includes:',
|
|
401
427
|
subscription: 'Subscribe',
|
|
402
428
|
setup: 'Subscribe',
|
|
403
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
|
+
}
|