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,167 @@
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 { TPaymentLinkExpanded } from '@did-pay/types';
5
+ import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
6
+ import { useRequest } from 'ahooks';
7
+ import { useEffect, useState } from 'react';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import { joinURL } from 'ufo';
10
+ import useBus from 'use-bus';
11
+
12
+ import Copyable from '../../../../components/copyable';
13
+ import PaymentLinkActions from '../../../../components/payment-link/actions';
14
+ import Status from '../../../../components/status';
15
+ import Table from '../../../../components/table';
16
+ import { ProductsProvider } from '../../../../contexts/products';
17
+ import { useSettingsContext } from '../../../../contexts/settings';
18
+ import api from '../../../../libs/api';
19
+ import { formatPaymentLinkPricing, formatTime } from '../../../../libs/util';
20
+
21
+ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPaymentLinkExpanded[]; count: number }> => {
22
+ const search = new URLSearchParams();
23
+ Object.keys(params).forEach((key) => {
24
+ search.set(key, String(params[key]));
25
+ });
26
+ return api.get(`/api/payment-links?${search.toString()}`).then((res) => res.data);
27
+ };
28
+
29
+ function PaymentLinks() {
30
+ const listKey = 'payment-links';
31
+ const { t } = useLocaleContext();
32
+ const { settings } = useSettingsContext();
33
+ const navigate = useNavigate();
34
+
35
+ const persisted = getDurableData(listKey);
36
+ const [search, setSearch] = useState<{ active: string; pageSize: number; page: number }>({
37
+ active: '',
38
+ pageSize: persisted.rowsPerPage || 20,
39
+ page: persisted.page ? persisted.page + 1 : 1,
40
+ });
41
+
42
+ const { loading, error, data, refresh } = useRequest(() => fetchData(search));
43
+ useEffect(() => {
44
+ refresh();
45
+ }, [search, refresh]);
46
+
47
+ useBus('paymentLink.created', () => refresh(), []);
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 columns = [
58
+ {
59
+ label: t('common.url'),
60
+ name: 'id',
61
+ options: {
62
+ customBodyRenderLite: (_: string, index: number) => {
63
+ const item = data.list[index] as TPaymentLinkExpanded;
64
+ return (
65
+ <Copyable text={joinURL(window.blocklet.appUrl, window.blocklet.prefix, `/checkout/pay/${item.id}`)}>
66
+ <Typography sx={{ color: 'text.primary', mr: 1 }}>{item.id}</Typography>
67
+ </Copyable>
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.name'),
84
+ name: 'name',
85
+ },
86
+ {
87
+ label: t('admin.price.name'),
88
+ name: '',
89
+ options: {
90
+ sort: false,
91
+ customBodyRenderLite: (_: string, index: number) => {
92
+ const doc = data.list[index] as TPaymentLinkExpanded;
93
+ const result = formatPaymentLinkPricing(doc, settings.baseCurrency);
94
+ return [result.amount, result.then].filter(Boolean).join(', ');
95
+ },
96
+ },
97
+ },
98
+ {
99
+ label: t('common.createdAt'),
100
+ name: 'created_at',
101
+ options: {
102
+ customBodyRender: (e: string) => {
103
+ return formatTime(e);
104
+ },
105
+ },
106
+ },
107
+ {
108
+ label: t('common.actions'),
109
+ name: 'id',
110
+ options: {
111
+ sort: false,
112
+ customBodyRenderLite: (_: string, index: number) => {
113
+ const doc = data.list[index] as TPaymentLinkExpanded;
114
+ return <PaymentLinkActions data={doc} onChange={refresh} />;
115
+ },
116
+ },
117
+ },
118
+ ];
119
+
120
+ const onTableChange = ({ page, rowsPerPage }: any) => {
121
+ if (search.pageSize !== rowsPerPage) {
122
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
123
+ } else if (search.page !== page + 1) {
124
+ setSearch((x) => ({ ...x, page: page + 1 }));
125
+ }
126
+ };
127
+
128
+ return (
129
+ <Table
130
+ durable={listKey}
131
+ durableKeys={['page', 'rowsPerPage']}
132
+ title={
133
+ <div className="table-toolbar-left">
134
+ <ToggleButtonGroup
135
+ value={search.active}
136
+ onChange={(_, value) => setSearch((x) => ({ ...x, active: value }))}
137
+ exclusive>
138
+ <ToggleButton value="">All</ToggleButton>
139
+ <ToggleButton value="true">Active</ToggleButton>
140
+ <ToggleButton value="false">Archived</ToggleButton>
141
+ </ToggleButtonGroup>
142
+ </div>
143
+ }
144
+ data={data.list}
145
+ columns={columns}
146
+ options={{
147
+ count: data.count,
148
+ page: search.page - 1,
149
+ rowsPerPage: search.pageSize,
150
+ onRowClick: (_: any, { dataIndex }: any) => {
151
+ const item = data.list[dataIndex] as TPaymentLinkExpanded;
152
+ navigate(`/admin/payments/${item.id}`);
153
+ },
154
+ }}
155
+ loading={loading}
156
+ onChange={onTableChange}
157
+ />
158
+ );
159
+ }
160
+
161
+ export default function WrappedPaymentLinks() {
162
+ return (
163
+ <ProductsProvider>
164
+ <PaymentLinks />
165
+ </ProductsProvider>
166
+ );
167
+ }
@@ -0,0 +1,3 @@
1
+ export default function Coupons() {
2
+ return <div>Coupons List</div>;
3
+ }
@@ -0,0 +1,81 @@
1
+ import Button from '@arcblock/ux/lib/Button';
2
+ import Center from '@arcblock/ux/lib/Center';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import Tabs from '@arcblock/ux/lib/Tabs';
5
+ import { AddOutlined } from '@mui/icons-material';
6
+ import { CircularProgress, Stack, Typography } from '@mui/material';
7
+ import React, { Suspense, isValidElement } from 'react';
8
+ import { useNavigate, useParams } from 'react-router-dom';
9
+
10
+ const ProductCreate = React.lazy(() => import('./products/create'));
11
+ const ProductDetail = React.lazy(() => import('./products/detail'));
12
+ const PriceDetail = React.lazy(() => import('./prices/detail'));
13
+
14
+ const pages = {
15
+ products: React.lazy(() => import('./products')),
16
+ coupons: React.lazy(() => import('./coupons')),
17
+ 'pricing-tables': React.lazy(() => import('./pricing-tables')),
18
+ };
19
+
20
+ export default function Products() {
21
+ const navigate = useNavigate();
22
+ const { t } = useLocaleContext();
23
+ const { page = 'products' } = useParams();
24
+
25
+ if (page.startsWith('prod_')) {
26
+ return <ProductDetail id={page} />;
27
+ }
28
+
29
+ if (page.startsWith('price_')) {
30
+ return <PriceDetail id={page} />;
31
+ }
32
+
33
+ // @ts-ignore
34
+ const TabComponent = pages[page] || pages.products;
35
+ const tabs = [
36
+ { label: t('admin.products'), value: 'products' },
37
+ { label: t('admin.coupons'), value: 'coupons' },
38
+ { label: t('admin.pricingTables'), value: 'pricing-tables' },
39
+ ];
40
+
41
+ let extra = null;
42
+ if (page === 'products') {
43
+ extra = <ProductCreate />;
44
+ } else if (page === 'coupons') {
45
+ extra = (
46
+ <Button
47
+ variant="contained"
48
+ size="small"
49
+ color="primary"
50
+ onClick={() => navigate('/admin/products/coupons/create')}>
51
+ <AddOutlined />
52
+ {t('admin.coupon.create')}
53
+ </Button>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <>
59
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
60
+ <Typography variant="h5" sx={{ mb: 1, fontWeight: 600 }}>
61
+ {t('admin.products')}
62
+ </Typography>
63
+ {extra}
64
+ </Stack>
65
+ <Tabs
66
+ tabs={tabs}
67
+ current={page}
68
+ onChange={(newTab: string) => navigate(`/admin/products/${newTab}`)}
69
+ scrollButtons="auto"
70
+ />
71
+ <Suspense
72
+ fallback={
73
+ <Center relative="parent">
74
+ <CircularProgress />
75
+ </Center>
76
+ }>
77
+ {isValidElement(TabComponent) ? TabComponent : <TabComponent />}
78
+ </Suspense>
79
+ </>
80
+ );
81
+ }
@@ -0,0 +1,151 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TPrice } from '@did-pay/types';
4
+ import { useSetState } from 'ahooks';
5
+
6
+ import Actions from '../../../../components/actions';
7
+ import ConfirmDialog from '../../../../components/confirm';
8
+ import EditPrice from '../../../../components/product/edit-price';
9
+ import api from '../../../../libs/api';
10
+ import { formatError } from '../../../../libs/util';
11
+
12
+ type Props = {
13
+ data: TPrice;
14
+ onChange: Function;
15
+ variant?: string;
16
+ setAsDefault?: boolean;
17
+ };
18
+
19
+ PriceActions.defaultProps = {
20
+ variant: 'compact',
21
+ setAsDefault: false,
22
+ };
23
+
24
+ export default function PriceActions(props: Props) {
25
+ const { t } = useLocaleContext();
26
+
27
+ const canEdit = props.data.active;
28
+ const canArchive = props.data.active;
29
+ const canRemove = !props.data.locked && props.setAsDefault;
30
+
31
+ const [state, setState] = useSetState({
32
+ action: '',
33
+ loading: false,
34
+ });
35
+
36
+ const onEditPrice = async (updates: TPrice) => {
37
+ try {
38
+ setState({ loading: true });
39
+ await api.put(`/api/prices/${props.data.id}`, updates).then((res) => res.data);
40
+ Toast.success(t('common.saved'));
41
+ props.onChange(state.action);
42
+ } catch (err) {
43
+ console.error(err);
44
+ Toast.error(formatError(err));
45
+ } finally {
46
+ setState({ loading: false, action: '' });
47
+ }
48
+ };
49
+ const onArchivePrice = async () => {
50
+ try {
51
+ setState({ loading: true });
52
+ await api.put(`/api/prices/${props.data.id}/archive`).then((res) => res.data);
53
+ Toast.success(t('common.saved'));
54
+ props.onChange(state.action);
55
+ } catch (err) {
56
+ console.error(err);
57
+ Toast.error(formatError(err));
58
+ } finally {
59
+ setState({ loading: false, action: '' });
60
+ }
61
+ };
62
+ const onRemovePrice = async () => {
63
+ try {
64
+ setState({ loading: true });
65
+ await api.delete(`/api/prices/${props.data.id}`).then((res) => res.data);
66
+ Toast.success(t('common.removed'));
67
+ props.onChange(state.action);
68
+ } catch (err) {
69
+ console.error(err);
70
+ Toast.error(formatError(err));
71
+ } finally {
72
+ setState({ loading: false, action: '' });
73
+ }
74
+ };
75
+ const onSetAsDefault = async () => {
76
+ try {
77
+ setState({ loading: true });
78
+ await api
79
+ .put(`/api/products/${props.data.product_id}`, { default_price_id: props.data.id })
80
+ .then((res) => res.data);
81
+ Toast.success(t('common.removed'));
82
+ props.onChange(state.action);
83
+ } catch (err) {
84
+ console.error(err);
85
+ Toast.error(formatError(err));
86
+ } finally {
87
+ setState({ loading: false, action: '' });
88
+ }
89
+ };
90
+
91
+ const actions = [
92
+ {
93
+ label: t('admin.price.edit'),
94
+ handler: () => setState({ action: 'edit' }),
95
+ color: canEdit ? 'text.primary' : 'text.disabled',
96
+ disabled: !canEdit,
97
+ },
98
+ {
99
+ label: t('admin.price.archive'),
100
+ handler: () => setState({ action: 'archive' }),
101
+ color: canArchive ? 'text.primary' : 'text.disabled',
102
+ disabled: !canArchive,
103
+ divider: true,
104
+ },
105
+ {
106
+ label: t('admin.price.remove'),
107
+ handler: () => setState({ action: 'remove' }),
108
+ color: canRemove ? 'error.main' : 'text.disabled',
109
+ disabled: !canRemove,
110
+ divider: props.setAsDefault,
111
+ },
112
+ // FIXME: add these features later
113
+ // { label: 'Create payment link', handler: props.onRemove, color: 'error', divider: true },
114
+ // { label: 'Create pricing table', handler: props.onRemove, color: 'error', divider: true },
115
+ ];
116
+
117
+ if (props.setAsDefault) {
118
+ actions.push({
119
+ label: t('admin.price.setAsDefault'),
120
+ handler: onSetAsDefault,
121
+ color: 'text.primary',
122
+ disabled: false,
123
+ });
124
+ }
125
+
126
+ return (
127
+ <>
128
+ <Actions variant={props.variant} actions={actions} />
129
+ {state.action === 'edit' && (
130
+ // @ts-ignore
131
+ <EditPrice price={props.data} onSave={onEditPrice} onCancel={() => setState({ action: '' })} />
132
+ )}
133
+ {state.action === 'archive' && (
134
+ <ConfirmDialog
135
+ onConfirm={onArchivePrice}
136
+ onCancel={() => setState({ action: '' })}
137
+ title={t('admin.price.archive')}
138
+ message={t('admin.price.archiveTip')}
139
+ />
140
+ )}
141
+ {state.action === 'remove' && (
142
+ <ConfirmDialog
143
+ onConfirm={onRemovePrice}
144
+ onCancel={() => setState({ action: '' })}
145
+ title={t('admin.price.remove')}
146
+ message={t('admin.price.removeTip')}
147
+ />
148
+ )}
149
+ </>
150
+ );
151
+ }
@@ -0,0 +1,203 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TPrice, TPriceExpanded } 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 Currency from '../../../../components/currency';
13
+ import EventList from '../../../../components/event/list';
14
+ import InfoMetric from '../../../../components/info-metric';
15
+ import InfoRow from '../../../../components/info-row';
16
+ import MetadataEditor from '../../../../components/metadata/editor';
17
+ import EditPrice from '../../../../components/product/edit-price';
18
+ import SectionHeader from '../../../../components/section/header';
19
+ import api from '../../../../libs/api';
20
+ import { formatError, formatPrice, formatTime } from '../../../../libs/util';
21
+ import PriceActions from './actions';
22
+
23
+ const fetchData = (id: string): Promise<TPriceExpanded> => {
24
+ return api.get(`/api/prices/${id}`).then((res) => res.data);
25
+ };
26
+
27
+ export default function PriceDetail(props: { id: string }) {
28
+ const { t } = useLocaleContext();
29
+ const navigate = useNavigate();
30
+ const [state, setState] = useSetState({
31
+ adding: {
32
+ price: false,
33
+ },
34
+ editing: {
35
+ metadata: false,
36
+ price: false,
37
+ },
38
+ loading: {
39
+ metadata: false,
40
+ price: false,
41
+ },
42
+ });
43
+
44
+ const { loading, error, data, runAsync } = useRequest(() => fetchData(props.id));
45
+
46
+ if (error) {
47
+ return <Alert severity="error">{error.message}</Alert>;
48
+ }
49
+
50
+ if (loading || !data) {
51
+ return <CircularProgress />;
52
+ }
53
+
54
+ const createUpdater = (key: string) => async (updates: TPrice) => {
55
+ try {
56
+ setState((prev) => ({ loading: { ...prev.loading, [key]: true } }));
57
+ await api.put(`/api/prices/${props.id}`, updates).then((res) => res.data);
58
+ Toast.success(t('common.saved'));
59
+ runAsync();
60
+ } catch (err) {
61
+ console.error(err);
62
+ Toast.error(formatError(err));
63
+ } finally {
64
+ setState((prev) => ({ loading: { ...prev.loading, [key]: false } }));
65
+ }
66
+ };
67
+
68
+ const onUpdateMetadata = createUpdater('metadata');
69
+ const onUpdateAll = createUpdater('price');
70
+ const onChange = (action: string) => {
71
+ if (action === 'remove') {
72
+ navigate('/admin/products');
73
+ } else {
74
+ runAsync();
75
+ }
76
+ };
77
+
78
+ return (
79
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
80
+ {data.active === false && (
81
+ <Alert severity="warning">
82
+ <AlertTitle>{t('admin.price.archived')}</AlertTitle>
83
+ {t('admin.price.archivedTip')}
84
+ </Alert>
85
+ )}
86
+ <Box>
87
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
88
+ <Link to="/admin/products">
89
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
90
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
91
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
92
+ {t('admin.products')}
93
+ </Typography>
94
+ </Stack>
95
+ </Link>
96
+ <Copyable text={props.id} style={{ marginLeft: 4 }} />
97
+ </Stack>
98
+ <Box mt={2}>
99
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
100
+ <Typography variant="h5" fontWeight={600}>
101
+ Price for {data.product.name}
102
+ </Typography>
103
+ <PriceActions data={data} onChange={onChange} variant="normal" />
104
+ </Stack>
105
+ <Stack
106
+ className="section-body"
107
+ direction="row"
108
+ spacing={3}
109
+ justifyContent="flex-start"
110
+ flexWrap="wrap"
111
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
112
+ <InfoMetric
113
+ label={t('admin.product.name.label')}
114
+ value={<Link to={`/admin/products/${data.product_id}`}>{data.product.name}</Link>}
115
+ divider
116
+ />
117
+ <InfoMetric label={t('admin.price.amount')} value={formatPrice(data, data.currency)} divider />
118
+ <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
119
+ <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
120
+ </Stack>
121
+ </Box>
122
+ </Box>
123
+ <Box className="section">
124
+ <SectionHeader title={t('admin.details')}>
125
+ <Button
126
+ variant="outlined"
127
+ color="inherit"
128
+ size="small"
129
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, price: true } }))}>
130
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
131
+ {t('common.edit')}
132
+ </Button>
133
+ </SectionHeader>
134
+ <Grid className="section-body" container>
135
+ <Grid item xs={12} md={6}>
136
+ <InfoRow label={t('admin.price.type')} value={data.type} />
137
+ <InfoRow label={t('admin.price.recurring.interval')} value={data.recurring?.interval} />
138
+ <InfoRow label={t('admin.price.nickname.label')} value={data.nickname} />
139
+ <InfoRow label={t('admin.price.lookupKey')} value={data.lookup_key} />
140
+ <InfoRow
141
+ label={t('admin.paymentCurrency.name')}
142
+ value={<Currency logo={data.currency.logo} name={data.currency.symbol} />}
143
+ />
144
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
145
+ <InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
146
+ </Grid>
147
+ {state.editing.price && (
148
+ <EditPrice
149
+ // @ts-ignore
150
+ price={data}
151
+ loading={state.loading.price}
152
+ onSave={onUpdateAll}
153
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, price: false } }))}
154
+ />
155
+ )}
156
+ </Grid>
157
+ </Box>
158
+ <Box className="section">
159
+ <SectionHeader title={t('common.metadata.label')}>
160
+ <Button
161
+ variant="outlined"
162
+ color="inherit"
163
+ size="small"
164
+ disabled={state.editing.metadata}
165
+ onClick={() => setState((prev) => ({ editing: { ...prev.editing, metadata: true } }))}>
166
+ <Edit fontSize="small" sx={{ mr: 0.5 }} />
167
+ {t('common.metadata.edit')}
168
+ </Button>
169
+ </SectionHeader>
170
+ <Box className="section-body">
171
+ {!state.editing.metadata &&
172
+ (isEmpty(data.metadata) ? (
173
+ <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
174
+ ) : (
175
+ <Grid container>
176
+ <Grid item xs={12} md={6}>
177
+ {Object.keys(data.metadata || {}).map((key) => (
178
+ <InfoRow key={key} label={key} value={data.metadata[key]} />
179
+ ))}
180
+ </Grid>
181
+ </Grid>
182
+ ))}
183
+ {state.editing.metadata && (
184
+ <MetadataEditor
185
+ data={data}
186
+ loading={state.loading.metadata}
187
+ onSave={onUpdateMetadata}
188
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
189
+ />
190
+ )}
191
+ </Box>
192
+ </Box>
193
+ <Box className="section">
194
+ <SectionHeader title={t('admin.events')} />
195
+ <Box className="section-body">
196
+ <EventList features={{ toolbar: false }} object_id={data.id} />
197
+ </Box>
198
+ </Box>
199
+ </Root>
200
+ );
201
+ }
202
+
203
+ const Root = styled(Stack)``;