payment-kit 1.13.15

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 (222) hide show
  1. package/.eslintrc.js +15 -0
  2. package/README.md +3 -0
  3. package/api/dev.ts +6 -0
  4. package/api/hooks/pre-start.js +12 -0
  5. package/api/src/hooks/pre-start.ts +21 -0
  6. package/api/src/index.ts +92 -0
  7. package/api/src/jobs/event.ts +72 -0
  8. package/api/src/jobs/invoice.ts +148 -0
  9. package/api/src/jobs/payment.ts +208 -0
  10. package/api/src/jobs/subscription.ts +301 -0
  11. package/api/src/jobs/webhook.ts +113 -0
  12. package/api/src/libs/audit.ts +73 -0
  13. package/api/src/libs/auth.ts +40 -0
  14. package/api/src/libs/chain/arcblock.ts +13 -0
  15. package/api/src/libs/dayjs.ts +17 -0
  16. package/api/src/libs/env.ts +5 -0
  17. package/api/src/libs/hooks.ts +42 -0
  18. package/api/src/libs/logger.ts +27 -0
  19. package/api/src/libs/middleware.ts +12 -0
  20. package/api/src/libs/payment.ts +53 -0
  21. package/api/src/libs/queue/index.ts +263 -0
  22. package/api/src/libs/queue/store.ts +47 -0
  23. package/api/src/libs/security.ts +95 -0
  24. package/api/src/libs/session.ts +164 -0
  25. package/api/src/libs/util.ts +93 -0
  26. package/api/src/locales/en.ts +3 -0
  27. package/api/src/locales/index.ts +37 -0
  28. package/api/src/locales/zh.ts +3 -0
  29. package/api/src/routes/checkout-sessions.ts +536 -0
  30. package/api/src/routes/connect/collect.ts +109 -0
  31. package/api/src/routes/connect/pay.ts +116 -0
  32. package/api/src/routes/connect/setup.ts +121 -0
  33. package/api/src/routes/connect/shared.ts +410 -0
  34. package/api/src/routes/connect/subscribe.ts +128 -0
  35. package/api/src/routes/customers.ts +70 -0
  36. package/api/src/routes/events.ts +76 -0
  37. package/api/src/routes/index.ts +59 -0
  38. package/api/src/routes/invoices.ts +126 -0
  39. package/api/src/routes/payment-currencies.ts +38 -0
  40. package/api/src/routes/payment-intents.ts +122 -0
  41. package/api/src/routes/payment-links.ts +221 -0
  42. package/api/src/routes/payment-methods.ts +39 -0
  43. package/api/src/routes/prices.ts +134 -0
  44. package/api/src/routes/products.ts +191 -0
  45. package/api/src/routes/settings.ts +33 -0
  46. package/api/src/routes/subscription-items.ts +148 -0
  47. package/api/src/routes/subscriptions.ts +254 -0
  48. package/api/src/routes/usage-records.ts +120 -0
  49. package/api/src/routes/webhook-attempts.ts +57 -0
  50. package/api/src/routes/webhook-endpoints.ts +105 -0
  51. package/api/src/store/migrate.ts +16 -0
  52. package/api/src/store/migrations/20230905-genesis.ts +52 -0
  53. package/api/src/store/migrations/20230911-seeding.ts +145 -0
  54. package/api/src/store/models/checkout-session.ts +395 -0
  55. package/api/src/store/models/coupon.ts +137 -0
  56. package/api/src/store/models/customer.ts +199 -0
  57. package/api/src/store/models/discount.ts +116 -0
  58. package/api/src/store/models/event.ts +111 -0
  59. package/api/src/store/models/index.ts +165 -0
  60. package/api/src/store/models/invoice-item.ts +185 -0
  61. package/api/src/store/models/invoice.ts +492 -0
  62. package/api/src/store/models/job.ts +75 -0
  63. package/api/src/store/models/payment-currency.ts +139 -0
  64. package/api/src/store/models/payment-intent.ts +282 -0
  65. package/api/src/store/models/payment-link.ts +219 -0
  66. package/api/src/store/models/payment-method.ts +169 -0
  67. package/api/src/store/models/price.ts +266 -0
  68. package/api/src/store/models/product.ts +162 -0
  69. package/api/src/store/models/promotion-code.ts +112 -0
  70. package/api/src/store/models/setup-intent.ts +206 -0
  71. package/api/src/store/models/subscription-item.ts +103 -0
  72. package/api/src/store/models/subscription-schedule.ts +157 -0
  73. package/api/src/store/models/subscription.ts +307 -0
  74. package/api/src/store/models/types.ts +406 -0
  75. package/api/src/store/models/usage-record.ts +132 -0
  76. package/api/src/store/models/webhook-attempt.ts +96 -0
  77. package/api/src/store/models/webhook-endpoint.ts +96 -0
  78. package/api/src/store/sequelize.ts +15 -0
  79. package/api/third.d.ts +28 -0
  80. package/blocklet.md +3 -0
  81. package/blocklet.yml +89 -0
  82. package/index.html +14 -0
  83. package/logo.png +0 -0
  84. package/package.json +133 -0
  85. package/public/.gitkeep +0 -0
  86. package/screenshots/.gitkeep +0 -0
  87. package/screenshots/1-subscription.png +0 -0
  88. package/screenshots/2-customer-1.png +0 -0
  89. package/screenshots/3-customer-2.png +0 -0
  90. package/screenshots/4-admin-3.png +0 -0
  91. package/screenshots/5-admin-4.png +0 -0
  92. package/scripts/build-clean.js +6 -0
  93. package/scripts/bump-version.mjs +35 -0
  94. package/src/app.tsx +68 -0
  95. package/src/components/actions.tsx +85 -0
  96. package/src/components/blockchain/tx.tsx +29 -0
  97. package/src/components/checkout/amount.tsx +24 -0
  98. package/src/components/checkout/error.tsx +30 -0
  99. package/src/components/checkout/footer.tsx +12 -0
  100. package/src/components/checkout/form/address.tsx +38 -0
  101. package/src/components/checkout/form/index.tsx +295 -0
  102. package/src/components/checkout/header.tsx +23 -0
  103. package/src/components/checkout/pay.tsx +222 -0
  104. package/src/components/checkout/product-card.tsx +56 -0
  105. package/src/components/checkout/product-item.tsx +37 -0
  106. package/src/components/checkout/skeleton/overview.tsx +21 -0
  107. package/src/components/checkout/skeleton/payment.tsx +35 -0
  108. package/src/components/checkout/success.tsx +183 -0
  109. package/src/components/checkout/summary.tsx +34 -0
  110. package/src/components/collapse.tsx +50 -0
  111. package/src/components/confirm.tsx +55 -0
  112. package/src/components/copyable.tsx +38 -0
  113. package/src/components/currency.tsx +15 -0
  114. package/src/components/customer/actions.tsx +73 -0
  115. package/src/components/data.tsx +20 -0
  116. package/src/components/drawer-form.tsx +77 -0
  117. package/src/components/error-fallback.tsx +7 -0
  118. package/src/components/error.tsx +39 -0
  119. package/src/components/event/list.tsx +217 -0
  120. package/src/components/info-card.tsx +40 -0
  121. package/src/components/info-metric.tsx +35 -0
  122. package/src/components/info-row.tsx +28 -0
  123. package/src/components/input.tsx +40 -0
  124. package/src/components/invoice/action.tsx +94 -0
  125. package/src/components/invoice/list.tsx +225 -0
  126. package/src/components/invoice/table.tsx +110 -0
  127. package/src/components/layout.tsx +70 -0
  128. package/src/components/livemode.tsx +23 -0
  129. package/src/components/metadata/editor.tsx +57 -0
  130. package/src/components/metadata/form.tsx +45 -0
  131. package/src/components/payment-intent/actions.tsx +81 -0
  132. package/src/components/payment-intent/list.tsx +204 -0
  133. package/src/components/payment-link/actions.tsx +114 -0
  134. package/src/components/payment-link/after-pay.tsx +87 -0
  135. package/src/components/payment-link/before-pay.tsx +175 -0
  136. package/src/components/payment-link/item.tsx +135 -0
  137. package/src/components/payment-link/product-select.tsx +66 -0
  138. package/src/components/payment-link/rename.tsx +64 -0
  139. package/src/components/portal/invoice/list.tsx +110 -0
  140. package/src/components/portal/subscription/cancel.tsx +83 -0
  141. package/src/components/portal/subscription/list.tsx +232 -0
  142. package/src/components/price/actions.tsx +21 -0
  143. package/src/components/price/form.tsx +292 -0
  144. package/src/components/product/actions.tsx +125 -0
  145. package/src/components/product/add-price.tsx +59 -0
  146. package/src/components/product/create.tsx +97 -0
  147. package/src/components/product/edit-price.tsx +75 -0
  148. package/src/components/product/edit.tsx +67 -0
  149. package/src/components/product/features.tsx +32 -0
  150. package/src/components/product/form.tsx +76 -0
  151. package/src/components/relative-time.tsx +41 -0
  152. package/src/components/section/header.tsx +29 -0
  153. package/src/components/status.tsx +12 -0
  154. package/src/components/subscription/actions/cancel.tsx +66 -0
  155. package/src/components/subscription/actions/index.tsx +172 -0
  156. package/src/components/subscription/actions/pause.tsx +83 -0
  157. package/src/components/subscription/items/actions.tsx +31 -0
  158. package/src/components/subscription/items/index.tsx +107 -0
  159. package/src/components/subscription/list.tsx +200 -0
  160. package/src/components/switch.tsx +48 -0
  161. package/src/components/table.tsx +66 -0
  162. package/src/components/uploader.tsx +81 -0
  163. package/src/components/webhook/attempts.tsx +149 -0
  164. package/src/contexts/products.tsx +42 -0
  165. package/src/contexts/session.ts +10 -0
  166. package/src/contexts/settings.tsx +54 -0
  167. package/src/env.d.ts +17 -0
  168. package/src/global.css +97 -0
  169. package/src/hooks/mobile.ts +15 -0
  170. package/src/index.tsx +6 -0
  171. package/src/libs/api.ts +19 -0
  172. package/src/libs/dayjs.ts +17 -0
  173. package/src/libs/util.ts +474 -0
  174. package/src/locales/en.tsx +395 -0
  175. package/src/locales/index.tsx +8 -0
  176. package/src/locales/zh.tsx +389 -0
  177. package/src/pages/admin/billing/index.tsx +56 -0
  178. package/src/pages/admin/billing/invoices/detail.tsx +215 -0
  179. package/src/pages/admin/billing/invoices/index.tsx +5 -0
  180. package/src/pages/admin/billing/subscriptions/detail.tsx +237 -0
  181. package/src/pages/admin/billing/subscriptions/index.tsx +5 -0
  182. package/src/pages/admin/customers/customers/detail.tsx +209 -0
  183. package/src/pages/admin/customers/customers/index.tsx +109 -0
  184. package/src/pages/admin/customers/index.tsx +47 -0
  185. package/src/pages/admin/developers/events/detail.tsx +77 -0
  186. package/src/pages/admin/developers/events/index.tsx +5 -0
  187. package/src/pages/admin/developers/index.tsx +60 -0
  188. package/src/pages/admin/developers/logs.tsx +3 -0
  189. package/src/pages/admin/developers/overview.tsx +3 -0
  190. package/src/pages/admin/developers/webhooks/detail.tsx +109 -0
  191. package/src/pages/admin/developers/webhooks/index.tsx +102 -0
  192. package/src/pages/admin/index.tsx +120 -0
  193. package/src/pages/admin/overview.tsx +3 -0
  194. package/src/pages/admin/payments/index.tsx +65 -0
  195. package/src/pages/admin/payments/intents/detail.tsx +205 -0
  196. package/src/pages/admin/payments/intents/index.tsx +5 -0
  197. package/src/pages/admin/payments/links/create.tsx +141 -0
  198. package/src/pages/admin/payments/links/detail.tsx +318 -0
  199. package/src/pages/admin/payments/links/index.tsx +167 -0
  200. package/src/pages/admin/products/coupons/index.tsx +3 -0
  201. package/src/pages/admin/products/index.tsx +81 -0
  202. package/src/pages/admin/products/prices/actions.tsx +151 -0
  203. package/src/pages/admin/products/prices/detail.tsx +203 -0
  204. package/src/pages/admin/products/prices/list.tsx +95 -0
  205. package/src/pages/admin/products/pricing-tables.tsx +3 -0
  206. package/src/pages/admin/products/products/create.tsx +105 -0
  207. package/src/pages/admin/products/products/detail.tsx +246 -0
  208. package/src/pages/admin/products/products/index.tsx +154 -0
  209. package/src/pages/admin/settings/branding.tsx +3 -0
  210. package/src/pages/admin/settings/business.tsx +3 -0
  211. package/src/pages/admin/settings/index.tsx +47 -0
  212. package/src/pages/admin/settings/payment-methods.tsx +80 -0
  213. package/src/pages/checkout/index.tsx +38 -0
  214. package/src/pages/checkout/pay.tsx +89 -0
  215. package/src/pages/customer/index.tsx +93 -0
  216. package/src/pages/customer/invoice.tsx +147 -0
  217. package/src/pages/home.tsx +9 -0
  218. package/tsconfig.api.json +9 -0
  219. package/tsconfig.eslint.json +7 -0
  220. package/tsconfig.json +99 -0
  221. package/tsconfig.types.json +11 -0
  222. package/vite.config.ts +19 -0
@@ -0,0 +1,205 @@
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 { TPaymentIntentExpanded } from '@did-pay/types';
5
+ import { ArrowBackOutlined, Edit } from '@mui/icons-material';
6
+ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
7
+ import { styled } from '@mui/system';
8
+ import { fromUnitToToken } from '@ocap/util';
9
+ import { useRequest, useSetState } from 'ahooks';
10
+ import { isEmpty } from 'lodash';
11
+ import { Link } from 'react-router-dom';
12
+
13
+ import TxLink from '../../../../components/blockchain/tx';
14
+ import PaymentAmount from '../../../../components/checkout/amount';
15
+ import Copyable from '../../../../components/copyable';
16
+ import Currency from '../../../../components/currency';
17
+ import EventList from '../../../../components/event/list';
18
+ import InfoMetric from '../../../../components/info-metric';
19
+ import InfoRow from '../../../../components/info-row';
20
+ import MetadataEditor from '../../../../components/metadata/editor';
21
+ import PaymentIntentActions from '../../../../components/payment-intent/actions';
22
+ import SectionHeader from '../../../../components/section/header';
23
+ import Status from '../../../../components/status';
24
+ import api from '../../../../libs/api';
25
+ import { formatError, formatTime, getPaymentIntentStatusColor } from '../../../../libs/util';
26
+
27
+ const fetchData = (id: string): Promise<TPaymentIntentExpanded> => {
28
+ return api.get(`/api/payment-intents/${id}`).then((res) => res.data);
29
+ };
30
+
31
+ export default function PaymentIntentDetail(props: { id: string }) {
32
+ const { t } = useLocaleContext();
33
+ const [state, setState] = useSetState({
34
+ adding: {
35
+ price: false,
36
+ },
37
+ editing: {
38
+ metadata: false,
39
+ product: false,
40
+ },
41
+ loading: {
42
+ metadata: false,
43
+ price: false,
44
+ product: false,
45
+ },
46
+ });
47
+
48
+ const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
49
+
50
+ if (error) {
51
+ return <Alert severity="error">{error.message}</Alert>;
52
+ }
53
+
54
+ if (loading || !data) {
55
+ return <CircularProgress />;
56
+ }
57
+
58
+ const createUpdater = (key: string) => async (updates: TPaymentIntentExpanded) => {
59
+ try {
60
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
61
+ await api.put(`/api/payment-intents/${props.id}`, updates).then((res) => res.data);
62
+ Toast.success(t('common.saved'));
63
+ runAsync();
64
+ } catch (err) {
65
+ console.error(err);
66
+ Toast.error(formatError(err));
67
+ } finally {
68
+ setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
69
+ }
70
+ };
71
+
72
+ const onUpdateMetadata = createUpdater('metadata');
73
+
74
+ const currency = data.paymentCurrency;
75
+ const amount = [fromUnitToToken(data?.amount, currency.decimal), currency.symbol].join(' ');
76
+
77
+ return (
78
+ <Root direction="column" spacing={4} mb={4}>
79
+ <Box>
80
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
81
+ <Link to="/admin/payments/intents">
82
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
83
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
84
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
85
+ {t('admin.payments')}
86
+ </Typography>
87
+ </Stack>
88
+ </Link>
89
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
90
+ </Stack>
91
+ <Box mt={2}>
92
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
93
+ <Stack direction="row" alignItems="center">
94
+ <PaymentAmount amount={amount} sx={{ my: 0, fontSize: '2rem', lineHeight: '1rem' }} />
95
+ <Status label={data.status} color={getPaymentIntentStatusColor(data.status)} sx={{ ml: 2 }} />
96
+ </Stack>
97
+ <PaymentIntentActions data={data} variant="normal" />
98
+ </Stack>
99
+ <Stack
100
+ className="section-body"
101
+ direction="row"
102
+ spacing={3}
103
+ justifyContent="flex-start"
104
+ flexWrap="wrap"
105
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
106
+ <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
107
+ <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
108
+ <InfoMetric
109
+ label={t('common.customer')}
110
+ value={<Link to={`/admin/customers/${data.customer.id}`}>{data.customer.name}</Link>}
111
+ />
112
+ </Stack>
113
+ </Box>
114
+ </Box>
115
+ <Box className="section">
116
+ <SectionHeader title={t('admin.details')} />
117
+ <Stack>
118
+ <InfoRow label={t('common.amount')} value={amount} />
119
+ <InfoRow
120
+ label={t('common.status')}
121
+ value={<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />}
122
+ />
123
+ <InfoRow label={t('common.description')} value={data.description} />
124
+ <InfoRow label={t('common.statementDescriptor')} value={data.statement_descriptor} />
125
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
126
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
127
+ </Stack>
128
+ </Box>
129
+ <Box className="section">
130
+ <SectionHeader title={t('admin.paymentMethod.name')} />
131
+ <Stack>
132
+ <InfoRow label={t('common.id')} value={data.paymentMethod.id} />
133
+ <InfoRow label={t('admin.paymentMethod.type')} value={data.paymentMethod.type} />
134
+ <InfoRow
135
+ label={t('admin.paymentMethod.name')}
136
+ value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
137
+ />
138
+ <InfoRow
139
+ label={t('admin.paymentCurrency.name')}
140
+ value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
141
+ />
142
+ <InfoRow
143
+ label={t('common.txHash')}
144
+ value={<TxLink hash={data.payment_details?.tx_hash || data.metadata?.txHash} method={data.paymentMethod} />}
145
+ />
146
+ </Stack>
147
+ </Box>
148
+ <Box className="section">
149
+ <SectionHeader title={t('common.metadata.label')}>
150
+ <Button
151
+ variant="outlined"
152
+ color="inherit"
153
+ size="small"
154
+ disabled={state.editing.metadata}
155
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
156
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
157
+ {t('common.metadata.edit')}
158
+ </Button>
159
+ </SectionHeader>
160
+ <Box className="section-body">
161
+ {!state.editing.metadata &&
162
+ (isEmpty(data.metadata) ? (
163
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
164
+ ) : (
165
+ Object.keys(data.metadata || {}).map((key) => (
166
+ // @ts-ignore
167
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
168
+ ))
169
+ ))}
170
+ {state.editing.metadata && (
171
+ <MetadataEditor
172
+ data={data}
173
+ loading={state.loading.metadata}
174
+ onSave={onUpdateMetadata}
175
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
176
+ />
177
+ )}
178
+ </Box>
179
+ </Box>
180
+ <Box className="section">
181
+ <SectionHeader title={t('admin.connections')} />
182
+ <Stack>
183
+ <InfoRow
184
+ label={t('admin.subscription.name')}
185
+ value={
186
+ data.subscription ? <Link to={`/admin/billing/${data.subscription.id}`}>{data.subscription.id}</Link> : ''
187
+ }
188
+ />
189
+ <InfoRow
190
+ label={t('admin.invoice.name')}
191
+ value={data.invoice_id ? <Link to={`/admin/billing/${data.invoice_id}`}>{data.invoice_id}</Link> : ''}
192
+ />
193
+ </Stack>
194
+ </Box>
195
+ <Box className="section">
196
+ <SectionHeader title={t('admin.events')} />
197
+ <Box className="section-body">
198
+ <EventList features={{ toolbar: false }} object_id={data.id} />
199
+ </Box>
200
+ </Box>
201
+ </Root>
202
+ );
203
+ }
204
+
205
+ const Root = styled(Stack)``;
@@ -0,0 +1,5 @@
1
+ import PaymentList from '../../../../components/payment-intent/list';
2
+
3
+ export default function PaymentsList() {
4
+ return <PaymentList />;
5
+ }
@@ -0,0 +1,141 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Tabs from '@arcblock/ux/lib/Tabs';
4
+ import Toast from '@arcblock/ux/lib/Toast';
5
+ import type { InferFormType, TPaymentLink } from '@did-pay/types';
6
+ import { AddOutlined } from '@mui/icons-material';
7
+ import { Button, Stack, Typography } from '@mui/material';
8
+ import { useState } from 'react';
9
+ import { FormProvider, useForm } from 'react-hook-form';
10
+ import { dispatch } from 'use-bus';
11
+
12
+ import DrawerForm from '../../../../components/drawer-form';
13
+ import AfterPay from '../../../../components/payment-link/after-pay';
14
+ import BeforePay from '../../../../components/payment-link/before-pay';
15
+ import { ProductsProvider } from '../../../../contexts/products';
16
+ import { useSettingsContext } from '../../../../contexts/settings';
17
+ import api from '../../../../libs/api';
18
+ import { formatError } from '../../../../libs/util';
19
+
20
+ type PaymentLink = InferFormType<TPaymentLink> & {
21
+ include_free_trial: boolean;
22
+ };
23
+
24
+ export default function CreatePaymentLink() {
25
+ const { t } = useLocaleContext();
26
+ const [current, setCurrent] = useState('beforePay');
27
+ const { settings } = useSettingsContext();
28
+
29
+ const methods = useForm<PaymentLink>({
30
+ shouldUnregister: false,
31
+ defaultValues: {
32
+ name: '',
33
+ line_items: [],
34
+ currency_id: settings.baseCurrency.id,
35
+ after_completion: {
36
+ type: 'hosted_confirmation',
37
+ hosted_confirmation: {
38
+ custom_message: '',
39
+ },
40
+ redirect: {
41
+ url: '',
42
+ },
43
+ },
44
+ allow_promotion_codes: false,
45
+ customer_creation: 'always',
46
+ consent_collection: {
47
+ promotions: 'none',
48
+ terms_of_service: 'none',
49
+ },
50
+ invoice_creation: {
51
+ enabled: false,
52
+ },
53
+ phone_number_collection: {
54
+ enabled: false,
55
+ },
56
+ billing_address_collection: 'auto',
57
+ include_free_trial: false,
58
+ subscription_data: {
59
+ description: '',
60
+ trial_period_days: 0,
61
+ },
62
+ metadata: [], // FIXME:
63
+ custom_fields: [], // FIXME:
64
+ submit_type: 'pay', // FIXME:
65
+ },
66
+ });
67
+
68
+ const tabs = [
69
+ { label: t('admin.paymentLink.beforePay'), value: 'beforePay', component: BeforePay },
70
+ { label: t('admin.paymentLink.afterPay'), value: 'afterPay', component: AfterPay },
71
+ ];
72
+ const TabComponent = tabs.find((x) => x.value === current)?.component || BeforePay;
73
+
74
+ const onSubmit = (data: TPaymentLink) => {
75
+ if (data.line_items.length === 0) {
76
+ Toast.error(t('admin.paymentLink.noProducts'));
77
+ return;
78
+ }
79
+ if (data.after_completion?.type === 'redirect' && !data.after_completion?.redirect?.url) {
80
+ Toast.error(t('admin.paymentLink.noRedirectUrl'));
81
+ return;
82
+ }
83
+ // @ts-ignore
84
+ if (data.include_free_trial && data.subscription_data?.trial_period_days === 0) {
85
+ Toast.error(t('admin.paymentLink.noSubscriptionTrialDays'));
86
+ return;
87
+ }
88
+
89
+ api
90
+ .post('/api/payment-links', data)
91
+ .then(() => {
92
+ Toast.success(t('admin.paymentLink.saved'));
93
+ methods.reset();
94
+ dispatch('drawer.submitted');
95
+ dispatch('paymentLink.created');
96
+ })
97
+ .catch((err) => {
98
+ console.error(err);
99
+ Toast.error(formatError(err));
100
+ });
101
+ };
102
+
103
+ return (
104
+ <DrawerForm
105
+ icon={<AddOutlined />}
106
+ text={t('admin.paymentLink.add')}
107
+ maxWidth={1280}
108
+ addons={
109
+ // @ts-ignore
110
+ <Button variant="contained" size="small" onClick={methods.handleSubmit(onSubmit)}>
111
+ {t('admin.paymentLink.save')}
112
+ </Button>
113
+ }>
114
+ <FormProvider {...methods}>
115
+ <Stack spacing={3} direction="row" alignItems="flex-start">
116
+ <Stack spacing={2} flex={1} alignItems="flex-start">
117
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
118
+ {t('common.setup')}
119
+ </Typography>
120
+ <Tabs
121
+ tabs={tabs}
122
+ current={current}
123
+ onChange={(v: string) => setCurrent(v)}
124
+ style={{ width: '100%' }}
125
+ scrollButtons="auto"
126
+ />
127
+ <ProductsProvider>
128
+ <TabComponent />
129
+ </ProductsProvider>
130
+ </Stack>
131
+ <Stack flex={2}>
132
+ <Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
133
+ {t('common.preview')}
134
+ </Typography>
135
+ <pre>FIXME</pre>
136
+ </Stack>
137
+ </Stack>
138
+ </FormProvider>
139
+ </DrawerForm>
140
+ );
141
+ }
@@ -0,0 +1,318 @@
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, TPaymentLinkExpanded, TPrice, TProduct } from '@did-pay/types';
5
+ import { ArrowBackOutlined, Edit } from '@mui/icons-material';
6
+ import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
7
+ import { styled } from '@mui/system';
8
+ import { useRequest, useSetState } from 'ahooks';
9
+ import IframeResizer from 'iframe-resizer-react';
10
+ import { isEmpty } from 'lodash';
11
+ import { Link, useNavigate } from 'react-router-dom';
12
+ import { joinURL } from 'ufo';
13
+
14
+ import Copyable from '../../../../components/copyable';
15
+ import EventList from '../../../../components/event/list';
16
+ import InfoCard from '../../../../components/info-card';
17
+ import InfoRow from '../../../../components/info-row';
18
+ import MetadataEditor from '../../../../components/metadata/editor';
19
+ import PaymentLinkActions from '../../../../components/payment-link/actions';
20
+ import AddPrice from '../../../../components/product/add-price';
21
+ import SectionHeader from '../../../../components/section/header';
22
+ import Table from '../../../../components/table';
23
+ import { useSettingsContext } from '../../../../contexts/settings';
24
+ import api from '../../../../libs/api';
25
+ import { formatError, formatPaymentLinkPricing, formatProductPrice, formatTime } from '../../../../libs/util';
26
+
27
+ const fetchData = (id: string): Promise<TPaymentLinkExpanded> => {
28
+ return api.get(`/api/payment-links/${id}`).then((res) => res.data);
29
+ };
30
+
31
+ export default function PaymentLinkDetail(props: { id: string }) {
32
+ const { t, locale } = useLocaleContext();
33
+ const navigate = useNavigate();
34
+ const { settings } = useSettingsContext();
35
+ const [state, setState] = useSetState({
36
+ adding: {
37
+ price: false,
38
+ },
39
+ editing: {
40
+ metadata: false,
41
+ product: false,
42
+ },
43
+ loading: {
44
+ metadata: false,
45
+ price: false,
46
+ product: false,
47
+ },
48
+ });
49
+
50
+ const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
51
+
52
+ if (error) {
53
+ return <Alert severity="error">{error.message}</Alert>;
54
+ }
55
+
56
+ if (loading || !data) {
57
+ return <CircularProgress />;
58
+ }
59
+
60
+ const createUpdater = (key: string) => async (updates: TProduct) => {
61
+ try {
62
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
63
+ await api.put(`/api/payment-links/${props.id}`, updates).then((res) => res.data);
64
+ Toast.success(t('common.saved'));
65
+ runAsync();
66
+ } catch (err) {
67
+ console.error(err);
68
+ Toast.error(formatError(err));
69
+ } finally {
70
+ setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
71
+ }
72
+ };
73
+
74
+ const onAddPrice = async (price: TPrice) => {
75
+ try {
76
+ setState((prev) => ({ loading: { ...prev.loading, price: true } }));
77
+ await api.post('/api/prices', { ...price, product_id: props.id });
78
+ Toast.success(t('common.saved'));
79
+ runAsync();
80
+ } catch (err) {
81
+ console.error(err);
82
+ Toast.error(formatError(err));
83
+ } finally {
84
+ setState((prev) => ({ loading: { ...prev.loading, price: false } }));
85
+ }
86
+ };
87
+
88
+ const onUpdateMetadata = createUpdater('metadata');
89
+ const onChange = (action: string) => {
90
+ if (action === 'remove') {
91
+ navigate('/admin/payments/links');
92
+ } else {
93
+ runAsync();
94
+ }
95
+ };
96
+
97
+ const result = formatPaymentLinkPricing(data, settings.baseCurrency);
98
+
99
+ return (
100
+ <Grid container spacing={4} sx={{ mb: 4 }}>
101
+ <Grid item md={12}>
102
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
103
+ <Link to="/admin/payments/links">
104
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
105
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
106
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
107
+ {t('admin.paymentLinks')}
108
+ </Typography>
109
+ </Stack>
110
+ </Link>
111
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
112
+ </Stack>
113
+ <Box mt={2}>
114
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
115
+ <Stack direction="column" alignItems="flex-start">
116
+ <Stack direction="row" alignItems="center" mb={1}>
117
+ <Typography variant="h5" sx={{ color: 'text.primary', fontWeight: 600 }}>
118
+ {data.name}
119
+ </Typography>
120
+ <Typography sx={{ mx: 1, color: 'text.secondary' }}>for</Typography>
121
+ <Typography variant="h6" sx={{ color: 'text.primary', fontWeight: 500 }}>
122
+ {[result.amount, result.then].filter(Boolean).join(', ')}
123
+ </Typography>
124
+ </Stack>
125
+ <Copyable text={joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pay/${data.id}`)}>
126
+ <Typography variant="h6" sx={{ color: 'text.secondary', mr: 1 }}>
127
+ {joinURL(window.blocklet.prefix, `/checkout/pay/${data.id}`)}
128
+ </Typography>
129
+ </Copyable>
130
+ </Stack>
131
+ <PaymentLinkActions data={data} onChange={onChange} variant="normal" />
132
+ </Stack>
133
+ </Box>
134
+ </Grid>
135
+ <Grid item xs={12} md={5}>
136
+ <Div direction="column" spacing={4}>
137
+ <Box className="section">
138
+ <SectionHeader title={t('admin.products')} />
139
+ <Box className="section-body">
140
+ <Table
141
+ className="link-products-table"
142
+ toolbar={false}
143
+ footer={false}
144
+ locale={locale}
145
+ data={data.line_items}
146
+ columns={[
147
+ {
148
+ label: t('common.name'),
149
+ name: 'name',
150
+ options: {
151
+ sort: false,
152
+ customBodyRenderLite: (_: any, index: number) => {
153
+ const item = data.line_items[index] as TLineItemExpanded;
154
+ return (
155
+ <Link to={`/admin/products/${item.price.product_id}`}>
156
+ <InfoCard
157
+ name={item.price.product.name}
158
+ description={formatProductPrice(
159
+ // @ts-ignore
160
+ { ...item.price.product, prices: [item.price] },
161
+ settings.baseCurrency
162
+ )}
163
+ logo={item.price.product.images[0]}
164
+ />
165
+ </Link>
166
+ );
167
+ },
168
+ },
169
+ },
170
+ {
171
+ label: t('common.quantity'),
172
+ name: 'quantity',
173
+ },
174
+ {
175
+ label: t('admin.paymentLink.adjustable'),
176
+ name: 'price_id',
177
+ options: {
178
+ customBodyRenderLite: (_: any, index: number) => {
179
+ const item = data.line_items[index];
180
+ if (item?.adjustable_quantity?.enabled) {
181
+ return `${t('common.yes')} (${item.adjustable_quantity.minimum} - ${
182
+ item.adjustable_quantity.maximum
183
+ })`;
184
+ }
185
+
186
+ return t('common.no');
187
+ },
188
+ },
189
+ },
190
+ ]}
191
+ options={{
192
+ count: data.line_items.length,
193
+ page: 0,
194
+ rowsPerPage: 20,
195
+ }}
196
+ />
197
+ {state.adding.price && (
198
+ <AddPrice
199
+ loading={state.loading.price}
200
+ onSave={onAddPrice}
201
+ onCancel={() => setState((prev) => ({ adding: { ...prev.adding, price: false } }))}
202
+ />
203
+ )}
204
+ </Box>
205
+ </Box>
206
+ <Box className="section">
207
+ <SectionHeader title={t('admin.details')}>
208
+ <Button
209
+ variant="outlined"
210
+ color="inherit"
211
+ size="small"
212
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
213
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
214
+ {t('common.edit')}
215
+ </Button>
216
+ </SectionHeader>
217
+ <Stack>
218
+ <InfoRow
219
+ label={t('admin.paymentLink.allowPromotionCodes')}
220
+ value={data.allow_promotion_codes ? 'Yes' : 'No'}
221
+ />
222
+ <InfoRow
223
+ label={t('admin.paymentLink.requireBillingAddress')}
224
+ value={data.billing_address_collection ? 'Yes' : 'No'}
225
+ />
226
+ <InfoRow
227
+ label={t('admin.paymentLink.requirePhoneNumber')}
228
+ value={data.phone_number_collection?.enabled ? 'Yes' : 'No'}
229
+ />
230
+ <InfoRow
231
+ label={t('admin.paymentLink.includeFreeTrail')}
232
+ value={data.subscription_data?.trial_period_days ? 'Yes' : 'No'}
233
+ />
234
+ <InfoRow label={t('admin.paymentLink.showConfirmPage')} value={data.after_completion?.type} />
235
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
236
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
237
+ </Stack>
238
+ </Box>
239
+ <Box className="section">
240
+ <SectionHeader title={t('common.metadata.label')}>
241
+ <Button
242
+ variant="outlined"
243
+ color="inherit"
244
+ size="small"
245
+ disabled={state.editing.metadata}
246
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
247
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
248
+ {t('common.metadata.edit')}
249
+ </Button>
250
+ </SectionHeader>
251
+ <Box className="section-body">
252
+ {!state.editing.metadata &&
253
+ (isEmpty(data.metadata) ? (
254
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
255
+ ) : (
256
+ <Grid container>
257
+ <Grid item xs={12} md={6}>
258
+ {Object.keys(data.metadata || {}).map((key) => (
259
+ // @ts-ignore
260
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
261
+ ))}
262
+ </Grid>
263
+ </Grid>
264
+ ))}
265
+ {state.editing.metadata && (
266
+ <MetadataEditor
267
+ data={data}
268
+ loading={state.loading.metadata}
269
+ onSave={onUpdateMetadata}
270
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
271
+ />
272
+ )}
273
+ </Box>
274
+ </Box>
275
+ <Box className="section">
276
+ <SectionHeader title={t('admin.events')} />
277
+ <Box className="section-body">
278
+ <EventList features={{ toolbar: false }} object_id={data.id} />
279
+ </Box>
280
+ </Box>
281
+ </Div>
282
+ </Grid>
283
+ <Grid item xs={12} md={7}>
284
+ <Div>
285
+ <Box className="section">
286
+ <SectionHeader title={t('common.preview')} />
287
+ <Box className="section-body">
288
+ <Chrome>
289
+ <IframeResizer
290
+ style={{
291
+ width: '1px',
292
+ minWidth: '840px',
293
+ minHeight: '840px',
294
+ transform: 'scale(0.65)',
295
+ transformOrigin: '20% 10%',
296
+ border: 'none',
297
+ }}
298
+ src={`${window.blocklet.prefix}checkout/pay/${data.id}?preview=1`}
299
+ />
300
+ </Chrome>
301
+ </Box>
302
+ </Box>
303
+ </Div>
304
+ </Grid>
305
+ </Grid>
306
+ );
307
+ }
308
+
309
+ const Div = styled(Stack)``;
310
+
311
+ const Chrome = styled(Box)`
312
+ background-color: #fcfeff;
313
+ border-radius: 8px;
314
+ margin-top: 40px;
315
+ position: relative;
316
+ overflow: hidden;
317
+ box-shadow: 0 20px 44px #32325d1f, 0 -1px 32px #32325d0f, 0 3px 12px #00000014;
318
+ `;