payment-kit 1.18.3 → 1.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/api/src/integrations/stripe/setup.ts +27 -0
  2. package/api/src/routes/payment-methods.ts +28 -2
  3. package/api/src/routes/settings.ts +251 -2
  4. package/api/src/store/migrations/20250211-setting.ts +10 -0
  5. package/api/src/store/migrations/20250214-setting-component.ts +22 -0
  6. package/api/src/store/models/index.ts +6 -1
  7. package/api/src/store/models/payment-method.ts +12 -6
  8. package/api/src/store/models/setting.ts +84 -0
  9. package/api/src/store/models/types.ts +2 -0
  10. package/blocklet.yml +13 -1
  11. package/package.json +8 -8
  12. package/src/app.tsx +6 -1
  13. package/src/components/invoice/list.tsx +1 -3
  14. package/src/components/payment-intent/list.tsx +5 -6
  15. package/src/components/payouts/list.tsx +3 -3
  16. package/src/components/refund/list.tsx +3 -6
  17. package/src/components/subscription/list.tsx +1 -2
  18. package/src/locales/en.tsx +100 -1
  19. package/src/locales/zh.tsx +96 -0
  20. package/src/pages/admin/customers/customers/index.tsx +27 -16
  21. package/src/pages/admin/developers/webhooks/index.tsx +14 -12
  22. package/src/pages/admin/products/links/index.tsx +22 -15
  23. package/src/pages/admin/products/pricing-tables/index.tsx +16 -14
  24. package/src/pages/admin/products/products/index.tsx +19 -15
  25. package/src/pages/admin/settings/payment-methods/edit.tsx +3 -0
  26. package/src/pages/home.tsx +1 -1
  27. package/src/pages/integrations/donations/edit-form.tsx +349 -0
  28. package/src/pages/integrations/donations/index.tsx +360 -0
  29. package/src/pages/integrations/donations/preview.tsx +229 -0
  30. package/src/pages/integrations/index.tsx +80 -0
  31. package/src/pages/integrations/overview.tsx +121 -0
@@ -0,0 +1,360 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { Toast } from '@arcblock/ux';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { api, formatError, Table, ConfirmDialog, Switch, clearDonateCache, getPrefix } from '@blocklet/payment-react';
5
+ import { Alert, Typography, Button, CircularProgress, Stack, Avatar, Box, IconButton, Tooltip } from '@mui/material';
6
+ import { ContentCopy } from '@mui/icons-material';
7
+ import { useLocalStorageState, useRequest } from 'ahooks';
8
+ import Dialog from '@arcblock/ux/lib/Dialog';
9
+ import { useState, useEffect } from 'react';
10
+ import { joinURL } from 'ufo';
11
+ import { getDurableData } from '@arcblock/ux/lib/Datatable';
12
+ import useBus from 'use-bus';
13
+ import EditDonationForm from './edit-form';
14
+
15
+ const exampleCode = `import { PaymentProvider, DonateProvider, CheckoutDonate } from '@blocklet/payment-react';
16
+
17
+ // In your component
18
+ function YourComponent() {
19
+ return (
20
+ <PaymentProvider session={session} connect={connectApi}>
21
+ <DonateProvider
22
+ mountLocation="your-unique-donate-instance"
23
+ description="Help locate this donation instance"
24
+ >
25
+ <CheckoutDonate
26
+ settings={{
27
+ target: "post-123", // required, unique identifier for the donation instance
28
+ title: "Support Author", // required, title of the donation modal
29
+ description: "If you find this article helpful, feel free to buy me a coffee", // required, description of the donation
30
+ reference: "https://your-site.com/posts/123", // required, reference link of the donation
31
+ beneficiaries: [
32
+ {
33
+ address: "tip user did", // required, address of the beneficiary
34
+ share: "100", // required, percentage share
35
+ },
36
+ ],
37
+ }}
38
+ />
39
+ </DonateProvider>
40
+ </PaymentProvider>
41
+ );
42
+ }`;
43
+
44
+ const fetchData = (params: Record<string, any> = {}): Promise<any> => {
45
+ const search = new URLSearchParams();
46
+ Object.keys(params).forEach((key) => {
47
+ let v = params[key];
48
+ if (key === 'q') {
49
+ v = Object.entries(v)
50
+ .map((x) => x.join(':'))
51
+ .join(' ');
52
+ }
53
+ search.set(key, String(v));
54
+ });
55
+ return api.get(`/api/settings/donate?${search.toString()}`).then((res) => res.data);
56
+ };
57
+
58
+ function CodeExample({ code }: { code: string }) {
59
+ const { t } = useLocaleContext();
60
+
61
+ const handleCopy = async () => {
62
+ try {
63
+ await navigator.clipboard.writeText(code);
64
+ Toast.success(t('common.copySuccess'));
65
+ } catch (err) {
66
+ Toast.error(t('common.copyFailed'));
67
+ }
68
+ };
69
+
70
+ return (
71
+ <Box sx={{ position: 'relative' }}>
72
+ <Box
73
+ component="pre"
74
+ sx={{
75
+ backgroundColor: '#F8F9FA', // 更浅的背景色
76
+ color: '#495057', // 更柔和的文字颜色
77
+ borderRadius: 1,
78
+ p: 2,
79
+ overflow: 'auto',
80
+ '&::-webkit-scrollbar': {
81
+ height: 8,
82
+ bgcolor: 'background.paper',
83
+ },
84
+ '&::-webkit-scrollbar-thumb': {
85
+ bgcolor: '#CED4DA', // 更浅的滚动条颜色
86
+ borderRadius: 4,
87
+ },
88
+ fontSize: '0.875rem',
89
+ fontFamily: 'monospace',
90
+ lineHeight: 1.5, // 增加行高
91
+ border: '1px solid #E9ECEF', // 添加柔和的边框
92
+ }}>
93
+ {code}
94
+ </Box>
95
+ <Tooltip title={t('common.copy')} placement="top">
96
+ <IconButton
97
+ onClick={handleCopy}
98
+ sx={{
99
+ position: 'absolute',
100
+ top: 8,
101
+ right: 8,
102
+ bgcolor: 'background.paper',
103
+ '&:hover': {
104
+ bgcolor: '#F8F9FA', // 更浅的悬停色
105
+ },
106
+ }}>
107
+ <ContentCopy fontSize="small" />
108
+ </IconButton>
109
+ </Tooltip>
110
+ </Box>
111
+ );
112
+ }
113
+
114
+ type SearchProps = {
115
+ pageSize: number;
116
+ page: number;
117
+ q?: {
118
+ 'like-description'?: string;
119
+ 'like-component_did'?: string;
120
+ 'like-mount_location'?: string;
121
+ };
122
+ o?: 'asc' | 'desc';
123
+ };
124
+
125
+ export default function DonationList() {
126
+ const listKey = 'donation-settings';
127
+ const { t } = useLocaleContext();
128
+ const [editingItem, setEditingItem] = useState<any>(null);
129
+ const [showExample, setShowExample] = useState(false);
130
+ const [confirmDialog, setConfirmDialog] = useState({
131
+ open: false,
132
+ id: '',
133
+ active: false,
134
+ mountLocation: '',
135
+ });
136
+
137
+ const persisted = getDurableData(listKey);
138
+ const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
139
+ defaultValue: {
140
+ o: 'desc',
141
+ pageSize: persisted.rowsPerPage || 10,
142
+ page: persisted.page ? persisted.page + 1 : 1,
143
+ },
144
+ });
145
+
146
+ const { error, data = { list: [], count: 0 }, refresh } = useRequest(() => fetchData(search));
147
+ const listData = data.list || [];
148
+
149
+ useEffect(() => {
150
+ refresh();
151
+ }, [search, refresh]);
152
+
153
+ useBus('donation.updated', () => refresh(), []);
154
+
155
+ const handleStatusChange = async (item: any, active: boolean) => {
156
+ if (active) {
157
+ setConfirmDialog({ open: true, id: item.id, active, mountLocation: item.mount_location });
158
+ return;
159
+ }
160
+ try {
161
+ await api.put(`/api/settings/${item.id}`, { active: !active });
162
+ clearDonateCache(item.mount_location);
163
+ Toast.success(t('admin.donate.status.activeSuccess'));
164
+ refresh();
165
+ } catch (err) {
166
+ Toast.error(formatError(err));
167
+ }
168
+ };
169
+
170
+ const handleConfirmStatusChange = async () => {
171
+ try {
172
+ await api.put(`/api/settings/${confirmDialog.id}`, { active: !confirmDialog.active });
173
+ clearDonateCache(confirmDialog.mountLocation);
174
+ Toast.success(t('admin.donate.status.inactiveSuccess'));
175
+ refresh();
176
+ } catch (err) {
177
+ Toast.error(formatError(err));
178
+ } finally {
179
+ setConfirmDialog({ open: false, id: '', active: false, mountLocation: '' });
180
+ }
181
+ };
182
+
183
+ const columns = [
184
+ {
185
+ label: t('admin.donate.component'),
186
+ name: 'component_did',
187
+ options: {
188
+ sort: false,
189
+ customBodyRenderLite: (index: number) => {
190
+ const componentInfo = window.blocklet?.componentMountPoints.find(
191
+ (c) => c?.did === listData[index]?.component_did
192
+ );
193
+ if (!componentInfo) {
194
+ return data[index]?.component_did || t('common.none');
195
+ }
196
+ return (
197
+ <Stack direction="row" alignItems="center" gap={1}>
198
+ <Avatar
199
+ src={joinURL(getPrefix(), `.well-known/service/blocklet/logo-bundle/${componentInfo.did}`)}
200
+ alt={componentInfo?.title}
201
+ />
202
+ <Typography onClick={() => window.open(componentInfo.mountPoint, '_blank')} sx={{ cursor: 'pointer' }}>
203
+ {componentInfo?.title}
204
+ </Typography>
205
+ </Stack>
206
+ );
207
+ },
208
+ },
209
+ },
210
+ {
211
+ label: t('admin.donate.mountLocation'),
212
+ name: 'mount_location',
213
+ options: { sort: false },
214
+ },
215
+ {
216
+ label: t('admin.donate.description'),
217
+ name: 'description',
218
+ options: { sort: false },
219
+ },
220
+ {
221
+ label: t('admin.donate.status.title'),
222
+ name: 'active',
223
+ width: 100,
224
+ options: {
225
+ sort: false,
226
+ customBodyRenderLite: (index: number) => (
227
+ <Switch
228
+ checked={listData[index].active}
229
+ onChange={() => handleStatusChange(listData[index], listData[index].active)}
230
+ />
231
+ ),
232
+ },
233
+ },
234
+ {
235
+ label: t('common.setting'),
236
+ width: 100,
237
+ options: {
238
+ sort: false,
239
+ customBodyRenderLite: (index: number) => (
240
+ <Button
241
+ component="text"
242
+ sx={{
243
+ color: 'text.link',
244
+ minWidth: 'fit-content',
245
+ }}
246
+ onClick={() => setEditingItem(listData[index])}>
247
+ {t('common.edit')}
248
+ </Button>
249
+ ),
250
+ },
251
+ },
252
+ ];
253
+
254
+ const onTableChange = ({ page, rowsPerPage }: { page: number; rowsPerPage: number }) => {
255
+ if (search!.pageSize !== rowsPerPage) {
256
+ setSearch((x: any) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
257
+ } else if (search!.page !== page + 1) {
258
+ setSearch((x: any) => ({ ...x, page: page + 1 }));
259
+ }
260
+ };
261
+
262
+ if (error) {
263
+ return <Alert severity="error">{error.message}</Alert>;
264
+ }
265
+
266
+ if (!data) {
267
+ return <CircularProgress />;
268
+ }
269
+
270
+ return (
271
+ <>
272
+ <Stack direction="row" alignItems="center" gap={1} flexWrap="wrap" mb={-1}>
273
+ <Typography variant="h6">{t('admin.donate.intro')}</Typography>
274
+ <Button variant="text" size="small" onClick={() => setShowExample(true)} sx={{ color: 'text.link' }}>
275
+ {t('admin.donate.usage.viewExample')}
276
+ </Button>
277
+ </Stack>
278
+ <Table
279
+ hasSearch
280
+ durable={`__${listKey}__`}
281
+ durableKeys={['searchText']}
282
+ data={listData}
283
+ columns={columns}
284
+ options={{
285
+ count: data.count,
286
+ page: search!.page - 1,
287
+ rowsPerPage: search!.pageSize,
288
+ onSearchChange: (text: string) => {
289
+ if (text) {
290
+ setSearch((x) => ({
291
+ ...x!,
292
+ q: {
293
+ 'like-mount_location': text,
294
+ 'like-description': text,
295
+ 'like-component_did': text,
296
+ },
297
+ page: 1,
298
+ }));
299
+ } else {
300
+ setSearch((x) => ({
301
+ ...x!,
302
+ page: 1,
303
+ q: {},
304
+ }));
305
+ }
306
+ },
307
+ }}
308
+ onChange={onTableChange}
309
+ />
310
+
311
+ {showExample && (
312
+ <Dialog
313
+ open
314
+ onClose={() => setShowExample(false)}
315
+ title={t('admin.donate.usage.title')}
316
+ maxWidth="md"
317
+ sx={{ '.MuiDialogContent-root': { padding: 0 } }}
318
+ fullWidth>
319
+ <Box sx={{ p: 3 }}>
320
+ <Typography variant="body2" paragraph>
321
+ {t('admin.donate.usage.description')}
322
+ </Typography>
323
+ <CodeExample code={exampleCode} />
324
+ <Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
325
+ {t('admin.donate.usage.note')}
326
+ </Typography>
327
+ </Box>
328
+ </Dialog>
329
+ )}
330
+
331
+ {editingItem && (
332
+ <Dialog
333
+ open
334
+ onClose={() => setEditingItem(null)}
335
+ title={t('admin.donate.editTitle')}
336
+ maxWidth="md"
337
+ fullWidth
338
+ sx={{ '.MuiDialogContent-root': { padding: 0 } }}>
339
+ <EditDonationForm
340
+ item={editingItem}
341
+ onSuccess={() => {
342
+ refresh();
343
+ setEditingItem(null);
344
+ }}
345
+ onCancel={() => setEditingItem(null)}
346
+ />
347
+ </Dialog>
348
+ )}
349
+
350
+ {confirmDialog.open && (
351
+ <ConfirmDialog
352
+ title={t('admin.donate.status.inactiveTitle')}
353
+ message={t('admin.donate.status.inactiveTip')}
354
+ onConfirm={handleConfirmStatusChange}
355
+ onCancel={() => setConfirmDialog({ open: false, id: '', active: false, mountLocation: '' })}
356
+ />
357
+ )}
358
+ </>
359
+ );
360
+ }
@@ -0,0 +1,229 @@
1
+ import { useState } from 'react';
2
+ import { Box, Typography, Button, TextField, Stack, Paper, Divider } from '@mui/material';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import type { TSetting } from '@blocklet/payment-types';
5
+
6
+ export default function DonationPreview({ config }: { config: TSetting['settings'] }) {
7
+ const { t } = useLocaleContext();
8
+ const hasPresets = config.settings.amount.presets?.length > 0;
9
+ const [selectedAmount, setSelectedAmount] = useState(config.settings.amount.preset);
10
+ const [customAmount, setCustomAmount] = useState('');
11
+ const [showCustom, setShowCustom] = useState(hasPresets ? false : config.settings.amount.custom);
12
+
13
+ const handleAmountSelect = (amount: string) => {
14
+ setSelectedAmount(amount);
15
+ setShowCustom(false);
16
+ setCustomAmount('');
17
+ };
18
+
19
+ const handleCustomClick = () => {
20
+ setShowCustom(true);
21
+ setSelectedAmount('');
22
+ };
23
+
24
+ return (
25
+ <Stack spacing={3}>
26
+ <Typography variant="h6" gutterBottom color="text.primary">
27
+ {t('admin.donate.previewTitle')}
28
+ </Typography>
29
+
30
+ {/* Button Preview */}
31
+ <Paper
32
+ elevation={0}
33
+ sx={{
34
+ p: 2,
35
+ bgcolor: 'background.paper',
36
+ border: '1px dashed',
37
+ borderColor: 'divider',
38
+ borderRadius: 1,
39
+ }}>
40
+ <Typography variant="caption" display="block" gutterBottom color="text.secondary" mb={1}>
41
+ {t('admin.donate.btn.preview')}
42
+ </Typography>
43
+ <Stack spacing={2} alignItems="center">
44
+ <Button variant="contained" color="primary">
45
+ {config.settings.btnText}
46
+ </Button>
47
+
48
+ <Box sx={{ width: '100%', maxWidth: 300 }}>
49
+ <Typography variant="caption" display="block" gutterBottom color="text.secondary" mb={1} textAlign="center">
50
+ total XX ABT
51
+ </Typography>
52
+ {config.settings.historyType === 'avatar' ? (
53
+ <Box
54
+ sx={{
55
+ height: 40,
56
+ position: 'relative',
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ justifyContent: 'center',
60
+ }}>
61
+ {Array(5)
62
+ .fill(0)
63
+ .map((_, i) => (
64
+ <Box
65
+ // eslint-disable-next-line react/no-array-index-key
66
+ key={i}
67
+ sx={{
68
+ width: 32,
69
+ height: 32,
70
+ borderRadius: '50%',
71
+ bgcolor: '#e0e0e0',
72
+ position: 'absolute',
73
+ left: `calc(50% - 16px + ${i * 24}px - ${2 * 24}px)`,
74
+ opacity: 1 - i * 0.1,
75
+ border: '2px solid #fff',
76
+ }}
77
+ />
78
+ ))}
79
+ </Box>
80
+ ) : (
81
+ <Box
82
+ sx={{
83
+ borderRadius: 1,
84
+ p: 1.5,
85
+ display: 'flex',
86
+ flexDirection: 'column',
87
+ alignItems: 'center',
88
+ gap: 1.5,
89
+ }}>
90
+ {Array(3)
91
+ .fill(0)
92
+ .map((_, i) => (
93
+ <Box
94
+ // eslint-disable-next-line react/no-array-index-key
95
+ key={i}
96
+ sx={{
97
+ display: 'flex',
98
+ justifyContent: 'center',
99
+ gap: 2,
100
+ opacity: 1 - i * 0.2,
101
+ width: '100%',
102
+ maxWidth: 240,
103
+ }}>
104
+ <Box
105
+ sx={{
106
+ width: '30%',
107
+ height: 8,
108
+ borderRadius: 1,
109
+ bgcolor: '#e0e0e0',
110
+ }}
111
+ />
112
+ <Box
113
+ sx={{
114
+ width: '20%',
115
+ height: 8,
116
+ borderRadius: 1,
117
+ bgcolor: '#e0e0e0',
118
+ }}
119
+ />
120
+ <Box
121
+ sx={{
122
+ width: '15%',
123
+ height: 8,
124
+ borderRadius: 1,
125
+ bgcolor: '#e0e0e0',
126
+ }}
127
+ />
128
+ </Box>
129
+ ))}
130
+ </Box>
131
+ )}
132
+ </Box>
133
+ </Stack>
134
+ </Paper>
135
+
136
+ {/* Dialog Preview */}
137
+ <Paper
138
+ elevation={0}
139
+ sx={{
140
+ p: 2,
141
+ bgcolor: 'background.paper',
142
+ border: '1px dashed',
143
+ borderColor: 'divider',
144
+ borderRadius: 1,
145
+ }}>
146
+ <Typography variant="caption" display="block" gutterBottom color="text.secondary" mb={1}>
147
+ {t('admin.donate.dialog.preview')}
148
+ </Typography>
149
+ <Stack spacing={3}>
150
+ <Box display="flex" gap={1} flexWrap="wrap">
151
+ {config.settings.amount.presets?.map((amount: string) => (
152
+ <Button
153
+ key={amount}
154
+ variant="outlined"
155
+ size="small"
156
+ sx={{
157
+ borderColor: selectedAmount === amount ? 'primary.main' : 'divider',
158
+ borderWidth: selectedAmount === amount ? 2 : 1,
159
+ color: selectedAmount === amount ? 'primary.main' : 'text.primary',
160
+ }}
161
+ onClick={() => handleAmountSelect(amount)}>
162
+ {`${amount} ABT`}
163
+ </Button>
164
+ ))}
165
+ {config.settings.amount.custom && hasPresets && (
166
+ <Button
167
+ variant="outlined"
168
+ size="small"
169
+ sx={{
170
+ borderColor: showCustom ? 'primary.main' : 'divider',
171
+ borderWidth: showCustom ? 2 : 1,
172
+ color: showCustom ? 'primary.main' : 'text.primary',
173
+ }}
174
+ onClick={handleCustomClick}>
175
+ {t('common.custom')}
176
+ </Button>
177
+ )}
178
+ </Box>
179
+
180
+ {showCustom && config.settings.amount.custom && (
181
+ <TextField
182
+ fullWidth
183
+ size="small"
184
+ autoFocus
185
+ label={t('admin.donate.customAmount')}
186
+ value={customAmount}
187
+ onChange={(e) => {
188
+ const { value } = e.target;
189
+ setCustomAmount(value);
190
+ }}
191
+ InputProps={{
192
+ endAdornment: <Typography sx={{ ml: 1 }}>ABT</Typography>,
193
+ autoComplete: 'off',
194
+ }}
195
+ helperText={t('admin.donate.amountRange', {
196
+ min: config.settings.amount.minimum,
197
+ max: config.settings.amount.maximum,
198
+ })}
199
+ />
200
+ )}
201
+ </Stack>
202
+ <Divider sx={{ my: 2 }} />
203
+ <Box display="flex" justifyContent="flex-end" alignItems="center" gap={1}>
204
+ <Box
205
+ sx={{
206
+ px: 2,
207
+ py: 0.5,
208
+ bgcolor: '#f5f5f5',
209
+ borderRadius: 1,
210
+ width: 40,
211
+ height: 24,
212
+ }}
213
+ />
214
+ <Box
215
+ sx={{
216
+ px: 2,
217
+ py: 0.5,
218
+ bgcolor: 'primary.main',
219
+ borderRadius: 1,
220
+ width: 40,
221
+ height: 24,
222
+ opacity: 0.5,
223
+ }}
224
+ />
225
+ </Box>
226
+ </Paper>
227
+ </Stack>
228
+ );
229
+ }
@@ -0,0 +1,80 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Stack } from '@mui/material';
3
+ import React, { useEffect } from 'react';
4
+ import { useNavigate, useParams } from 'react-router-dom';
5
+ import Tabs from '@arcblock/ux/lib/Tabs';
6
+
7
+ import Layout from '../../components/layout/admin';
8
+ import ProgressBar, { useTransitionContext } from '../../components/progress-bar';
9
+ import { useSessionContext } from '../../contexts/session';
10
+
11
+ const pages = {
12
+ overview: React.lazy(() => import('./overview')),
13
+ donations: React.lazy(() => import('./donations')),
14
+ };
15
+
16
+ function Integrations() {
17
+ const navigate = useNavigate();
18
+ const { t } = useLocaleContext();
19
+ const { group = 'overview' } = useParams();
20
+ const { isPending, startTransition } = useTransitionContext();
21
+
22
+ const onTabChange = (newTab: string) => {
23
+ startTransition(() => {
24
+ navigate(`/integrations/${newTab}`);
25
+ });
26
+ };
27
+
28
+ // @ts-ignore
29
+ const TabComponent = pages[group] || pages.overview;
30
+ const tabs = [
31
+ { label: t('common.quickStarts'), value: 'overview' },
32
+ { label: t('admin.donate.title'), value: 'donations' },
33
+ ];
34
+
35
+ return (
36
+ <>
37
+ <ProgressBar pending={isPending} />
38
+ <Stack direction="row" alignItems="center" justifyContent="end" flexWrap="wrap" spacing={1} sx={{ mt: 1, pb: 2 }}>
39
+ <Tabs
40
+ tabs={tabs}
41
+ current={group}
42
+ onChange={onTabChange}
43
+ scrollButtons="auto"
44
+ variant="scrollable"
45
+ sx={{
46
+ flex: '1 0 auto',
47
+ maxWidth: '100%',
48
+ '.MuiTab-root': {
49
+ marginBottom: '12px',
50
+ fontWeight: '500',
51
+ color: 'text.lighter',
52
+ '&.Mui-selected': {
53
+ color: 'text.primary',
54
+ },
55
+ },
56
+ '.MuiTouchRipple-root': {
57
+ display: 'none',
58
+ },
59
+ }}
60
+ />
61
+ </Stack>
62
+ <div className="page-content">{React.isValidElement(TabComponent) ? TabComponent : <TabComponent />}</div>
63
+ </>
64
+ );
65
+ }
66
+
67
+ export default function WrappedIntegrations() {
68
+ const { session } = useSessionContext();
69
+ const navigate = useNavigate();
70
+ useEffect(() => {
71
+ if (session.user && ['owner', 'admin'].includes(session.user.role) === false) {
72
+ navigate('/customer');
73
+ }
74
+ }, [session.user]);
75
+ return (
76
+ <Layout>
77
+ <Integrations />
78
+ </Layout>
79
+ );
80
+ }