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,95 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Stack, Typography } from '@mui/material';
4
+ import { Link } from 'react-router-dom';
5
+
6
+ import Copyable from '../../../../components/copyable';
7
+ import type { Product } from '../../../../components/product/form';
8
+ import Status from '../../../../components/status';
9
+ import Table from '../../../../components/table';
10
+ import { useSettingsContext } from '../../../../contexts/settings';
11
+ import { formatPrice, formatTime } from '../../../../libs/util';
12
+ import PriceActions from './actions';
13
+
14
+ export default function PricesList({ product, onChange }: { product: Product; onChange: Function }) {
15
+ const { t } = useLocaleContext();
16
+ const { settings } = useSettingsContext();
17
+
18
+ const columns = [
19
+ {
20
+ label: t('admin.price.name'),
21
+ name: 'name',
22
+ options: {
23
+ sort: false,
24
+ customBodyRenderLite: (_: any, index: number) => {
25
+ const price = product.prices[index] as any;
26
+ return (
27
+ <Link to={`/admin/products/${price.id}`} color="text.primary">
28
+ <Stack direction="row" alignItems="center" spacing={1}>
29
+ <Typography component="span">{formatPrice(price, settings.baseCurrency)}</Typography>
30
+ <Typography component="span">
31
+ {price.id === product.default_price_id && <Status label="default" color="info" sx={{ height: 18 }} />}
32
+ </Typography>
33
+ </Stack>
34
+ </Link>
35
+ );
36
+ },
37
+ },
38
+ },
39
+ {
40
+ label: t('common.id'),
41
+ name: 'id',
42
+ options: {
43
+ sort: false,
44
+ customBodyRender: (id: string) => {
45
+ return <Copyable text={id} />;
46
+ },
47
+ },
48
+ },
49
+ {
50
+ label: t('common.createdAt'),
51
+ name: 'created_at',
52
+ options: {
53
+ customBodyRender: (e: string) => {
54
+ return formatTime(e);
55
+ },
56
+ },
57
+ },
58
+ {
59
+ label: t('common.updatedAt'),
60
+ name: 'updated_at',
61
+ options: {
62
+ customBodyRender: (e: string) => {
63
+ return formatTime(e);
64
+ },
65
+ },
66
+ },
67
+ {
68
+ label: t('common.actions'),
69
+ name: 'id',
70
+ width: 100,
71
+ align: 'center',
72
+ options: {
73
+ sort: false,
74
+ customBodyRenderLite: (_: any, index: number) => {
75
+ const price = product.prices[index] as any;
76
+ return <PriceActions data={price} onChange={onChange} setAsDefault={price.id !== product.default_price_id} />;
77
+ },
78
+ },
79
+ },
80
+ ];
81
+
82
+ return (
83
+ <Table
84
+ toolbar={false}
85
+ footer={false}
86
+ data={product.prices}
87
+ columns={columns}
88
+ options={{
89
+ count: product.prices.length,
90
+ page: 0,
91
+ rowsPerPage: 20,
92
+ }}
93
+ />
94
+ );
95
+ }
@@ -0,0 +1,3 @@
1
+ export default function PricingTables() {
2
+ return <div>PricingTables</div>;
3
+ }
@@ -0,0 +1,105 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import { AddOutlined } from '@mui/icons-material';
5
+ import { Box, Button, Divider, Typography } from '@mui/material';
6
+ import { cloneDeep } from 'lodash';
7
+ import { Fragment } from 'react';
8
+ import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
9
+ import { dispatch } from 'use-bus';
10
+
11
+ import Collapse from '../../../../components/collapse';
12
+ import DrawerForm from '../../../../components/drawer-form';
13
+ import PriceActions from '../../../../components/price/actions';
14
+ import PriceForm, { DEFAULT_PRICE } from '../../../../components/price/form';
15
+ import ProductForm, { Product } from '../../../../components/product/form';
16
+ import { useSettingsContext } from '../../../../contexts/settings';
17
+ import api from '../../../../libs/api';
18
+ import { formatError, formatPrice } from '../../../../libs/util';
19
+
20
+ export default function ProductsCreate() {
21
+ const { t } = useLocaleContext();
22
+ const { settings } = useSettingsContext();
23
+
24
+ const methods = useForm<Product>({
25
+ defaultValues: {
26
+ type: 'service',
27
+ name: '',
28
+ description: '',
29
+ images: [],
30
+ statement_descriptor: '',
31
+ unit_label: '',
32
+ features: [],
33
+ prices: [{ ...DEFAULT_PRICE, currency_id: settings.baseCurrency.id }],
34
+ metadata: [],
35
+ },
36
+ });
37
+ const { control, handleSubmit } = methods;
38
+
39
+ const prices = useFieldArray({ control, name: 'prices' });
40
+ const getPrice = (index: number) => methods.getValues().prices[index];
41
+
42
+ const onSubmit = (data: Product) => {
43
+ api
44
+ .post('/api/products', data)
45
+ .then(() => {
46
+ Toast.success(t('admin.product.saved'));
47
+ methods.reset();
48
+ dispatch('drawer.submitted');
49
+ dispatch('project.created');
50
+ })
51
+ .catch((err) => {
52
+ console.error(err);
53
+ Toast.error(formatError(err));
54
+ });
55
+ };
56
+
57
+ return (
58
+ <DrawerForm
59
+ icon={<AddOutlined />}
60
+ text={t('admin.product.add')}
61
+ maxWidth={640}
62
+ addons={
63
+ <Button variant="contained" size="small" onClick={handleSubmit(onSubmit)}>
64
+ {t('admin.product.save')}
65
+ </Button>
66
+ }>
67
+ <FormProvider {...methods}>
68
+ <Typography variant="h6" sx={{ mb: 3, fontWeight: 600 }}>
69
+ {t('admin.product.info')}
70
+ </Typography>
71
+ <ProductForm />
72
+ <Typography variant="h6" sx={{ mt: 5, mb: 3, fontWeight: 600 }}>
73
+ {t('admin.price.info')}
74
+ </Typography>
75
+ <Box>
76
+ {prices.fields.map((price, index) => (
77
+ <Fragment key={price.id}>
78
+ <Collapse
79
+ expanded
80
+ style={{ fontWeight: 'bold', width: '50%' }}
81
+ addons={<PriceActions onDuplicate={() => prices.append(price)} onRemove={() => prices.remove(index)} />}
82
+ trigger={(expanded: boolean) =>
83
+ // @ts-ignore
84
+ expanded ? t('admin.price.detail') : formatPrice(getPrice(index), settings.baseCurrency)
85
+ }>
86
+ <PriceForm prefix={`prices.${index}`} />
87
+ </Collapse>
88
+ <Divider sx={{ mt: 2, mb: 4 }} />
89
+ </Fragment>
90
+ ))}
91
+ <Box mt={1}>
92
+ <Button
93
+ size="small"
94
+ variant="outlined"
95
+ color="inherit"
96
+ // @ts-ignore
97
+ onClick={() => prices.append({ ...cloneDeep(DEFAULT_PRICE), currency_id: settings.baseCurrency.id })}>
98
+ <AddOutlined fontSize="small" /> Add another price
99
+ </Button>
100
+ </Box>
101
+ </Box>
102
+ </FormProvider>
103
+ </DrawerForm>
104
+ );
105
+ }
@@ -0,0 +1,246 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TPrice, TProduct, TProductExpanded } from '@did-pay/types';
4
+ import { ArrowBackOutlined, Edit } from '@mui/icons-material';
5
+ import { Alert, AlertTitle, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
6
+ import { styled } from '@mui/system';
7
+ import { useRequest, useSetState } from 'ahooks';
8
+ import { isEmpty } from 'lodash';
9
+ import { Link, useNavigate } from 'react-router-dom';
10
+
11
+ import Copyable from '../../../../components/copyable';
12
+ import EventList from '../../../../components/event/list';
13
+ import InfoCard from '../../../../components/info-card';
14
+ import InfoMetric from '../../../../components/info-metric';
15
+ import InfoRow from '../../../../components/info-row';
16
+ import MetadataEditor from '../../../../components/metadata/editor';
17
+ import ProductActions from '../../../../components/product/actions';
18
+ import AddPrice from '../../../../components/product/add-price';
19
+ import EditProduct from '../../../../components/product/edit';
20
+ import SectionHeader from '../../../../components/section/header';
21
+ import { useSettingsContext } from '../../../../contexts/settings';
22
+ import api from '../../../../libs/api';
23
+ import { formatError, formatProductPrice, formatTime } from '../../../../libs/util';
24
+ import PricesList from '../prices/list';
25
+
26
+ const getProduct = (id: string): Promise<TProductExpanded> => {
27
+ return api.get(`/api/products/${id}`).then((res) => res.data);
28
+ };
29
+
30
+ export default function ProductDetail(props: { id: string }) {
31
+ const { t } = useLocaleContext();
32
+ const navigate = useNavigate();
33
+ const { settings } = useSettingsContext();
34
+ const [state, setState] = useSetState({
35
+ adding: {
36
+ price: false,
37
+ },
38
+ editing: {
39
+ metadata: false,
40
+ product: false,
41
+ },
42
+ loading: {
43
+ metadata: false,
44
+ price: false,
45
+ product: false,
46
+ },
47
+ });
48
+
49
+ const { loading, error, data, runAsync } = useRequest(() => getProduct(props.id));
50
+
51
+ if (error) {
52
+ return <Alert severity="error">{error.message}</Alert>;
53
+ }
54
+
55
+ if (loading || !data) {
56
+ return <CircularProgress />;
57
+ }
58
+
59
+ const createProductUpdater = (key: string) => async (updates: TProduct) => {
60
+ try {
61
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
62
+ await api.put(`/api/products/${props.id}`, updates).then((res) => res.data);
63
+ Toast.success(t('common.saved'));
64
+ runAsync();
65
+ } catch (err) {
66
+ console.error(err);
67
+ Toast.error(formatError(err));
68
+ } finally {
69
+ setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
70
+ }
71
+ };
72
+
73
+ const onAddPrice = async (price: TPrice) => {
74
+ try {
75
+ setState((prev) => ({ loading: { ...prev.loading, price: true } }));
76
+ await api.post('/api/prices', { ...price, product_id: props.id });
77
+ Toast.success(t('common.saved'));
78
+ runAsync();
79
+ } catch (err) {
80
+ console.error(err);
81
+ Toast.error(formatError(err));
82
+ } finally {
83
+ setState((prev) => ({ loading: { ...prev.loading, price: false } }));
84
+ }
85
+ };
86
+
87
+ const onUpdateMetadata = createProductUpdater('metadata');
88
+ const onUpdateAll = createProductUpdater('product');
89
+ const onChange = (action: string) => {
90
+ if (action === 'remove') {
91
+ navigate('/admin/products');
92
+ } else {
93
+ runAsync();
94
+ }
95
+ };
96
+
97
+ return (
98
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
99
+ {data.active === false && (
100
+ <Alert severity="warning">
101
+ <AlertTitle>{t('admin.product.archived')}</AlertTitle>
102
+ {t('admin.product.archivedTip')}
103
+ </Alert>
104
+ )}
105
+ <Box>
106
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
107
+ <Link to="/admin/products">
108
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
109
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
110
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
111
+ {t('admin.products')}
112
+ </Typography>
113
+ </Stack>
114
+ </Link>
115
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
116
+ </Stack>
117
+ <Box mt={2}>
118
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
119
+ <InfoCard
120
+ logo={data.images[0]}
121
+ name={data.name}
122
+ // @ts-ignore
123
+ description={formatProductPrice(data, settings.baseCurrency)}
124
+ />
125
+ <ProductActions data={data} onChange={onChange} variant="normal" />
126
+ </Stack>
127
+ <Stack
128
+ className="section-body"
129
+ direction="row"
130
+ spacing={3}
131
+ justifyContent="flex-start"
132
+ flexWrap="wrap"
133
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
134
+ <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
135
+ <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
136
+ </Stack>
137
+ </Box>
138
+ </Box>
139
+ <Box className="section">
140
+ <SectionHeader title={t('admin.details')}>
141
+ <Button
142
+ variant="outlined"
143
+ color="inherit"
144
+ size="small"
145
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, product: true } }))}>
146
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
147
+ {t('common.edit')}
148
+ </Button>
149
+ </SectionHeader>
150
+ <Grid className="section-body" container>
151
+ <Grid item xs={12} md={6}>
152
+ <InfoRow label={t('admin.product.name.label')} value={data.name} />
153
+ <InfoRow label={t('admin.product.description.label')} value={data.description} />
154
+ <InfoRow label={t('admin.product.statement_descriptor.label')} value={data.statement_descriptor} />
155
+ <InfoRow label={t('admin.product.unit_label.label')} value={data.unit_label} />
156
+ <InfoRow label={t('admin.product.features.label')} value={data.features.map((x) => x.name).join(',')} />
157
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
158
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
159
+ </Grid>
160
+ <Grid item xs={12} md={6}>
161
+ <InfoRow
162
+ label={t('admin.product.image.label')}
163
+ value={
164
+ data.images.length ? <img src={data.images[0]} width={160} height={160} alt={data.name} /> : 'No Image'
165
+ }
166
+ />
167
+ </Grid>
168
+ {state.editing.product && (
169
+ <EditProduct
170
+ product={data}
171
+ loading={state.loading.product}
172
+ onSave={onUpdateAll}
173
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, product: false } }))}
174
+ />
175
+ )}
176
+ </Grid>
177
+ </Box>
178
+ <Box className="section">
179
+ <SectionHeader title={t('admin.product.pricing')}>
180
+ <Button
181
+ variant="outlined"
182
+ color="inherit"
183
+ size="small"
184
+ onClick={() => setState((prev) => ({ adding: { ...prev.adding, price: true } }))}>
185
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
186
+ {t('admin.price.add')}
187
+ </Button>
188
+ </SectionHeader>
189
+ <Box className="section-body">
190
+ <PricesList product={data as any} onChange={runAsync} />
191
+ {state.adding.price && (
192
+ <AddPrice
193
+ loading={state.loading.price}
194
+ onSave={onAddPrice}
195
+ onCancel={() => setState((prev) => ({ adding: { ...prev.adding, price: false } }))}
196
+ />
197
+ )}
198
+ </Box>
199
+ </Box>
200
+ <Box className="section">
201
+ <SectionHeader title={t('common.metadata.label')}>
202
+ <Button
203
+ variant="outlined"
204
+ color="inherit"
205
+ size="small"
206
+ disabled={state.editing.metadata}
207
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
208
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
209
+ {t('common.metadata.edit')}
210
+ </Button>
211
+ </SectionHeader>
212
+ <Box className="section-body">
213
+ {!state.editing.metadata &&
214
+ (isEmpty(data.metadata) ? (
215
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
216
+ ) : (
217
+ <Grid container>
218
+ <Grid item xs={12} md={6}>
219
+ {Object.keys(data.metadata || {}).map((key) => (
220
+ // @ts-ignore
221
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
222
+ ))}
223
+ </Grid>
224
+ </Grid>
225
+ ))}
226
+ {state.editing.metadata && (
227
+ <MetadataEditor
228
+ data={data}
229
+ loading={state.loading.metadata}
230
+ onSave={onUpdateMetadata}
231
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
232
+ />
233
+ )}
234
+ </Box>
235
+ </Box>
236
+ <Box className="section">
237
+ <SectionHeader title={t('admin.events')} />
238
+ <Box className="section-body">
239
+ <EventList features={{ toolbar: false }} object_id={data.id} />
240
+ </Box>
241
+ </Box>
242
+ </Root>
243
+ );
244
+ }
245
+
246
+ const Root = styled(Stack)``;
@@ -0,0 +1,154 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import type { TProductExpanded } from '@did-pay/types';
5
+ import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup } from '@mui/material';
6
+ import { useRequest } from 'ahooks';
7
+ import { useEffect, useState } from 'react';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import useBus from 'use-bus';
10
+
11
+ import InfoCard from '../../../../components/info-card';
12
+ import ProductActions from '../../../../components/product/actions';
13
+ import Status from '../../../../components/status';
14
+ import Table from '../../../../components/table';
15
+ import { useSettingsContext } from '../../../../contexts/settings';
16
+ import api from '../../../../libs/api';
17
+ import { formatProductPrice, formatTime } from '../../../../libs/util';
18
+
19
+ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TProductExpanded[]; count: number }> => {
20
+ const search = new URLSearchParams();
21
+ Object.keys(params).forEach((key) => {
22
+ search.set(key, String(params[key]));
23
+ });
24
+ return api.get(`/api/products?${search.toString()}`).then((res) => res.data);
25
+ };
26
+
27
+ export default function ProductsList() {
28
+ const listKey = 'products';
29
+ const persisted = getDurableData(listKey);
30
+
31
+ const { t } = useLocaleContext();
32
+ const navigate = useNavigate();
33
+ const { settings } = useSettingsContext();
34
+ const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
35
+ active: '',
36
+ pageSize: persisted.rowsPerPage || 20,
37
+ page: persisted.page ? persisted.page + 1 : 1,
38
+ });
39
+
40
+ const { loading, error, data, refresh } = useRequest(() => fetchData(search));
41
+ useEffect(() => {
42
+ refresh();
43
+ }, [search, refresh]);
44
+
45
+ useBus('project.created', () => refresh(), []);
46
+
47
+ if (error) {
48
+ return <Alert severity="error">{error.message}</Alert>;
49
+ }
50
+
51
+ if (loading || !data) {
52
+ return <CircularProgress />;
53
+ }
54
+
55
+ const columns = [
56
+ {
57
+ label: t('admin.product.name.label'),
58
+ name: 'name',
59
+ options: {
60
+ customBodyRenderLite: (_: string, index: number) => {
61
+ const product = data.list[index] as TProductExpanded;
62
+ return (
63
+ <InfoCard
64
+ name={product.name}
65
+ description={formatProductPrice(product as any, settings.baseCurrency)}
66
+ logo={product.images[0]}
67
+ />
68
+ );
69
+ },
70
+ },
71
+ },
72
+ {
73
+ label: t('common.status'),
74
+ name: 'active',
75
+ options: {
76
+ customBodyRenderLite: (_: string, index: number) => {
77
+ const item = data.list[index];
78
+ return <Status label={item?.active ? 'Active' : 'Archived'} color={item?.active ? 'success' : 'default'} />;
79
+ },
80
+ },
81
+ },
82
+ {
83
+ label: t('common.createdAt'),
84
+ name: 'created_at',
85
+ options: {
86
+ customBodyRender: (e: string) => {
87
+ return formatTime(e);
88
+ },
89
+ },
90
+ },
91
+ {
92
+ label: t('common.updatedAt'),
93
+ name: 'updated_at',
94
+ options: {
95
+ customBodyRender: (e: string) => {
96
+ return formatTime(e);
97
+ },
98
+ },
99
+ },
100
+ {
101
+ label: t('common.actions'),
102
+ name: 'id',
103
+ width: 100,
104
+ align: 'center',
105
+ options: {
106
+ sort: false,
107
+ customBodyRenderLite: (_: string, index: number) => {
108
+ const product = data.list[index] as TProductExpanded;
109
+ return <ProductActions data={product} onChange={refresh} />;
110
+ },
111
+ },
112
+ },
113
+ ];
114
+
115
+ const onTableChange = ({ page, rowsPerPage }: any) => {
116
+ if (search.pageSize !== rowsPerPage) {
117
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
118
+ } else if (search.page !== page + 1) {
119
+ setSearch((x) => ({ ...x, page: page + 1 }));
120
+ }
121
+ };
122
+
123
+ return (
124
+ <Table
125
+ durable={listKey}
126
+ durableKeys={['page', 'rowsPerPage']}
127
+ title={
128
+ <div className="table-toolbar-left">
129
+ <ToggleButtonGroup
130
+ value={search.active}
131
+ onChange={(_, value) => setSearch((x) => ({ ...x, active: value }))}
132
+ exclusive>
133
+ <ToggleButton value="">All</ToggleButton>
134
+ <ToggleButton value="true">Active</ToggleButton>
135
+ <ToggleButton value="false">Archived</ToggleButton>
136
+ </ToggleButtonGroup>
137
+ </div>
138
+ }
139
+ data={data.list}
140
+ columns={columns}
141
+ options={{
142
+ count: data.count,
143
+ page: search.page - 1,
144
+ rowsPerPage: search.pageSize,
145
+ onRowClick: (_: any, { dataIndex }: any) => {
146
+ const item = data.list[dataIndex] as TProductExpanded;
147
+ navigate(`/admin/products/${item.id}`);
148
+ },
149
+ }}
150
+ loading={loading}
151
+ onChange={onTableChange}
152
+ />
153
+ );
154
+ }
@@ -0,0 +1,3 @@
1
+ export default function BrandingSettings() {
2
+ return <div>Branding settings</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function BusinessSettings() {
2
+ return <div>Business settings</div>;
3
+ }
@@ -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 pages = {
9
+ paymentMethods: React.lazy(() => import('./payment-methods')),
10
+ branding: React.lazy(() => import('./branding')),
11
+ business: React.lazy(() => import('./business')),
12
+ };
13
+
14
+ export default function SettingsIndex() {
15
+ const navigate = useNavigate();
16
+ const { t } = useLocaleContext();
17
+ const { page = 'payment-methods' } = useParams();
18
+
19
+ const onTabChange = (newTab: string) => {
20
+ navigate(`/admin/settings/${newTab}`);
21
+ };
22
+
23
+ // @ts-ignore
24
+ const TabComponent = pages[page] || pages.paymentMethods;
25
+ const tabs = [
26
+ { label: t('admin.paymentMethods'), value: 'payment-methods' },
27
+ { label: t('admin.branding'), value: 'branding' },
28
+ { label: t('admin.business'), value: 'business' },
29
+ ];
30
+
31
+ return (
32
+ <div>
33
+ <Typography variant="h5" sx={{ mb: 1, fontWeight: 600 }}>
34
+ {t('admin.settings')}
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
+ }