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,154 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import type { TPricingTableExpanded } from '@did-pay/types';
5
+ import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
6
+ import { useRequest } from 'ahooks';
7
+ import { useEffect, useState } from 'react';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import { joinURL } from 'ufo';
10
+ import useBus from 'use-bus';
11
+
12
+ import Copyable from '../../../../components/copyable';
13
+ import PricingTableActions from '../../../../components/pricing-table/actions';
14
+ import Status from '../../../../components/status';
15
+ import Table from '../../../../components/table';
16
+ import { ProductsProvider } from '../../../../contexts/products';
17
+ import api from '../../../../libs/api';
18
+ import { formatTime } from '../../../../libs/util';
19
+
20
+ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPricingTableExpanded[]; count: number }> => {
21
+ const search = new URLSearchParams();
22
+ Object.keys(params).forEach((key) => {
23
+ search.set(key, String(params[key]));
24
+ });
25
+ return api.get(`/api/pricing-tables?${search.toString()}`).then((res) => res.data);
26
+ };
27
+
28
+ function PricingTables() {
29
+ const listKey = 'pricing-tables';
30
+ const { t } = useLocaleContext();
31
+ const navigate = useNavigate();
32
+
33
+ const persisted = getDurableData(listKey);
34
+ const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
35
+ active: '',
36
+ pageSize: persisted.rowsPerPage || 20,
37
+ page: persisted.page ? persisted.page + 1 : 1,
38
+ });
39
+
40
+ const { loading, error, data, refresh } = useRequest(() => fetchData(search));
41
+ useEffect(() => {
42
+ refresh();
43
+ }, [search, refresh]);
44
+
45
+ useBus('pricingTable.created', () => refresh(), []);
46
+
47
+ if (error) {
48
+ return <Alert severity="error">{error.message}</Alert>;
49
+ }
50
+
51
+ if (loading || !data) {
52
+ return <CircularProgress />;
53
+ }
54
+
55
+ const columns = [
56
+ {
57
+ label: t('common.url'),
58
+ name: 'id',
59
+ options: {
60
+ customBodyRenderLite: (_: string, index: number) => {
61
+ const item = data.list[index] as TPricingTableExpanded;
62
+ return (
63
+ <Copyable
64
+ text={joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pricing-table/${item.id}`)}>
65
+ <Typography sx={{ color: 'text.primary', mr: 1 }}>{item.id}</Typography>
66
+ </Copyable>
67
+ );
68
+ },
69
+ },
70
+ },
71
+ {
72
+ label: t('common.status'),
73
+ name: 'active',
74
+ options: {
75
+ customBodyRenderLite: (_: string, index: number) => {
76
+ const item = data.list[index];
77
+ return <Status label={item?.active ? 'Active' : 'Archived'} color={item?.active ? 'success' : 'default'} />;
78
+ },
79
+ },
80
+ },
81
+ {
82
+ label: t('common.name'),
83
+ name: 'name',
84
+ },
85
+ {
86
+ label: t('common.createdAt'),
87
+ name: 'created_at',
88
+ options: {
89
+ customBodyRender: (e: string) => {
90
+ return formatTime(e);
91
+ },
92
+ },
93
+ },
94
+ {
95
+ label: t('common.actions'),
96
+ name: 'id',
97
+ options: {
98
+ sort: false,
99
+ customBodyRenderLite: (_: string, index: number) => {
100
+ const doc = data.list[index] as TPricingTableExpanded;
101
+ return <PricingTableActions data={doc} onChange={refresh} />;
102
+ },
103
+ },
104
+ },
105
+ ];
106
+
107
+ const onTableChange = ({ page, rowsPerPage }: any) => {
108
+ if (search.pageSize !== rowsPerPage) {
109
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
110
+ } else if (search.page !== page + 1) {
111
+ setSearch((x) => ({ ...x, page: page + 1 }));
112
+ }
113
+ };
114
+
115
+ return (
116
+ <Table
117
+ durable={listKey}
118
+ durableKeys={['page', 'rowsPerPage']}
119
+ title={
120
+ <div className="table-toolbar-left">
121
+ <ToggleButtonGroup
122
+ value={search.active}
123
+ onChange={(_, value) => setSearch((x) => ({ ...x, active: value }))}
124
+ exclusive>
125
+ <ToggleButton value="">All</ToggleButton>
126
+ <ToggleButton value="true">Active</ToggleButton>
127
+ <ToggleButton value="false">Archived</ToggleButton>
128
+ </ToggleButtonGroup>
129
+ </div>
130
+ }
131
+ data={data.list}
132
+ columns={columns}
133
+ options={{
134
+ count: data.count,
135
+ page: search.page - 1,
136
+ rowsPerPage: search.pageSize,
137
+ onRowClick: (_: any, { dataIndex }: any) => {
138
+ const item = data.list[dataIndex] as TPricingTableExpanded;
139
+ navigate(`/admin/products/${item.id}`);
140
+ },
141
+ }}
142
+ loading={loading}
143
+ onChange={onTableChange}
144
+ />
145
+ );
146
+ }
147
+
148
+ export default function WrappedPricingTables() {
149
+ return (
150
+ <ProductsProvider>
151
+ <PricingTables />
152
+ </ProductsProvider>
153
+ );
154
+ }
@@ -34,7 +34,7 @@ export default function ProductsCreate() {
34
34
  metadata: [],
35
35
  },
36
36
  });
37
- const { control, handleSubmit } = methods;
37
+ const { control, handleSubmit, getValues } = methods;
38
38
 
39
39
  const prices = useFieldArray({ control, name: 'prices' });
40
40
  const getPrice = (index: number) => methods.getValues().prices[index];
@@ -79,10 +79,14 @@ export default function ProductsCreate() {
79
79
  expanded
80
80
  style={{ fontWeight: 'bold', width: '50%' }}
81
81
  addons={<PriceActions onDuplicate={() => prices.append(price)} onRemove={() => prices.remove(index)} />}
82
- trigger={(expanded: boolean) =>
82
+ trigger={(expanded: boolean) => {
83
+ if (expanded) {
84
+ return t('admin.price.detail');
85
+ }
86
+
83
87
  // @ts-ignore
84
- expanded ? t('admin.price.detail') : formatPrice(getPrice(index), settings.baseCurrency)
85
- }>
88
+ return formatPrice(getPrice(index), settings.baseCurrency, getValues().unit_label, 1, false);
89
+ }}>
86
90
  <PriceForm prefix={`prices.${index}`} />
87
91
  </Collapse>
88
92
  <Divider sx={{ mt: 2, mb: 4 }} />
@@ -6,10 +6,11 @@ import { SettingsProvider } from '../../contexts/settings';
6
6
 
7
7
  const pages = {
8
8
  pay: React.lazy(() => import('./pay')),
9
+ 'pricing-table': React.lazy(() => import('./pricing-table')),
9
10
  };
10
11
 
11
12
  function Checkout() {
12
- const { action, id } = useParams<{ action: 'pay'; id: string }>();
13
+ const { action, id } = useParams<{ action: string; id: string }>();
13
14
 
14
15
  // @ts-ignore
15
16
  const TabComponent = pages[action];
@@ -0,0 +1,195 @@
1
+ import Center from '@arcblock/ux/lib/Center';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import type { PriceRecurring, TPricingTableExpanded, TPricingTableItem } from '@did-pay/types';
5
+ import { CheckOutlined } from '@mui/icons-material';
6
+ import { LoadingButton } from '@mui/lab';
7
+ import {
8
+ Alert,
9
+ Box,
10
+ CircularProgress,
11
+ Fade,
12
+ List,
13
+ ListItem,
14
+ ListItemIcon,
15
+ ListItemText,
16
+ Stack,
17
+ ToggleButton,
18
+ ToggleButtonGroup,
19
+ Typography,
20
+ } from '@mui/material';
21
+ import { useLocalStorageState, useRequest, useSetState } from 'ahooks';
22
+ import { useEffect } from 'react';
23
+
24
+ import PaymentAmount from '../../components/checkout/amount';
25
+ import Livemode from '../../components/livemode';
26
+ import api from '../../libs/api';
27
+ import { formatPriceAmount, formatRecurring } from '../../libs/util';
28
+
29
+ type Props = {
30
+ id: string;
31
+ };
32
+
33
+ const fetchData = async (id: string): Promise<TPricingTableExpanded> => {
34
+ const { data } = await api.get(`/api/pricing-tables/${id}`);
35
+ return data;
36
+ };
37
+
38
+ const groupItemsByRecurring = (items: TPricingTableItem[]) => {
39
+ const grouped: { [key: string]: TPricingTableItem[] } = {};
40
+ const recurring: { [key: string]: PriceRecurring } = {};
41
+
42
+ items.forEach((x) => {
43
+ const key = [x.price.recurring?.interval, x.price.recurring?.interval_count].join('-');
44
+ recurring[key] = x.price.recurring as PriceRecurring;
45
+
46
+ if (!grouped[key]) {
47
+ grouped[key] = [];
48
+ }
49
+
50
+ // @ts-ignore
51
+ grouped[key].push(x);
52
+ });
53
+
54
+ return { recurring, grouped };
55
+ };
56
+
57
+ export default function PricingTable({ id }: Props) {
58
+ const { t } = useLocaleContext();
59
+ const { error, loading, data } = useRequest(() => fetchData(id));
60
+ const [state, setState] = useSetState({ interval: '', loading: '' });
61
+ const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
62
+
63
+ useEffect(() => {
64
+ if (data && !state.interval) {
65
+ const { recurring } = groupItemsByRecurring(data.items);
66
+ const keys = Object.keys(recurring);
67
+ if (keys[0]) {
68
+ setState({ interval: keys[0] });
69
+ }
70
+ }
71
+ // eslint-disable-next-line react-hooks/exhaustive-deps
72
+ }, [data]);
73
+
74
+ if (error) {
75
+ return (
76
+ <Center>
77
+ <Alert severity="error">{error.message}</Alert>
78
+ </Center>
79
+ );
80
+ }
81
+
82
+ if (loading || !data) {
83
+ return (
84
+ <Center>
85
+ <CircularProgress />
86
+ </Center>
87
+ );
88
+ }
89
+
90
+ const { recurring, grouped } = groupItemsByRecurring(data.items);
91
+
92
+ const onStartCheckoutSession = (priceId: string) => {
93
+ setState({ loading: priceId });
94
+ api
95
+ .post(`/api/pricing-tables/${data.id}/checkout/${priceId}`)
96
+ .then((res) => {
97
+ window.location.href = res.data.url;
98
+ })
99
+ .catch((err) => {
100
+ console.error(err);
101
+ Toast.error(err.message);
102
+ setState({ loading: '' });
103
+ });
104
+ };
105
+
106
+ return (
107
+ <Center>
108
+ <Stack direction="column" alignItems="center" spacing={4}>
109
+ <Typography variant="h4" color="text.primary" fontWeight={600}>
110
+ {data.name}
111
+ {!livemode && <Livemode />}
112
+ </Typography>
113
+ {Object.keys(recurring).length > 1 && (
114
+ <ToggleButtonGroup value={state.interval} onChange={(_, value) => setState({ interval: value })} exclusive>
115
+ {Object.keys(recurring).map((x) => (
116
+ <ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
117
+ {formatRecurring(recurring[x] as PriceRecurring)}
118
+ </ToggleButton>
119
+ ))}
120
+ </ToggleButtonGroup>
121
+ )}
122
+ <Stack direction="row" flexWrap="wrap" spacing={5}>
123
+ {grouped[state.interval]?.map((x) => {
124
+ return (
125
+ <Fade in>
126
+ <Stack
127
+ key={x.price_id}
128
+ padding={4}
129
+ spacing={2}
130
+ direction="column"
131
+ alignItems="center"
132
+ sx={{
133
+ width: 320,
134
+ cursor: 'pointer',
135
+ border: '1px solid #eee',
136
+ borderRadius: 1,
137
+ transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
138
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
139
+ '&:hover': {
140
+ borderColor: '#ddd',
141
+ boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
142
+ },
143
+ }}>
144
+ <Box textAlign="center">
145
+ <Typography variant="h5" color="text.primary" fontWeight={600}>
146
+ {x.product.name}
147
+ </Typography>
148
+ <Typography color="text.secondary">{x.product.description}</Typography>
149
+ </Box>
150
+ <Stack direction="row" alignItems="center" spacing={1}>
151
+ <PaymentAmount amount={formatPriceAmount(x.price, data.currency, x.product.unit_label)} />
152
+ <Stack direction="column" alignItems="flex-start">
153
+ <Typography component="span" color="text.secondary" fontSize="0.8rem">
154
+ per
155
+ </Typography>
156
+ <Typography component="span" color="text.secondary" fontSize="0.8rem">
157
+ {formatRecurring(x.price.recurring as PriceRecurring, false, '')}
158
+ </Typography>
159
+ </Stack>
160
+ </Stack>
161
+ <LoadingButton
162
+ fullWidth
163
+ size="large"
164
+ loadingPosition="end"
165
+ variant={x.is_highlight ? 'contained' : 'outlined'}
166
+ color={x.is_highlight ? 'primary' : 'info'}
167
+ sx={{ fontSize: '1.2rem' }}
168
+ loading={state.loading === x.price_id}
169
+ onClick={() => onStartCheckoutSession(x.price_id)}>
170
+ {x.subscription_data?.trial_period_days ? t('checkout.try') : t('checkout.subscription')}
171
+ </LoadingButton>
172
+ {x.product.features.length > 0 && (
173
+ <Box>
174
+ <Typography>{t('checkout.include')}</Typography>
175
+ <List dense>
176
+ {x.product.features.map((f) => (
177
+ <ListItem key={f.name} disableGutters disablePadding>
178
+ <ListItemIcon sx={{ minWidth: 25 }}>
179
+ <CheckOutlined color="success" fontSize="small" />
180
+ </ListItemIcon>
181
+ <ListItemText primary={f.name} />
182
+ </ListItem>
183
+ ))}
184
+ </List>
185
+ </Box>
186
+ )}
187
+ </Stack>
188
+ </Fade>
189
+ );
190
+ })}
191
+ </Stack>
192
+ </Stack>
193
+ </Center>
194
+ );
195
+ }
@@ -1,3 +0,0 @@
1
- export default function PricingTables() {
2
- return <div>PricingTables</div>;
3
- }