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,237 @@
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 { TProduct, TSubscriptionExpanded } 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 { useRequest, useSetState } from 'ahooks';
9
+ import { isEmpty } from 'lodash';
10
+ import { Link } from 'react-router-dom';
11
+
12
+ import TxLink from '../../../../components/blockchain/tx';
13
+ import Copyable from '../../../../components/copyable';
14
+ import Currency from '../../../../components/currency';
15
+ import EventList from '../../../../components/event/list';
16
+ import InfoMetric from '../../../../components/info-metric';
17
+ import InfoRow from '../../../../components/info-row';
18
+ import InvoiceList from '../../../../components/invoice/list';
19
+ import MetadataEditor from '../../../../components/metadata/editor';
20
+ import SectionHeader from '../../../../components/section/header';
21
+ import Status from '../../../../components/status';
22
+ import SubscriptionActions from '../../../../components/subscription/actions';
23
+ import SubscriptionItemList from '../../../../components/subscription/items';
24
+ import api from '../../../../libs/api';
25
+ import { formatError, formatSubscriptionProduct, formatTime, getSubscriptionStatusColor } from '../../../../libs/util';
26
+
27
+ const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
28
+ return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
29
+ };
30
+
31
+ export default function SubscriptionDetail(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: TProduct) => {
59
+ try {
60
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
61
+ await api.put(`/api/subscriptions/${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
+ return (
75
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
76
+ <Box>
77
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
78
+ <Link to="/admin/billing/subscriptions">
79
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
80
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
81
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
82
+ {t('admin.subscriptions')}
83
+ </Typography>
84
+ </Stack>
85
+ </Link>
86
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
87
+ </Stack>
88
+ <Box mt={2}>
89
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
90
+ <Stack direction="row" alignItems="center">
91
+ <Typography variant="h5" sx={{ fontWeight: 600 }}>
92
+ {data.customer.name}
93
+ </Typography>
94
+ <Typography sx={{ mx: 1 }}>on</Typography>
95
+ <Typography variant="h5" sx={{ fontWeight: 600 }}>
96
+ {formatSubscriptionProduct(data.items)}
97
+ </Typography>
98
+ <Status label={data.status} color={getSubscriptionStatusColor(data.status)} sx={{ ml: 1 }} />
99
+ </Stack>
100
+ <SubscriptionActions data={data} onChange={runAsync} variant="normal" />
101
+ </Stack>
102
+ <Stack
103
+ className="section-body"
104
+ direction="row"
105
+ spacing={3}
106
+ justifyContent="flex-start"
107
+ flexWrap="wrap"
108
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
109
+ <InfoMetric
110
+ label={t('admin.subscription.startedAt')}
111
+ value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
112
+ divider
113
+ />
114
+ {!data.cancel_at && (
115
+ <InfoMetric
116
+ label={t('admin.subscription.nextInvoice')}
117
+ value={formatTime(data.current_period_end * 1000)}
118
+ divider
119
+ />
120
+ )}
121
+ {data.cancel_at && (
122
+ <InfoMetric
123
+ label={t('admin.subscription.cancel.schedule')}
124
+ value={formatTime(data.cancel_at * 1000)}
125
+ divider
126
+ />
127
+ )}
128
+ </Stack>
129
+ </Box>
130
+ </Box>
131
+
132
+ <Box className="section">
133
+ <SectionHeader title={t('admin.details')}>
134
+ <Button
135
+ variant="outlined"
136
+ color="inherit"
137
+ size="small"
138
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
139
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
140
+ {t('common.edit')}
141
+ </Button>
142
+ </SectionHeader>
143
+ <Stack>
144
+ <InfoRow
145
+ label={t('common.customer')}
146
+ value={<Link to={`/admin/customers/${data.customer.id}`}>{data.customer.name}</Link>}
147
+ />
148
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
149
+ {data.status === 'paused' && !!data.pause_collection?.resumes_at && (
150
+ <InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
151
+ )}
152
+ <InfoRow
153
+ label={t('admin.subscription.currentPeriod')}
154
+ value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
155
+ ' ~ '
156
+ )}
157
+ />
158
+ <InfoRow
159
+ label={t('admin.subscription.trialingPeriod')}
160
+ value={
161
+ data.trail_end && data.trail_start
162
+ ? [formatTime(data.trail_start * 1000), formatTime(data.trail_end * 1000)].join(' ~ ')
163
+ : ''
164
+ }
165
+ />
166
+ <InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
167
+ <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
168
+ <InfoRow
169
+ label={t('admin.paymentMethod.name')}
170
+ value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
171
+ />
172
+ <InfoRow
173
+ label={t('admin.paymentCurrency.name')}
174
+ value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
175
+ />
176
+ {data.payment_details?.tx_hash && (
177
+ <InfoRow
178
+ label={t('common.txHash')}
179
+ value={<TxLink hash={data.payment_details?.tx_hash} method={data.paymentMethod} />}
180
+ />
181
+ )}
182
+ </Stack>
183
+ </Box>
184
+ <Box className="section">
185
+ <SectionHeader title={t('common.metadata.label')}>
186
+ <Button
187
+ variant="outlined"
188
+ color="inherit"
189
+ size="small"
190
+ disabled={state.editing.metadata}
191
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
192
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
193
+ {t('common.metadata.edit')}
194
+ </Button>
195
+ </SectionHeader>
196
+ <Box className="section-body">
197
+ {!state.editing.metadata &&
198
+ (isEmpty(data.metadata) ? (
199
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
200
+ ) : (
201
+ Object.keys(data.metadata || {}).map((key) => (
202
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
203
+ ))
204
+ ))}
205
+ {state.editing.metadata && (
206
+ <MetadataEditor
207
+ data={data}
208
+ loading={state.loading.metadata}
209
+ onSave={onUpdateMetadata}
210
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
211
+ />
212
+ )}
213
+ </Box>
214
+ </Box>
215
+ <Box className="section">
216
+ <SectionHeader title={t('admin.product.pricing')} mb={0} />
217
+ <Box className="section-body">
218
+ <SubscriptionItemList data={data.items} currency={data.paymentCurrency} />
219
+ </Box>
220
+ </Box>
221
+ <Box className="section">
222
+ <SectionHeader title={t('admin.invoices')} mb={0} />
223
+ <Box className="section-body">
224
+ <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
225
+ </Box>
226
+ </Box>
227
+ <Box className="section">
228
+ <SectionHeader title={t('admin.events')} />
229
+ <Box className="section-body">
230
+ <EventList features={{ toolbar: false }} object_id={data.id} />
231
+ </Box>
232
+ </Box>
233
+ </Root>
234
+ );
235
+ }
236
+
237
+ const Root = styled(Stack)``;
@@ -0,0 +1,5 @@
1
+ import SubscriptionList from '../../../../components/subscription/list';
2
+
3
+ export default function SubscriptionsList() {
4
+ return <SubscriptionList />;
5
+ }
@@ -0,0 +1,209 @@
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 { TCustomerExpanded } 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 { useRequest, useSetState } from 'ahooks';
9
+ import { isEmpty } from 'lodash';
10
+ import { Link, useNavigate } from 'react-router-dom';
11
+
12
+ import Copyable from '../../../../components/copyable';
13
+ import CustomerActions from '../../../../components/customer/actions';
14
+ import EventList from '../../../../components/event/list';
15
+ import InfoMetric from '../../../../components/info-metric';
16
+ import InfoRow from '../../../../components/info-row';
17
+ import InvoiceList from '../../../../components/invoice/list';
18
+ import MetadataEditor from '../../../../components/metadata/editor';
19
+ import PaymentList from '../../../../components/payment-intent/list';
20
+ import SectionHeader from '../../../../components/section/header';
21
+ import SubscriptionList from '../../../../components/subscription/list';
22
+ import api from '../../../../libs/api';
23
+ import { formatError, formatTime } from '../../../../libs/util';
24
+
25
+ const fetchData = (id: string): Promise<TCustomerExpanded> => {
26
+ return api.get(`/api/customers/${id}`).then((res) => res.data);
27
+ };
28
+
29
+ export default function CustomerDetail(props: { id: string }) {
30
+ const { t } = useLocaleContext();
31
+ const navigate = useNavigate();
32
+ const [state, setState] = useSetState({
33
+ adding: {
34
+ price: false,
35
+ },
36
+ editing: {
37
+ metadata: false,
38
+ product: false,
39
+ },
40
+ loading: {
41
+ metadata: false,
42
+ price: false,
43
+ product: false,
44
+ },
45
+ });
46
+
47
+ const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
48
+
49
+ if (error) {
50
+ return <Alert severity="error">{error.message}</Alert>;
51
+ }
52
+
53
+ if (loading || !data) {
54
+ return <CircularProgress />;
55
+ }
56
+
57
+ const createUpdater = (key: string) => async (updates: TCustomerExpanded) => {
58
+ try {
59
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
60
+ await api.put(`/api/customers/${props.id}`, updates).then((res) => res.data);
61
+ Toast.success(t('common.saved'));
62
+ runAsync();
63
+ } catch (err) {
64
+ console.error(err);
65
+ Toast.error(formatError(err));
66
+ } finally {
67
+ setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
68
+ }
69
+ };
70
+
71
+ const onUpdateMetadata = createUpdater('metadata');
72
+ const onChange = (action: string) => {
73
+ if (action === 'remove') {
74
+ navigate('/admin/customers');
75
+ } else {
76
+ runAsync();
77
+ }
78
+ };
79
+
80
+ return (
81
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
82
+ <Box>
83
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
84
+ <Link to="/admin/customers">
85
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
86
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
87
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
88
+ {t('admin.customers')}
89
+ </Typography>
90
+ </Stack>
91
+ </Link>
92
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
93
+ </Stack>
94
+ <Box mt={2}>
95
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
96
+ <Typography variant="h5" sx={{ fontWeight: 600 }}>
97
+ {data.name}
98
+ </Typography>
99
+ <CustomerActions data={data} onChange={onChange} variant="normal" />
100
+ </Stack>
101
+ <Stack
102
+ className="section-body"
103
+ direction="row"
104
+ spacing={3}
105
+ justifyContent="flex-start"
106
+ flexWrap="wrap"
107
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
108
+ <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
109
+ <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
110
+ <InfoMetric label={t('admin.customer.spent')} value={0} divider />
111
+ <InfoMetric label={t('admin.customer.refund')} value={0} divider />
112
+ <InfoMetric label={t('admin.customer.dispute')} value={0} divider />
113
+ </Stack>
114
+ </Box>
115
+ </Box>
116
+
117
+ <Box className="section">
118
+ <SectionHeader title={t('admin.details')}>
119
+ <Button
120
+ variant="outlined"
121
+ color="inherit"
122
+ size="small"
123
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
124
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
125
+ {t('common.edit')}
126
+ </Button>
127
+ </SectionHeader>
128
+ <Stack>
129
+ <InfoRow label={t('admin.customer.name')} value={data.name} />
130
+ <InfoRow label={t('admin.customer.phone')} value={data.phone} />
131
+ <InfoRow label={t('admin.customer.email')} value={data.email} />
132
+ <InfoRow label={t('admin.customer.invoicePrefix')} value={data.invoice_prefix} />
133
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
134
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
135
+ <InfoRow
136
+ label={t('admin.customer.address.label')}
137
+ value={
138
+ <Stack direction="column">
139
+ <InfoRow label={t('admin.customer.address.country')} value={data.address?.country} />
140
+ <InfoRow label={t('admin.customer.address.state')} value={data.address?.state} />
141
+ <InfoRow label={t('admin.customer.address.city')} value={data.address?.city} />
142
+ <InfoRow label={t('admin.customer.address.line1')} value={data.address?.line1} />
143
+ <InfoRow label={t('admin.customer.address.line2')} value={data.address?.line2} />
144
+ <InfoRow label={t('admin.customer.address.postal_code')} value={data.address?.postal_code} />
145
+ </Stack>
146
+ }
147
+ />
148
+ </Stack>
149
+ </Box>
150
+ <Box className="section">
151
+ <SectionHeader title={t('admin.subscriptions')} mb={0} />
152
+ <Box className="section-body">
153
+ <SubscriptionList features={{ customer: false, toolbar: false }} customer_id={data.id} />
154
+ </Box>
155
+ </Box>
156
+ <Box className="section">
157
+ <SectionHeader title={t('admin.payments')} mb={0} />
158
+ <Box className="section-body">
159
+ <PaymentList features={{ customer: false, toolbar: false }} customer_id={data.id} />
160
+ </Box>
161
+ </Box>
162
+ <Box className="section">
163
+ <SectionHeader title={t('admin.invoices')} mb={0} />
164
+ <Box className="section-body">
165
+ <InvoiceList features={{ customer: false, toolbar: false }} customer_id={data.id} />
166
+ </Box>
167
+ </Box>
168
+ <Box className="section">
169
+ <SectionHeader title={t('common.metadata.label')}>
170
+ <Button
171
+ variant="outlined"
172
+ color="inherit"
173
+ size="small"
174
+ disabled={state.editing.metadata}
175
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
176
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
177
+ {t('common.metadata.edit')}
178
+ </Button>
179
+ </SectionHeader>
180
+ <Box className="section-body">
181
+ {!state.editing.metadata &&
182
+ (isEmpty(data.metadata) ? (
183
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
184
+ ) : (
185
+ Object.keys(data.metadata || {}).map((key) => (
186
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
187
+ ))
188
+ ))}
189
+ {state.editing.metadata && (
190
+ <MetadataEditor
191
+ data={data}
192
+ loading={state.loading.metadata}
193
+ onSave={onUpdateMetadata}
194
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
195
+ />
196
+ )}
197
+ </Box>
198
+ </Box>
199
+ <Box className="section">
200
+ <SectionHeader title={t('admin.events')} />
201
+ <Box className="section-body">
202
+ <EventList features={{ toolbar: false }} object_id={data.id} />
203
+ </Box>
204
+ </Box>
205
+ </Root>
206
+ );
207
+ }
208
+
209
+ const Root = styled(Stack)``;
@@ -0,0 +1,109 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
+ import DidAddress from '@arcblock/ux/lib/DID';
4
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
+ import type { TCustomer } from '@did-pay/types';
6
+ import { Alert, CircularProgress } from '@mui/material';
7
+ import { useRequest } from 'ahooks';
8
+ import { useEffect, useState } from 'react';
9
+ import { useNavigate } from 'react-router-dom';
10
+
11
+ import Table from '../../../../components/table';
12
+ import api from '../../../../libs/api';
13
+ import { formatTime } from '../../../../libs/util';
14
+
15
+ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TCustomer[]; count: number }> => {
16
+ const search = new URLSearchParams();
17
+ Object.keys(params).forEach((key) => {
18
+ search.set(key, String(params[key]));
19
+ });
20
+ return api.get(`/api/customers?${search.toString()}`).then((res) => res.data);
21
+ };
22
+
23
+ export default function CustomersList() {
24
+ const listKey = 'customers';
25
+ const persisted = getDurableData(listKey);
26
+
27
+ const { t } = useLocaleContext();
28
+ const navigate = useNavigate();
29
+ const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
30
+ active: '',
31
+ pageSize: persisted.rowsPerPage || 20,
32
+ page: persisted.page ? persisted.page + 1 : 1,
33
+ });
34
+
35
+ const { loading, error, data, refresh } = useRequest(() => fetchData(search));
36
+ useEffect(() => {
37
+ refresh();
38
+ }, [search, refresh]);
39
+
40
+ if (error) {
41
+ return <Alert severity="error">{error.message}</Alert>;
42
+ }
43
+
44
+ if (loading || !data) {
45
+ return <CircularProgress />;
46
+ }
47
+
48
+ const columns = [
49
+ {
50
+ label: t('admin.product.name.label'),
51
+ name: 'name',
52
+ },
53
+ {
54
+ label: t('common.did'),
55
+ name: 'did',
56
+ options: {
57
+ customBodyRenderLite: (_: string, index: number) => {
58
+ const item = data.list[index] as TCustomer;
59
+ return <DidAddress did={item.did} />;
60
+ },
61
+ },
62
+ },
63
+ {
64
+ label: t('common.email'),
65
+ name: 'email',
66
+ },
67
+ {
68
+ label: t('admin.customer.phone'),
69
+ name: 'phone',
70
+ },
71
+ {
72
+ label: t('common.createdAt'),
73
+ name: 'created_at',
74
+ options: {
75
+ customBodyRender: (e: string) => {
76
+ return formatTime(e);
77
+ },
78
+ },
79
+ },
80
+ ];
81
+
82
+ const onTableChange = ({ page, rowsPerPage }: any) => {
83
+ if (search.pageSize !== rowsPerPage) {
84
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
85
+ } else if (search.page !== page + 1) {
86
+ setSearch((x) => ({ ...x, page: page + 1 }));
87
+ }
88
+ };
89
+
90
+ return (
91
+ <Table
92
+ durable={listKey}
93
+ durableKeys={['page', 'rowsPerPage']}
94
+ data={data.list}
95
+ columns={columns}
96
+ options={{
97
+ count: data.count,
98
+ page: search.page - 1,
99
+ rowsPerPage: search.pageSize,
100
+ onRowClick: (_: any, { dataIndex }: any) => {
101
+ const item = data.list[dataIndex] as TCustomer;
102
+ navigate(`/admin/customers/${item.id}`);
103
+ },
104
+ }}
105
+ loading={loading}
106
+ onChange={onTableChange}
107
+ />
108
+ );
109
+ }
@@ -0,0 +1,47 @@
1
+ import Center from '@arcblock/ux/lib/Center';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Tabs from '@arcblock/ux/lib/Tabs';
4
+ import { CircularProgress, Typography } from '@mui/material';
5
+ import React, { Suspense, isValidElement } from 'react';
6
+ import { useNavigate, useParams } from 'react-router-dom';
7
+
8
+ const CustomerDetail = React.lazy(() => import('./customers/detail'));
9
+
10
+ const pages = {
11
+ overview: React.lazy(() => import('./customers')),
12
+ };
13
+
14
+ export default function CustomerIndex() {
15
+ const navigate = useNavigate();
16
+ const { t } = useLocaleContext();
17
+ const { page = 'overview' } = useParams();
18
+
19
+ if (page.startsWith('cus_')) {
20
+ return <CustomerDetail id={page} />;
21
+ }
22
+
23
+ const onTabChange = (newTab: string) => {
24
+ navigate(`/admin/customers/${newTab}`);
25
+ };
26
+
27
+ // @ts-ignore
28
+ const TabComponent = pages[page] || pages.overview;
29
+ const tabs = [{ label: t('admin.overview'), value: 'overview' }];
30
+
31
+ return (
32
+ <div>
33
+ <Typography variant="h5" sx={{ mb: 1, fontWeight: 600 }}>
34
+ {t('admin.customers')}
35
+ </Typography>
36
+ <Tabs tabs={tabs} current={page} onChange={onTabChange} scrollButtons="auto" />
37
+ <Suspense
38
+ fallback={
39
+ <Center relative="parent">
40
+ <CircularProgress />
41
+ </Center>
42
+ }>
43
+ {isValidElement(TabComponent) ? TabComponent : <TabComponent />}
44
+ </Suspense>
45
+ </div>
46
+ );
47
+ }