payment-kit 1.13.92 → 1.13.94

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 (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/audit.ts +28 -34
  3. package/api/src/libs/payment.ts +2 -11
  4. package/api/src/libs/session.ts +1 -1
  5. package/api/src/libs/util.ts +8 -5
  6. package/api/src/routes/checkout-sessions.ts +41 -39
  7. package/api/src/routes/connect/collect.ts +12 -12
  8. package/api/src/routes/connect/setup.ts +8 -11
  9. package/api/src/routes/connect/shared.ts +81 -20
  10. package/api/src/routes/connect/subscribe.ts +8 -11
  11. package/api/src/routes/connect/update.ts +134 -0
  12. package/api/src/routes/pricing-table.ts +9 -121
  13. package/api/src/routes/subscriptions.ts +417 -142
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/pricing-table.ts +125 -1
  16. package/api/src/store/models/subscription.ts +4 -0
  17. package/api/src/store/models/types.ts +8 -0
  18. package/api/tests/libs/util.spec.ts +6 -6
  19. package/blocklet.yml +1 -1
  20. package/package.json +6 -6
  21. package/src/app.tsx +12 -4
  22. package/src/components/checkout/form/address.tsx +41 -34
  23. package/src/components/checkout/form/index.tsx +1 -1
  24. package/src/components/checkout/pricing-table.tsx +205 -0
  25. package/src/components/payment-link/product-select.tsx +13 -3
  26. package/src/components/portal/invoice/list.tsx +1 -1
  27. package/src/components/portal/subscription/actions.tsx +153 -0
  28. package/src/components/portal/subscription/list.tsx +21 -150
  29. package/src/components/subscription/metrics.tsx +46 -0
  30. package/src/contexts/products.tsx +2 -1
  31. package/src/libs/util.ts +43 -0
  32. package/src/locales/en.tsx +15 -1
  33. package/src/locales/zh.tsx +16 -2
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
  35. package/src/pages/checkout/pricing-table.tsx +9 -158
  36. package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
  37. package/src/pages/customer/subscription/update.tsx +281 -0
@@ -2,32 +2,15 @@ import Center from '@arcblock/ux/lib/Center';
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import Header from '@blocklet/ui-react/lib/Header';
5
- import type { PriceRecurring, TPricingTableExpanded, TPricingTableItem } from '@did-pay/types';
6
- import { CheckOutlined } from '@mui/icons-material';
7
- import { LoadingButton } from '@mui/lab';
8
- import {
9
- Alert,
10
- Box,
11
- Fade,
12
- List,
13
- ListItem,
14
- ListItemIcon,
15
- ListItemText,
16
- Skeleton,
17
- Stack,
18
- ToggleButton,
19
- ToggleButtonGroup,
20
- Typography,
21
- } from '@mui/material';
22
- import { useLocalStorageState, useRequest, useSetState } from 'ahooks';
23
- import { useEffect } from 'react';
5
+ import type { TPricingTableExpanded } from '@did-pay/types';
6
+ import { Alert, Box, Skeleton, Stack, Typography } from '@mui/material';
7
+ import { useLocalStorageState, useRequest } from 'ahooks';
24
8
  import { useSearchParams } from 'react-router-dom';
25
9
 
26
- import PaymentAmount from '../../components/checkout/amount';
10
+ import PricingTable from '../../components/checkout/pricing-table';
27
11
  import Livemode from '../../components/livemode';
28
12
  import ProductSkeleton from '../../components/pricing-table/product-skeleton';
29
13
  import api from '../../libs/api';
30
- import { formatPriceAmount, formatRecurring } from '../../libs/util';
31
14
 
32
15
  type Props = {
33
16
  id: string;
@@ -38,42 +21,11 @@ const fetchData = async (id: string): Promise<TPricingTableExpanded> => {
38
21
  return data;
39
22
  };
40
23
 
41
- const groupItemsByRecurring = (items: TPricingTableItem[]) => {
42
- const grouped: { [key: string]: TPricingTableItem[] } = {};
43
- const recurring: { [key: string]: PriceRecurring } = {};
44
-
45
- items.forEach((x) => {
46
- const key = [x.price.recurring?.interval, x.price.recurring?.interval_count].join('-');
47
- recurring[key] = x.price.recurring as PriceRecurring;
48
-
49
- if (!grouped[key]) {
50
- grouped[key] = [];
51
- }
52
-
53
- // @ts-ignore
54
- grouped[key].push(x);
55
- });
56
-
57
- return { recurring, grouped };
58
- };
59
-
60
- export default function PricingTable({ id }: Props) {
61
- const { t, locale } = useLocaleContext();
24
+ export default function PricingTablePage({ id }: Props) {
25
+ const { t } = useLocaleContext();
62
26
  const [params] = useSearchParams();
63
- const { error, loading, data } = useRequest(() => fetchData(id));
64
- const [state, setState] = useSetState({ interval: '', loading: '' });
65
27
  const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
66
-
67
- useEffect(() => {
68
- if (data && !state.interval) {
69
- const { recurring } = groupItemsByRecurring(data.items);
70
- const keys = Object.keys(recurring);
71
- if (keys[0]) {
72
- setState({ interval: keys[0] });
73
- }
74
- }
75
- // eslint-disable-next-line react-hooks/exhaustive-deps
76
- }, [data]);
28
+ const { error, loading, data } = useRequest(() => fetchData(id));
77
29
 
78
30
  if (error) {
79
31
  return (
@@ -141,10 +93,7 @@ export default function PricingTable({ id }: Props) {
141
93
  );
142
94
  }
143
95
 
144
- const { recurring, grouped } = groupItemsByRecurring(data.items);
145
-
146
96
  const onStartCheckoutSession = (priceId: string) => {
147
- setState({ loading: priceId });
148
97
  api
149
98
  .post(`/api/pricing-tables/${data.id}/checkout/${priceId}?${params.toString()}`)
150
99
  .then((res) => {
@@ -153,7 +102,6 @@ export default function PricingTable({ id }: Props) {
153
102
  .catch((err) => {
154
103
  console.error(err);
155
104
  Toast.error(err.message);
156
- setState({ loading: '' });
157
105
  });
158
106
  };
159
107
 
@@ -171,109 +119,12 @@ export default function PricingTable({ id }: Props) {
171
119
  }}>
172
120
  <Header />
173
121
  <Center relative="parent">
174
- <Stack
175
- direction="column"
176
- alignItems="center"
177
- sx={{
178
- pt: {
179
- xs: 4,
180
- sm: 2,
181
- },
182
- gap: {
183
- xs: 3,
184
- sm: 5,
185
- },
186
- }}>
122
+ <Stack direction="column" alignItems="center" spacing={4}>
187
123
  <Typography variant="h4" color="text.primary" fontWeight={600}>
188
124
  {data.name}
189
125
  {!livemode && <Livemode />}
190
126
  </Typography>
191
- {Object.keys(recurring).length > 1 && (
192
- <ToggleButtonGroup
193
- value={state.interval}
194
- onChange={(_, value) => {
195
- if (value !== null) {
196
- setState({ interval: value });
197
- }
198
- }}
199
- exclusive>
200
- {Object.keys(recurring).map((x) => (
201
- <ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
202
- {formatRecurring(recurring[x] as PriceRecurring, true, '', locale)}
203
- </ToggleButton>
204
- ))}
205
- </ToggleButtonGroup>
206
- )}
207
- <Stack flexWrap="wrap" direction="row" gap={{ xs: 3, sm: 5, md: 10 }} justifyContent="center">
208
- {grouped[state.interval]?.map((x) => {
209
- return (
210
- <Fade in>
211
- <Stack
212
- key={x.price_id}
213
- padding={4}
214
- spacing={2}
215
- direction="column"
216
- alignItems="center"
217
- sx={{
218
- width: 320,
219
- cursor: 'pointer',
220
- border: '1px solid #eee',
221
- borderRadius: 1,
222
- transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
223
- boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
224
- '&:hover': {
225
- borderColor: '#ddd',
226
- boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
227
- },
228
- }}>
229
- <Box textAlign="center">
230
- <Typography variant="h5" color="text.primary" fontWeight={600}>
231
- {x.product.name}
232
- </Typography>
233
- <Typography color="text.secondary">{x.product.description}</Typography>
234
- </Box>
235
- <Stack direction="row" alignItems="center" spacing={1}>
236
- <PaymentAmount amount={formatPriceAmount(x.price, data.currency, x.product.unit_label)} />
237
- <Stack direction="column" alignItems="flex-start">
238
- <Typography component="span" color="text.secondary" fontSize="0.8rem">
239
- {t('checkout.per')}
240
- </Typography>
241
- <Typography component="span" color="text.secondary" fontSize="0.8rem">
242
- {formatRecurring(x.price.recurring as PriceRecurring, false, '', locale)}
243
- </Typography>
244
- </Stack>
245
- </Stack>
246
- <LoadingButton
247
- fullWidth
248
- size="large"
249
- loadingPosition="end"
250
- variant={x.is_highlight ? 'contained' : 'outlined'}
251
- color={x.is_highlight ? 'primary' : 'info'}
252
- sx={{ fontSize: '1.2rem' }}
253
- loading={state.loading === x.price_id}
254
- onClick={() => onStartCheckoutSession(x.price_id)}>
255
- {x.subscription_data?.trial_period_days ? t('checkout.try') : t('checkout.subscription')}
256
- </LoadingButton>
257
- {x.product.features.length > 0 && (
258
- <Box>
259
- <Typography>{t('checkout.include')}</Typography>
260
- <List dense>
261
- {x.product.features.map((f: any) => (
262
- <ListItem key={f.name} disableGutters disablePadding>
263
- <ListItemIcon sx={{ minWidth: 25 }}>
264
- <CheckOutlined color="success" fontSize="small" />
265
- </ListItemIcon>
266
- <ListItemText primary={f.name} />
267
- </ListItem>
268
- ))}
269
- </List>
270
- </Box>
271
- )}
272
- </Stack>
273
- </Fade>
274
- );
275
- })}
276
- </Stack>
127
+ <PricingTable table={data} onSelect={onStartCheckoutSession} />
277
128
  </Stack>
278
129
  </Center>
279
130
  </Box>
@@ -9,11 +9,12 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
9
9
 
10
10
  import TxLink from '../../../components/blockchain/tx';
11
11
  import Currency from '../../../components/currency';
12
- import InfoMetric from '../../../components/info-metric';
13
12
  import InfoRow from '../../../components/info-row';
14
13
  import InvoiceList from '../../../components/invoice/list';
14
+ import SubscriptionActions from '../../../components/portal/subscription/actions';
15
15
  import SectionHeader from '../../../components/section/header';
16
16
  import SubscriptionItemList from '../../../components/subscription/items';
17
+ import SubscriptionMetrics from '../../../components/subscription/metrics';
17
18
  import SubscriptionStatus from '../../../components/subscription/status';
18
19
  import api from '../../../libs/api';
19
20
  import { formatSubscriptionProduct, formatTime } from '../../../libs/util';
@@ -22,10 +23,10 @@ const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
22
23
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
23
24
  };
24
25
 
25
- export default function CustomerSubscription() {
26
+ export default function CustomerSubscriptionDetail() {
26
27
  const { id } = useParams() as { id: string };
27
28
  const { t } = useLocaleContext();
28
- const { loading, error, data } = useRequest(() => fetchData(id));
29
+ const { loading, error, data, refresh } = useRequest(() => fetchData(id));
29
30
  const navigate = useNavigate();
30
31
 
31
32
  if (error) {
@@ -57,6 +58,7 @@ export default function CustomerSubscription() {
57
58
  </Typography>
58
59
  <SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
59
60
  </Stack>
61
+ <SubscriptionActions subscription={data} onChange={() => refresh()} showUpdate />
60
62
  </Stack>
61
63
  <Stack
62
64
  className="section-body"
@@ -65,39 +67,7 @@ export default function CustomerSubscription() {
65
67
  justifyContent="flex-start"
66
68
  flexWrap="wrap"
67
69
  sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
68
- <InfoMetric
69
- label={t('admin.subscription.startedAt')}
70
- value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
71
- divider
72
- />
73
- {data.status === 'active' && !data.cancel_at && (
74
- <InfoMetric
75
- label={t('admin.subscription.nextInvoice')}
76
- value={formatTime(data.current_period_end * 1000)}
77
- divider
78
- />
79
- )}
80
- {['active', 'trailing'].includes(data.status) && data.cancel_at && (
81
- <InfoMetric
82
- label={t('admin.subscription.cancel.schedule')}
83
- value={formatTime(data.cancel_at * 1000)}
84
- divider
85
- />
86
- )}
87
- {data.status !== 'canceled' && data.cancel_at_period_end && (
88
- <InfoMetric
89
- label={t('admin.subscription.cancel.schedule')}
90
- value={formatTime(data.current_period_end * 1000)}
91
- divider
92
- />
93
- )}
94
- {data.status === 'canceled' && data.canceled_at && (
95
- <InfoMetric
96
- label={t('admin.subscription.cancel.done')}
97
- value={formatTime(data.canceled_at * 1000)}
98
- divider
99
- />
100
- )}
70
+ <SubscriptionMetrics subscription={data} />
101
71
  </Stack>
102
72
  </Box>
103
73
  </Box>
@@ -0,0 +1,281 @@
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 { TLineItemExpanded, TPricingTableExpanded, TSubscriptionExpanded } from '@did-pay/types';
5
+ import { ArrowBackOutlined } from '@mui/icons-material';
6
+ import { LoadingButton } from '@mui/lab';
7
+ import { Alert, CircularProgress, Divider, Stack, Typography } from '@mui/material';
8
+ import { styled } from '@mui/system';
9
+ import { fromUnitToToken } from '@ocap/util';
10
+ import { useRequest, useSetState } from 'ahooks';
11
+ import { Link, useNavigate, useParams } from 'react-router-dom';
12
+
13
+ import PricingTable from '../../../components/checkout/pricing-table';
14
+ import InfoCard from '../../../components/info-card';
15
+ import SectionHeader from '../../../components/section/header';
16
+ import { useSessionContext } from '../../../contexts/session';
17
+ import api from '../../../libs/api';
18
+ import { formatError, formatPrice, formatSubscriptionProduct, formatTime } from '../../../libs/util';
19
+
20
+ const fetchData = async (
21
+ id: string
22
+ ): Promise<{ subscription: TSubscriptionExpanded; table: TPricingTableExpanded }> => {
23
+ const results = await Promise.all([
24
+ api.get(`/api/subscriptions/${id}`).then((res) => res.data),
25
+ api.get(`/api/subscriptions/${id}/update`).then((res) => res.data),
26
+ ]);
27
+
28
+ return {
29
+ subscription: results[0],
30
+ table: results[1],
31
+ };
32
+ };
33
+
34
+ type UpdateItem = {
35
+ id?: string;
36
+ deleted?: boolean;
37
+ clear_usage?: boolean;
38
+ price_id?: string;
39
+ quantity?: number;
40
+ };
41
+
42
+ const simulateUpdate = ({ id, items }: { id: string; items: UpdateItem[] }) => {
43
+ return api.post(`/api/subscriptions/${id}/update`, { items }).then((res) => res.data);
44
+ };
45
+
46
+ export default function CustomerSubscriptionUpdate() {
47
+ const navigate = useNavigate();
48
+ const { id } = useParams() as { id: string };
49
+ const { t, locale } = useLocaleContext();
50
+ const { connectApi } = useSessionContext();
51
+
52
+ const { loading, error, data } = useRequest(() => fetchData(id));
53
+ const [state, setState] = useSetState({
54
+ loading: false,
55
+ priceId: '',
56
+ total: '',
57
+ credit: '',
58
+ setup: null,
59
+ prorations: [],
60
+ items: [],
61
+ paying: false,
62
+ paid: false,
63
+ });
64
+
65
+ if (error) {
66
+ return <Alert severity="error">{error.message}</Alert>;
67
+ }
68
+
69
+ if (loading || !data) {
70
+ return <CircularProgress />;
71
+ }
72
+
73
+ if (!data.subscription) {
74
+ return <Alert severity="error">{t('customer.upgrade.subscriptionNotFound')}</Alert>;
75
+ }
76
+
77
+ if (!data.table) {
78
+ return <Alert severity="error">{t('customer.upgrade.tableNotFound')}</Alert>;
79
+ }
80
+
81
+ const handleSelect = async (priceId: string) => {
82
+ try {
83
+ if (state.priceId === priceId) {
84
+ return;
85
+ }
86
+
87
+ const deleted = data.subscription.items.find((si) => data.table.items.some((ti) => ti.price_id === si.price_id));
88
+ if (deleted!.price_id === priceId) {
89
+ setState({ priceId: '', total: '', credit: '', setup: null, prorations: [], items: [] });
90
+ return;
91
+ }
92
+
93
+ const result = await simulateUpdate({
94
+ id: data.subscription.id,
95
+ items: [
96
+ {
97
+ id: deleted!.id,
98
+ deleted: true,
99
+ clear_usage: false,
100
+ },
101
+ {
102
+ price_id: priceId,
103
+ quantity: 1, // FIXME: support customized quantity
104
+ },
105
+ ],
106
+ });
107
+
108
+ setState({ priceId, ...result });
109
+ } catch (err) {
110
+ Toast.error(formatError(err));
111
+ setState({ priceId: '', total: '', credit: '', setup: null, prorations: [], items: [] });
112
+ }
113
+ };
114
+
115
+ const handleBack = () => {
116
+ navigate(`/customer/subscription/${data.subscription.id}`);
117
+ };
118
+
119
+ const handleConfirm = async () => {
120
+ try {
121
+ setState({ loading: true });
122
+ const deleted = data.subscription.items.find((si) => data.table.items.some((ti) => ti.price_id === si.price_id));
123
+ const items = [
124
+ {
125
+ id: deleted!.id,
126
+ deleted: true,
127
+ clear_usage: false,
128
+ },
129
+ {
130
+ price_id: state.priceId,
131
+ quantity: 1, // FIXME: support customized quantity
132
+ },
133
+ ];
134
+
135
+ // FIXME: support more proration_behavior
136
+ const result = await api.put(`/api/subscriptions/${id}`, { proration_behavior: 'create_prorations', items });
137
+ if (result.data.status === 'active') {
138
+ Toast.success(t('customer.upgrade.success'));
139
+ setState({ paid: true });
140
+ setTimeout(handleBack, 2000);
141
+ } else {
142
+ setState({ paying: true });
143
+ try {
144
+ setState({ paying: true });
145
+ connectApi.open({
146
+ action: result.data.connectAction,
147
+ timeout: 5 * 60 * 1000,
148
+ messages: {
149
+ title: t('customer.upgrade.pay'),
150
+ scan: t('customer.upgrade.scan'),
151
+ success: t('customer.upgrade.success'),
152
+ error: t('customer.upgrade.error'),
153
+ },
154
+ extraParams: { invoiceId: result.data.latest_invoice_id, subscriptionId: result.data.id },
155
+ onSuccess: () => {
156
+ setState({ paid: true, paying: false });
157
+ setTimeout(() => {
158
+ connectApi.close();
159
+ handleBack();
160
+ }, 2000);
161
+ },
162
+ onClose: () => {
163
+ connectApi.close();
164
+ setState({ paying: false });
165
+ },
166
+ onError: (err: any) => {
167
+ setState({ paying: false });
168
+ Toast.error(formatError(err));
169
+ },
170
+ });
171
+ } catch (err) {
172
+ Toast.error(formatError(err));
173
+ } finally {
174
+ setState({ paying: false });
175
+ }
176
+ }
177
+ } catch (err) {
178
+ console.error(err);
179
+ Toast.error(formatError(err));
180
+ } finally {
181
+ setState({ loading: false });
182
+ }
183
+ };
184
+
185
+ const table = { ...data.table, currency: data.subscription.paymentCurrency };
186
+ table.items.forEach((x: any) => {
187
+ x.is_selected = x.price_id === state.priceId;
188
+ if (data.subscription.items.find((y) => y.price_id === x.price_id)) {
189
+ x.is_highlight = true;
190
+ x.is_disabled = true;
191
+ x.highlight_text = t('customer.upgrade.current');
192
+ if (!state.priceId) {
193
+ x.is_selected = true;
194
+ }
195
+ }
196
+ });
197
+
198
+ const { recurring } = data.subscription.items.find((x) => x.price.type === 'recurring')?.price || {};
199
+ const interval = [recurring?.interval, recurring?.interval_count].join('-');
200
+
201
+ return (
202
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
203
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
204
+ <Link to={`/customer/subscription/${data.subscription.id}`}>
205
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', mt: '16px' }}>
206
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
207
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
208
+ {formatSubscriptionProduct(data.subscription.items)}
209
+ </Typography>
210
+ </Stack>
211
+ </Link>
212
+ </Stack>
213
+ <Stack direction="column" sx={{ marginTop: 32 }}>
214
+ <SectionHeader title={t('customer.upgrade.config')} />
215
+ <PricingTable mode="select" alignItems="left" interval={interval} table={table} onSelect={handleSelect} />
216
+ </Stack>
217
+ {state.priceId && state.total && state.setup && (
218
+ <Stack direction="column" spacing={3} sx={{ maxWidth: 640 }}>
219
+ <SectionHeader title={t('customer.upgrade.confirm')} />
220
+ <Stack direction="column" spacing={2}>
221
+ <Typography variant="h6" sx={{ fontWeight: 'normal' }}>
222
+ {t('customer.upgrade.summary', {
223
+ // @ts-ignore
224
+ date: formatTime(state.setup.period.end * 1000, 'lll', locale),
225
+ })}
226
+ </Typography>
227
+ {state.items.map((x: TLineItemExpanded) => {
228
+ const { product } = x.price;
229
+ return (
230
+ <Stack key={x.price_id} direction="row" alignItems="center" justifyContent="space-between">
231
+ <InfoCard logo={product.images[0]} name={product.name} description={product.description} />
232
+ <Typography component="p" style={{ fontWeight: 'bold' }}>
233
+ {formatPrice(
234
+ x.price,
235
+ data.subscription.paymentCurrency,
236
+ x.price.product.unit_label,
237
+ 1,
238
+ true,
239
+ locale
240
+ )}
241
+ </Typography>
242
+ </Stack>
243
+ );
244
+ })}
245
+ </Stack>
246
+ <Divider />
247
+ <Stack direction="row" justifyContent="space-between">
248
+ <Typography variant="h6" sx={{ fontWeight: 'normal' }}>
249
+ {t('customer.upgrade.due')}
250
+ </Typography>
251
+ <Typography component="p" style={{ fontWeight: 'bold' }}>
252
+ {fromUnitToToken(state.total, data.subscription.paymentCurrency.decimal)}{' '}
253
+ {data.subscription.paymentCurrency.symbol}
254
+ </Typography>
255
+ </Stack>
256
+ <Divider />
257
+ <Stack direction="row" alignItems="center" justifyContent="flex-end" spacing={2}>
258
+ <LoadingButton
259
+ disabled={state.loading || state.paying}
260
+ onClick={() => setState({ priceId: '' })}
261
+ variant="text"
262
+ size="large">
263
+ {t('common.cancel')}
264
+ </LoadingButton>
265
+ <LoadingButton
266
+ onClick={handleConfirm}
267
+ variant="contained"
268
+ size="large"
269
+ sx={{ width: 150 }}
270
+ loading={state.loading || state.paying}
271
+ loadingPosition="end">
272
+ {t('common.confirm')}
273
+ </LoadingButton>
274
+ </Stack>
275
+ </Stack>
276
+ )}
277
+ </Root>
278
+ );
279
+ }
280
+
281
+ const Root = styled(Stack)``;