payment-kit 1.13.26 → 1.13.27

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 (73) hide show
  1. package/api/src/index.ts +8 -1
  2. package/api/src/integrations/blockchain/nft.ts +125 -0
  3. package/api/src/integrations/blockchain/stake.ts +55 -0
  4. package/api/src/integrations/blocklet/notification.ts +101 -0
  5. package/api/src/integrations/blocklet/passport.ts +139 -0
  6. package/api/src/integrations/stripe/handlers/invoice.ts +1 -1
  7. package/api/src/integrations/stripe/resource.ts +7 -7
  8. package/api/src/integrations/stripe/setup.ts +1 -1
  9. package/api/src/jobs/checkout-session.ts +23 -0
  10. package/api/src/jobs/payment.ts +1 -2
  11. package/api/src/libs/audit.ts +44 -2
  12. package/api/src/libs/payment.ts +3 -4
  13. package/api/src/locales/en.ts +9 -1
  14. package/api/src/locales/zh.ts +9 -1
  15. package/api/src/routes/checkout-sessions.ts +44 -14
  16. package/api/src/routes/connect/collect.ts +1 -2
  17. package/api/src/routes/connect/pay.ts +1 -2
  18. package/api/src/routes/connect/setup.ts +1 -2
  19. package/api/src/routes/connect/shared.ts +7 -3
  20. package/api/src/routes/connect/subscribe.ts +2 -3
  21. package/api/src/routes/index.ts +4 -0
  22. package/api/src/routes/integrations/stripe.ts +1 -1
  23. package/api/src/routes/passports.ts +74 -0
  24. package/api/src/routes/payment-links.ts +12 -2
  25. package/api/src/routes/pricing-table.ts +17 -3
  26. package/api/src/routes/products.ts +3 -3
  27. package/api/src/routes/redirect.ts +18 -0
  28. package/api/src/routes/subscriptions.ts +2 -5
  29. package/api/src/store/migrations/20231021-nft.ts +22 -0
  30. package/api/src/store/models/checkout-session.ts +76 -20
  31. package/api/src/store/models/invoice.ts +2 -0
  32. package/api/src/store/models/payment-intent.ts +2 -0
  33. package/api/src/store/models/payment-link.ts +26 -15
  34. package/api/src/store/models/payment-method.ts +22 -1
  35. package/api/src/store/models/price.ts +2 -0
  36. package/api/src/store/models/subscription.ts +26 -4
  37. package/api/src/store/models/types.ts +32 -1
  38. package/api/third.d.ts +2 -0
  39. package/blocklet.yml +1 -1
  40. package/package.json +7 -5
  41. package/src/components/customer/actions.tsx +15 -17
  42. package/src/components/customer/form.tsx +1 -1
  43. package/src/components/invoice/list.tsx +2 -1
  44. package/src/components/passport/actions.tsx +62 -0
  45. package/src/components/passport/assign.tsx +82 -0
  46. package/src/components/payment-intent/list.tsx +5 -1
  47. package/src/components/payment-link/actions.tsx +14 -1
  48. package/src/components/payment-link/after-pay.tsx +33 -1
  49. package/src/components/payment-link/preview.tsx +3 -6
  50. package/src/components/price/form.tsx +22 -23
  51. package/src/components/pricing-table/actions.tsx +14 -1
  52. package/src/components/pricing-table/payment-settings.tsx +33 -1
  53. package/src/components/pricing-table/preview.tsx +3 -7
  54. package/src/components/pricing-table/product-settings.tsx +4 -0
  55. package/src/components/pricing-table/product-skeleton.tsx +39 -0
  56. package/src/components/product/actions.tsx +14 -1
  57. package/src/components/status.tsx +1 -1
  58. package/src/components/subscription/status.tsx +3 -3
  59. package/src/components/table.tsx +14 -4
  60. package/src/global.css +7 -5
  61. package/src/libs/util.ts +6 -0
  62. package/src/locales/en.tsx +53 -2
  63. package/src/locales/zh.tsx +272 -116
  64. package/src/pages/admin/payments/links/create.tsx +4 -0
  65. package/src/pages/admin/payments/links/detail.tsx +9 -4
  66. package/src/pages/admin/products/index.tsx +2 -0
  67. package/src/pages/admin/products/passports/index.tsx +154 -0
  68. package/src/pages/admin/products/pricing-tables/create.tsx +1 -1
  69. package/src/pages/admin/settings/index.tsx +1 -1
  70. package/src/pages/admin/settings/payment-methods/index.tsx +17 -7
  71. package/src/pages/checkout/pay.tsx +15 -13
  72. package/src/pages/checkout/pricing-table.tsx +127 -91
  73. package/api/src/libs/chain/arcblock.ts +0 -13
@@ -14,6 +14,7 @@ const pages = {
14
14
  products: React.lazy(() => import('./products')),
15
15
  coupons: React.lazy(() => import('./coupons')),
16
16
  'pricing-tables': React.lazy(() => import('./pricing-tables')),
17
+ passports: React.lazy(() => import('./passports')),
17
18
  };
18
19
 
19
20
  export default function Products() {
@@ -39,6 +40,7 @@ export default function Products() {
39
40
  { label: t('admin.products'), value: 'products' },
40
41
  { label: t('admin.coupons'), value: 'coupons' },
41
42
  { label: t('admin.pricingTables'), value: 'pricing-tables' },
43
+ { label: t('admin.passports'), value: 'passports' },
42
44
  ];
43
45
 
44
46
  let extra = null;
@@ -0,0 +1,154 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Alert, CircularProgress, Typography } from '@mui/material';
4
+ import { useRequest } from 'ahooks';
5
+ import { Link } from 'react-router-dom';
6
+
7
+ import PassportActions from '../../../../components/passport/actions';
8
+ import Table from '../../../../components/table';
9
+ import api from '../../../../libs/api';
10
+
11
+ const fetchData = (params: Record<string, any> = {}): Promise<any[]> => {
12
+ const search = new URLSearchParams();
13
+ Object.keys(params).forEach((key) => {
14
+ search.set(key, String(params[key]));
15
+ });
16
+ return api.get(`/api/passports?${search.toString()}`).then((res) => res.data);
17
+ };
18
+
19
+ export default function PassportList() {
20
+ const listKey = 'passports';
21
+
22
+ const { t } = useLocaleContext();
23
+
24
+ const { loading, error, data } = useRequest(fetchData);
25
+
26
+ if (error) {
27
+ return <Alert severity="error">{error.message}</Alert>;
28
+ }
29
+
30
+ if (loading || !data) {
31
+ return <CircularProgress />;
32
+ }
33
+
34
+ if (data && data.length === 0) {
35
+ return <Typography color="text.secondary">{t('admin.event.empty')}</Typography>;
36
+ }
37
+
38
+ const columns = [
39
+ {
40
+ label: t('common.name'),
41
+ name: 'title',
42
+ options: {
43
+ sort: false,
44
+ },
45
+ },
46
+ {
47
+ label: t('common.description'),
48
+ name: 'description',
49
+ options: {
50
+ sort: false,
51
+ },
52
+ },
53
+ {
54
+ label: t('admin.products'),
55
+ name: '',
56
+ options: {
57
+ sort: false,
58
+ customBodyRenderLite: (_: string, index: number) => {
59
+ const product = data[index].extra?.payment?.product;
60
+ if (product) {
61
+ return <Link to={`/admin/products/${product}`}>{product}</Link>;
62
+ }
63
+
64
+ return (
65
+ <Typography component="span" color="text.secondary">
66
+ None
67
+ </Typography>
68
+ );
69
+ },
70
+ },
71
+ },
72
+ {
73
+ label: t('admin.passport.payLink'),
74
+ name: '',
75
+ options: {
76
+ sort: false,
77
+ customBodyRenderLite: (_: string, index: number) => {
78
+ const payLink = data[index].extra?.acquire?.pay;
79
+ if (payLink) {
80
+ if (payLink.startsWith('plink_')) {
81
+ return <Link to={`/admin/payments/${payLink}`}>{payLink}</Link>;
82
+ }
83
+ if (payLink.startsWith('prctbl_')) {
84
+ return <Link to={`/admin/products/${payLink}`}>{payLink}</Link>;
85
+ }
86
+ }
87
+
88
+ return (
89
+ <Typography component="span" color="text.secondary">
90
+ None
91
+ </Typography>
92
+ );
93
+ },
94
+ },
95
+ },
96
+ {
97
+ label: t('admin.passport.payPreview'),
98
+ name: '',
99
+ options: {
100
+ sort: false,
101
+ customBodyRenderLite: (_: string, index: number) => {
102
+ const payLink = data[index].extra?.acquire?.pay;
103
+ if (payLink) {
104
+ if (payLink.startsWith('plink_')) {
105
+ return (
106
+ <Link to={`/checkout/pay/${payLink}`} target="_blank">
107
+ {payLink}
108
+ </Link>
109
+ );
110
+ }
111
+ if (payLink.startsWith('prctbl_')) {
112
+ return (
113
+ <Link to={`/checkout/pricing-table/${payLink}`} target="_blank">
114
+ {payLink}
115
+ </Link>
116
+ );
117
+ }
118
+ }
119
+
120
+ return (
121
+ <Typography component="span" color="text.secondary">
122
+ None
123
+ </Typography>
124
+ );
125
+ },
126
+ },
127
+ },
128
+ {
129
+ label: t('common.actions'),
130
+ name: '',
131
+ options: {
132
+ sort: false,
133
+ customBodyRenderLite: (_: string, index: number) => {
134
+ return <PassportActions data={data[index]} />;
135
+ },
136
+ },
137
+ },
138
+ ].filter(Boolean);
139
+
140
+ return (
141
+ <Table
142
+ durable={listKey}
143
+ data={data}
144
+ columns={columns}
145
+ loading={loading}
146
+ title={<Typography variant="subtitle1">{t('admin.passport.intro')}</Typography>}
147
+ options={{
148
+ count: data.length,
149
+ page: 1,
150
+ rowsPerPage: 100,
151
+ }}
152
+ />
153
+ );
154
+ }
@@ -42,7 +42,7 @@ export default function CreatePricingTable() {
42
42
  },
43
43
  });
44
44
 
45
- const changes = methods.watch(['items', 'branding_settings']);
45
+ const changes = methods.watch(['items', 'branding_settings', 'highlight', 'highlight_product_id', 'highlight_text']);
46
46
 
47
47
  useEffect(() => {
48
48
  api.post('/api/pricing-tables/stash', methods.getValues()).then(() => {
@@ -38,7 +38,7 @@ export default function SettingsIndex() {
38
38
  <>
39
39
  <Stack direction="row" alignItems="center" justifyContent="space-between">
40
40
  <Typography variant="h5" sx={{ mb: 1, fontWeight: 600 }}>
41
- {t('admin.products')}
41
+ {t('admin.settings')}
42
42
  </Typography>
43
43
  {extra}
44
44
  </Stack>
@@ -1,3 +1,4 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
1
2
  import type { TPaymentMethodExpanded } from '@did-pay/types';
2
3
  import { Alert, Box, CircularProgress, Grid, Typography } from '@mui/material';
3
4
  import { useRequest } from 'ahooks';
@@ -28,6 +29,7 @@ const groupByType = (methods: TPaymentMethodExpanded[]) => {
28
29
  };
29
30
 
30
31
  export default function PaymentMethods() {
32
+ const { t } = useLocaleContext();
31
33
  const { loading, error, data, runAsync } = useRequest(() => getMethods({}));
32
34
 
33
35
  useBus('paymentMethod.created', runAsync, []);
@@ -42,7 +44,6 @@ export default function PaymentMethods() {
42
44
 
43
45
  const groups = groupByType(data);
44
46
 
45
- // FIXME: i18n
46
47
  return (
47
48
  <>
48
49
  {Object.keys(groups).map((x) => (
@@ -63,13 +64,22 @@ export default function PaymentMethods() {
63
64
  }}>
64
65
  <Grid container spacing={2} mt={0}>
65
66
  <Grid item xs={12} md={6}>
66
- <InfoRow label="Type" value={method.type} />
67
- <InfoRow label="Confirmation" value={method.confirmation.type} />
68
- <InfoRow label="Recurring payments" value={method.features.recurring ? 'Yes' : 'No'} />
69
- <InfoRow label="Refund support" value={method.features.refund ? 'Yes' : 'No'} />
70
- <InfoRow label="Dispute support" value={method.features.dispute ? 'Yes' : 'No'} />
67
+ <InfoRow label={t('admin.paymentMethod.props.type')} value={method.type} />
68
+ <InfoRow label={t('admin.paymentMethod.props.confirmation')} value={method.confirmation.type} />
71
69
  <InfoRow
72
- label="Currencies"
70
+ label={t('admin.paymentMethod.props.recurring')}
71
+ value={method.features.recurring ? t('common.yes') : t('common.no')}
72
+ />
73
+ <InfoRow
74
+ label={t('admin.paymentMethod.props.refund')}
75
+ value={method.features.refund ? t('common.yes') : t('common.no')}
76
+ />
77
+ <InfoRow
78
+ label={t('admin.paymentMethod.props.dispute')}
79
+ value={method.features.dispute ? t('common.yes') : t('common.no')}
80
+ />
81
+ <InfoRow
82
+ label={t('admin.paymentMethod.props.currencies')}
73
83
  value={method.payment_currencies.map((currency) => currency.symbol).join(', ')}
74
84
  />
75
85
  </Grid>
@@ -24,8 +24,8 @@ type PageData = {
24
24
  customer?: TCustomer;
25
25
  };
26
26
 
27
- const startFromPaymentLink = async (id: string, preview: string): Promise<PageData> => {
28
- const { data } = await api.post(`/api/checkout-sessions/start/${id}?preview=${preview}`);
27
+ const startFromPaymentLink = async (id: string, preview: string, redirect: string = ''): Promise<PageData> => {
28
+ const { data } = await api.post(`/api/checkout-sessions/start/${id}?preview=${preview}&redirect=${redirect}`);
29
29
  return data;
30
30
  };
31
31
 
@@ -42,7 +42,9 @@ export default function Payment({ id }: Props) {
42
42
  const [state, setState] = useSetState({ completed: false, appError: null });
43
43
 
44
44
  const { error: apiError, data } = useRequest(() =>
45
- type === 'paymentLink' ? startFromPaymentLink(id, params.get('preview') || '') : fetchCheckoutSession(id)
45
+ type === 'paymentLink'
46
+ ? startFromPaymentLink(id, params.get('preview') || '', params.get('redirect') || '')
47
+ : fetchCheckoutSession(id)
46
48
  );
47
49
 
48
50
  useEffect(() => {
@@ -53,20 +55,20 @@ export default function Payment({ id }: Props) {
53
55
 
54
56
  const onPaid = () => {
55
57
  setState({ completed: true });
56
- if (data?.paymentLink) {
57
- if (data.paymentLink.after_completion?.type === 'redirect' && data.paymentLink.after_completion?.redirect?.url) {
58
- setTimeout(() => {
59
- // @ts-ignore
60
- window.location.href = data.paymentLink?.after_completion?.redirect?.url;
61
- }, 1000);
62
- }
63
- } else if (data?.checkoutSession?.success_url) {
58
+ if (data?.checkoutSession?.success_url) {
64
59
  setTimeout(() => {
65
- const tmp = new URL(data.checkoutSession.success_url as string);
60
+ const tmp = new URL(data.checkoutSession.success_url as string, window.location.origin);
66
61
  tmp.searchParams.set('checkout_session_id', data.checkoutSession.id);
67
- // @ts-ignore
68
62
  window.location.href = tmp.href;
69
63
  }, 1000);
64
+ } else if (data?.paymentLink) {
65
+ if (data.paymentLink.after_completion?.type === 'redirect' && data.paymentLink.after_completion?.redirect?.url) {
66
+ setTimeout(() => {
67
+ const tmp = new URL(data.paymentLink?.after_completion?.redirect?.url as string, window.location.origin);
68
+ tmp.searchParams.set('checkout_session_id', data.checkoutSession.id);
69
+ window.location.href = tmp.href;
70
+ }, 1000);
71
+ }
70
72
  }
71
73
  };
72
74
 
@@ -1,18 +1,19 @@
1
1
  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
+ import Header from '@blocklet/ui-react/lib/Header';
4
5
  import type { PriceRecurring, TPricingTableExpanded, TPricingTableItem } from '@did-pay/types';
5
6
  import { CheckOutlined } from '@mui/icons-material';
6
7
  import { LoadingButton } from '@mui/lab';
7
8
  import {
8
9
  Alert,
9
10
  Box,
10
- CircularProgress,
11
11
  Fade,
12
12
  List,
13
13
  ListItem,
14
14
  ListItemIcon,
15
15
  ListItemText,
16
+ Skeleton,
16
17
  Stack,
17
18
  ToggleButton,
18
19
  ToggleButtonGroup,
@@ -20,9 +21,11 @@ import {
20
21
  } from '@mui/material';
21
22
  import { useLocalStorageState, useRequest, useSetState } from 'ahooks';
22
23
  import { useEffect } from 'react';
24
+ import { useSearchParams } from 'react-router-dom';
23
25
 
24
26
  import PaymentAmount from '../../components/checkout/amount';
25
27
  import Livemode from '../../components/livemode';
28
+ import ProductSkeleton from '../../components/pricing-table/product-skeleton';
26
29
  import api from '../../libs/api';
27
30
  import { formatPriceAmount, formatRecurring } from '../../libs/util';
28
31
 
@@ -56,6 +59,7 @@ const groupItemsByRecurring = (items: TPricingTableItem[]) => {
56
59
 
57
60
  export default function PricingTable({ id }: Props) {
58
61
  const { t } = useLocaleContext();
62
+ const [params] = useSearchParams();
59
63
  const { error, loading, data } = useRequest(() => fetchData(id));
60
64
  const [state, setState] = useSetState({ interval: '', loading: '' });
61
65
  const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
@@ -73,17 +77,46 @@ export default function PricingTable({ id }: Props) {
73
77
 
74
78
  if (error) {
75
79
  return (
76
- <Center>
77
- <Alert severity="error">{error.message}</Alert>
78
- </Center>
80
+ <div style={{ height: '90vh', width: '100vw' }}>
81
+ <Header />
82
+ <Center relative="parent">
83
+ <Alert severity="error">{error.message}</Alert>
84
+ </Center>
85
+ </div>
79
86
  );
80
87
  }
81
88
 
82
89
  if (loading || !data) {
83
90
  return (
84
- <Center>
85
- <CircularProgress />
86
- </Center>
91
+ <div style={{ height: '90vh', width: '100vw' }}>
92
+ <Header />
93
+ <Center>
94
+ <Stack direction="column" alignItems="center" spacing={4}>
95
+ <Typography component="div" variant="h3" sx={{ width: '40%' }}>
96
+ <Skeleton />
97
+ </Typography>
98
+ <Typography component="div" variant="h6" sx={{ width: '10%' }}>
99
+ <Skeleton />
100
+ </Typography>
101
+ <Stack direction="row" flexWrap="wrap" spacing={5}>
102
+ <ProductSkeleton key={1} count={2} />
103
+ <ProductSkeleton key={2} count={3} />
104
+ <ProductSkeleton key={3} count={4} />
105
+ </Stack>
106
+ </Stack>
107
+ </Center>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ if (data.items.length === 0) {
113
+ return (
114
+ <div style={{ height: '90vh', width: '100vw' }}>
115
+ <Header />
116
+ <Center relative="parent">
117
+ <Alert severity="warning">{t('checkout.noPricing')}</Alert>
118
+ </Center>
119
+ </div>
87
120
  );
88
121
  }
89
122
 
@@ -92,7 +125,7 @@ export default function PricingTable({ id }: Props) {
92
125
  const onStartCheckoutSession = (priceId: string) => {
93
126
  setState({ loading: priceId });
94
127
  api
95
- .post(`/api/pricing-tables/${data.id}/checkout/${priceId}`)
128
+ .post(`/api/pricing-tables/${data.id}/checkout/${priceId}?redirect=${params.get('redirect') || ''}`)
96
129
  .then((res) => {
97
130
  window.location.href = res.data.url;
98
131
  })
@@ -104,92 +137,95 @@ export default function PricingTable({ id }: Props) {
104
137
  };
105
138
 
106
139
  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, '')}
140
+ <div style={{ height: '90vh', width: '100vw' }}>
141
+ <Header />
142
+ <Center relative="parent">
143
+ <Stack direction="column" alignItems="center" spacing={5}>
144
+ <Typography variant="h4" color="text.primary" fontWeight={600}>
145
+ {data.name}
146
+ {!livemode && <Livemode />}
147
+ </Typography>
148
+ {Object.keys(recurring).length > 1 && (
149
+ <ToggleButtonGroup value={state.interval} onChange={(_, value) => setState({ interval: value })} exclusive>
150
+ {Object.keys(recurring).map((x) => (
151
+ <ToggleButton key={x} value={x} sx={{ textTransform: 'capitalize' }}>
152
+ {formatRecurring(recurring[x] as PriceRecurring)}
153
+ </ToggleButton>
154
+ ))}
155
+ </ToggleButtonGroup>
156
+ )}
157
+ <Stack direction="row" flexWrap="wrap" spacing={5}>
158
+ {grouped[state.interval]?.map((x) => {
159
+ return (
160
+ <Fade in>
161
+ <Stack
162
+ key={x.price_id}
163
+ padding={4}
164
+ spacing={2}
165
+ direction="column"
166
+ alignItems="center"
167
+ sx={{
168
+ width: 320,
169
+ cursor: 'pointer',
170
+ border: '1px solid #eee',
171
+ borderRadius: 1,
172
+ transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
173
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
174
+ '&:hover': {
175
+ borderColor: '#ddd',
176
+ boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
177
+ },
178
+ }}>
179
+ <Box textAlign="center">
180
+ <Typography variant="h5" color="text.primary" fontWeight={600}>
181
+ {x.product.name}
158
182
  </Typography>
183
+ <Typography color="text.secondary">{x.product.description}</Typography>
184
+ </Box>
185
+ <Stack direction="row" alignItems="center" spacing={1}>
186
+ <PaymentAmount amount={formatPriceAmount(x.price, data.currency, x.product.unit_label)} />
187
+ <Stack direction="column" alignItems="flex-start">
188
+ <Typography component="span" color="text.secondary" fontSize="0.8rem">
189
+ per
190
+ </Typography>
191
+ <Typography component="span" color="text.secondary" fontSize="0.8rem">
192
+ {formatRecurring(x.price.recurring as PriceRecurring, false, '')}
193
+ </Typography>
194
+ </Stack>
159
195
  </Stack>
196
+ <LoadingButton
197
+ fullWidth
198
+ size="large"
199
+ loadingPosition="end"
200
+ variant={x.is_highlight ? 'contained' : 'outlined'}
201
+ color={x.is_highlight ? 'primary' : 'info'}
202
+ sx={{ fontSize: '1.2rem' }}
203
+ loading={state.loading === x.price_id}
204
+ onClick={() => onStartCheckoutSession(x.price_id)}>
205
+ {x.subscription_data?.trial_period_days ? t('checkout.try') : t('checkout.subscription')}
206
+ </LoadingButton>
207
+ {x.product.features.length > 0 && (
208
+ <Box>
209
+ <Typography>{t('checkout.include')}</Typography>
210
+ <List dense>
211
+ {x.product.features.map((f) => (
212
+ <ListItem key={f.name} disableGutters disablePadding>
213
+ <ListItemIcon sx={{ minWidth: 25 }}>
214
+ <CheckOutlined color="success" fontSize="small" />
215
+ </ListItemIcon>
216
+ <ListItemText primary={f.name} />
217
+ </ListItem>
218
+ ))}
219
+ </List>
220
+ </Box>
221
+ )}
160
222
  </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
- })}
223
+ </Fade>
224
+ );
225
+ })}
226
+ </Stack>
191
227
  </Stack>
192
- </Stack>
193
- </Center>
228
+ </Center>
229
+ </div>
194
230
  );
195
231
  }
@@ -1,13 +0,0 @@
1
- import Client from '@ocap/client';
2
-
3
- const cache = new Map<string, Client>();
4
- export function getClient(host: string) {
5
- const cached = cache.has(host);
6
- if (!cached) {
7
- const created = new Client(host);
8
- cache.set(host, created);
9
- return created;
10
- }
11
-
12
- return cache.get(host) as Client;
13
- }