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,81 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TPaymentIntentExpanded } from '@did-pay/types';
4
+ import { useSetState } from 'ahooks';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import type { LiteralUnion } from 'type-fest';
7
+
8
+ import api from '../../libs/api';
9
+ import { formatError } from '../../libs/util';
10
+ import Actions from '../actions';
11
+ import ConfirmDialog from '../confirm';
12
+
13
+ type Props = {
14
+ data: TPaymentIntentExpanded;
15
+ variant?: LiteralUnion<'compact' | 'normal', string>;
16
+ };
17
+
18
+ PaymentIntentActions.defaultProps = {
19
+ variant: 'compact',
20
+ };
21
+
22
+ export default function PaymentIntentActions({ data, variant }: Props) {
23
+ const { t } = useLocaleContext();
24
+ const navigate = useNavigate();
25
+ const [state, setState] = useSetState({
26
+ action: '',
27
+ loading: false,
28
+ });
29
+
30
+ const onRefund = async () => {
31
+ try {
32
+ setState({ loading: true });
33
+ await api.put(`/api/payment-intents/${data.id}/refund`).then((res) => res.data);
34
+ Toast.success(t('common.saved'));
35
+ } catch (err) {
36
+ console.error(err);
37
+ Toast.error(formatError(err));
38
+ } finally {
39
+ setState({ loading: false, action: '' });
40
+ }
41
+ };
42
+
43
+ const actions = [
44
+ {
45
+ label: t('admin.paymentIntent.refund'),
46
+ handler: () => setState({ action: 'refund' }),
47
+ color: 'primary',
48
+ disabled: true,
49
+ divider: true,
50
+ },
51
+ {
52
+ label: t('admin.customer.view'),
53
+ handler: () => navigate(`/admin/customers/${data.customer_id}`),
54
+ color: 'primary',
55
+ disabled: false,
56
+ },
57
+ ];
58
+ if (variant === 'compact') {
59
+ actions.push({
60
+ label: t('admin.paymentIntent.view'),
61
+ handler: () => navigate(`/admin/payments/${data.customer_id}`),
62
+ color: 'primary',
63
+ disabled: false,
64
+ });
65
+ }
66
+
67
+ return (
68
+ <>
69
+ <Actions variant={variant} actions={actions} />
70
+ {state.action === 'refund' && (
71
+ <ConfirmDialog
72
+ onConfirm={onRefund}
73
+ onCancel={() => setState({ action: '' })}
74
+ title={t('admin.paymentIntent.refund')}
75
+ message={t('admin.paymentIntent.refundTip')}
76
+ loading={state.loading}
77
+ />
78
+ )}
79
+ </>
80
+ );
81
+ }
@@ -0,0 +1,204 @@
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 { TPaymentIntentExpanded } from '@did-pay/types';
5
+ import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
6
+ import { fromUnitToToken } from '@ocap/util';
7
+ import { useRequest } from 'ahooks';
8
+ import { useEffect, useState } from 'react';
9
+ import { Link, useNavigate } from 'react-router-dom';
10
+
11
+ import api from '../../libs/api';
12
+ import { formatTime, getPaymentIntentStatusColor } from '../../libs/util';
13
+ import Status from '../status';
14
+ import Table from '../table';
15
+ import PaymentIntentActions from './actions';
16
+
17
+ const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPaymentIntentExpanded[]; count: number }> => {
18
+ const search = new URLSearchParams();
19
+ Object.keys(params).forEach((key) => {
20
+ search.set(key, String(params[key]));
21
+ });
22
+ return api.get(`/api/payment-intents?${search.toString()}`).then((res) => res.data);
23
+ };
24
+
25
+ type SearchProps = {
26
+ status: string;
27
+ pageSize: number;
28
+ page: number;
29
+ customer_id?: string;
30
+ invoice_id?: string;
31
+ };
32
+
33
+ type ListProps = {
34
+ features?: {
35
+ customer?: boolean;
36
+ toolbar?: boolean;
37
+ footer?: boolean;
38
+ };
39
+ customer_id?: string;
40
+ invoice_id?: string;
41
+ };
42
+
43
+ const getListKey = (props: ListProps) => {
44
+ if (props.customer_id) {
45
+ return `customer-payments-${props.customer_id}`;
46
+ }
47
+ if (props.invoice_id) {
48
+ return `invoice-payments-${props.invoice_id}`;
49
+ }
50
+
51
+ return 'payments';
52
+ };
53
+
54
+ PaymentList.defaultProps = {
55
+ features: {
56
+ customer: true,
57
+ filter: true,
58
+ },
59
+ customer_id: '',
60
+ invoice_id: '',
61
+ };
62
+
63
+ export default function PaymentList({ customer_id, invoice_id, features }: ListProps) {
64
+ const { t } = useLocaleContext();
65
+ const navigate = useNavigate();
66
+
67
+ const listKey = getListKey({ customer_id, invoice_id });
68
+
69
+ const persisted = getDurableData(listKey);
70
+ const [search, setSearch] = useState<SearchProps>({
71
+ status: '',
72
+ customer_id,
73
+ invoice_id,
74
+ pageSize: persisted.rowsPerPage || 20,
75
+ page: persisted.page ? persisted.page + 1 : 1,
76
+ });
77
+
78
+ const { loading, error, data, refresh } = useRequest(() => fetchData(search));
79
+ useEffect(() => {
80
+ refresh();
81
+ }, [search, refresh]);
82
+
83
+ if (error) {
84
+ return <Alert severity="error">{error.message}</Alert>;
85
+ }
86
+
87
+ if (loading || !data) {
88
+ return <CircularProgress />;
89
+ }
90
+
91
+ const columns = [
92
+ {
93
+ label: t('common.amount'),
94
+ name: 'id',
95
+ options: {
96
+ customBodyRenderLite: (_: string, index: number) => {
97
+ const item = data.list[index] as TPaymentIntentExpanded;
98
+ return (
99
+ <Typography component="span">
100
+ {fromUnitToToken(item?.amount, item?.paymentCurrency.decimal)}&nbsp;
101
+ {item?.paymentCurrency.symbol}
102
+ </Typography>
103
+ );
104
+ },
105
+ },
106
+ },
107
+ {
108
+ label: t('common.status'),
109
+ name: 'status',
110
+ options: {
111
+ customBodyRenderLite: (_: string, index: number) => {
112
+ const item = data.list[index] as TPaymentIntentExpanded;
113
+ return <Status label={item.status} color={getPaymentIntentStatusColor(item.status)} />;
114
+ },
115
+ },
116
+ },
117
+ {
118
+ label: t('common.description'),
119
+ name: 'description',
120
+ options: {
121
+ customBodyRenderLite: (_: string, index: number) => {
122
+ const item = data.list[index] as TPaymentIntentExpanded;
123
+ return item.description || item.id;
124
+ },
125
+ },
126
+ },
127
+ {
128
+ label: t('common.createdAt'),
129
+ name: 'created_at',
130
+ options: {
131
+ customBodyRender: (e: string) => {
132
+ return formatTime(e);
133
+ },
134
+ },
135
+ },
136
+ {
137
+ label: t('common.actions'),
138
+ name: '',
139
+ options: {
140
+ customBodyRenderLite: (_: string, index: number) => {
141
+ const item = data.list[index] as TPaymentIntentExpanded;
142
+ return <PaymentIntentActions data={item} />;
143
+ },
144
+ },
145
+ },
146
+ ];
147
+
148
+ if (features?.customer) {
149
+ columns.splice(3, 0, {
150
+ label: t('common.customer'),
151
+ name: 'customer_id',
152
+ options: {
153
+ customBodyRenderLite: (_: string, index: number) => {
154
+ const item = data.list[index] as TPaymentIntentExpanded;
155
+ return <Link to={`/admin/customers/${item?.customer.id}`}>{item?.customer.email}</Link>;
156
+ },
157
+ },
158
+ });
159
+ }
160
+
161
+ const onTableChange = ({ page, rowsPerPage }: any) => {
162
+ if (search.pageSize !== rowsPerPage) {
163
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
164
+ } else if (search.page !== page + 1) {
165
+ setSearch((x) => ({ ...x, page: page + 1 }));
166
+ }
167
+ };
168
+
169
+ return (
170
+ <Table
171
+ durable={listKey}
172
+ durableKeys={['page', 'rowsPerPage']}
173
+ data={data.list}
174
+ columns={columns}
175
+ loading={loading}
176
+ onChange={onTableChange}
177
+ options={{
178
+ count: data.count,
179
+ page: search.page - 1,
180
+ rowsPerPage: search.pageSize,
181
+ onRowClick: (_: any, { dataIndex }: any) => {
182
+ const item = data.list[dataIndex] as TPaymentIntentExpanded;
183
+ navigate(`/admin/payments/${item.id}`);
184
+ },
185
+ }}
186
+ toolbar={features?.toolbar}
187
+ footer={features?.footer}
188
+ title={
189
+ <div className="table-toolbar-left">
190
+ <ToggleButtonGroup
191
+ value={search.status}
192
+ onChange={(_, value) => setSearch((x) => ({ ...x, status: value }))}
193
+ exclusive>
194
+ <ToggleButton value="">All</ToggleButton>
195
+ <ToggleButton value="succeeded">Succeeded</ToggleButton>
196
+ <ToggleButton value="canceled">Canceled</ToggleButton>
197
+ <ToggleButton value="requires_capture">Uncaptured</ToggleButton>
198
+ <ToggleButton value="requires_action,requires_confirmation,requires_payment_method">Failed</ToggleButton>
199
+ </ToggleButtonGroup>
200
+ </div>
201
+ }
202
+ />
203
+ );
204
+ }
@@ -0,0 +1,114 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TPaymentLinkExpanded } from '@did-pay/types';
4
+ import { useSetState } from 'ahooks';
5
+ import type { LiteralUnion } from 'type-fest';
6
+
7
+ import api from '../../libs/api';
8
+ import { formatError } from '../../libs/util';
9
+ import Actions from '../actions';
10
+ import ConfirmDialog from '../confirm';
11
+ import RenamePaymentLink from './rename';
12
+
13
+ type Props = {
14
+ data: TPaymentLinkExpanded;
15
+ onChange: (action: string) => void;
16
+ variant?: LiteralUnion<'compact' | 'normal', string>;
17
+ };
18
+
19
+ PaymentLinkActions.defaultProps = {
20
+ variant: 'compact',
21
+ };
22
+
23
+ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
24
+ const { t } = useLocaleContext();
25
+ const [state, setState] = useSetState({
26
+ action: '',
27
+ loading: false,
28
+ });
29
+
30
+ const onUpdate = async (updates: TPaymentLinkExpanded) => {
31
+ try {
32
+ setState({ loading: true });
33
+ await api.put(`/api/payment-links/${data.id}`, updates).then((res) => res.data);
34
+ Toast.success(t('common.saved'));
35
+ onChange(state.action);
36
+ } catch (err) {
37
+ console.error(err);
38
+ Toast.error(formatError(err));
39
+ } finally {
40
+ setState({ loading: false, action: '' });
41
+ }
42
+ };
43
+ const onArchive = async () => {
44
+ try {
45
+ setState({ loading: true });
46
+ await api.put(`/api/payment-links/${data.id}/archive`).then((res) => res.data);
47
+ Toast.success(t('common.saved'));
48
+ onChange(state.action);
49
+ } catch (err) {
50
+ console.error(err);
51
+ Toast.error(formatError(err));
52
+ } finally {
53
+ setState({ loading: false, action: '' });
54
+ }
55
+ };
56
+ const noRemove = async () => {
57
+ try {
58
+ setState({ loading: true });
59
+ await api.delete(`/api/payment-links/${data.id}`).then((res) => res.data);
60
+ Toast.success(t('common.removed'));
61
+ onChange(state.action);
62
+ } catch (err) {
63
+ console.error(err);
64
+ Toast.error(formatError(err));
65
+ } finally {
66
+ setState({ loading: false, action: '' });
67
+ }
68
+ };
69
+
70
+ return (
71
+ <>
72
+ <Actions
73
+ variant={variant}
74
+ actions={[
75
+ // {
76
+ // label: t('admin.paymentLink.edit'),
77
+ // handler: () => setState({ action: 'edit' }),
78
+ // color: 'primary',
79
+ // divider: true,
80
+ // },
81
+ { label: t('admin.paymentLink.rename'), handler: () => setState({ action: 'rename' }), color: 'primary' },
82
+ { label: t('admin.paymentLink.archive'), handler: () => setState({ action: 'archive' }), color: 'primary' },
83
+ { label: t('admin.paymentLink.remove'), handler: () => setState({ action: 'remove' }), color: 'error' },
84
+ ]}
85
+ />
86
+ {state.action === 'rename' && (
87
+ <RenamePaymentLink
88
+ paymentLink={data}
89
+ loading={state.loading}
90
+ onSave={onUpdate}
91
+ onCancel={() => setState({ action: '' })}
92
+ />
93
+ )}
94
+ {state.action === 'archive' && (
95
+ <ConfirmDialog
96
+ onConfirm={onArchive}
97
+ onCancel={() => setState({ action: '' })}
98
+ title={t('admin.paymentLink.archive')}
99
+ message={t('admin.paymentLink.archiveTip')}
100
+ loading={state.loading}
101
+ />
102
+ )}
103
+ {state.action === 'remove' && (
104
+ <ConfirmDialog
105
+ onConfirm={noRemove}
106
+ onCancel={() => setState({ action: '' })}
107
+ title={t('admin.paymentLink.remove')}
108
+ message={t('admin.paymentLink.removeTip')}
109
+ loading={state.loading}
110
+ />
111
+ )}
112
+ </>
113
+ );
114
+ }
@@ -0,0 +1,87 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
4
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
5
+
6
+ export default function AfterPay() {
7
+ const { t } = useLocaleContext();
8
+ const { control, setValue, getValues } = useFormContext();
9
+ const type = useWatch({ control, name: 'after_completion.type' });
10
+
11
+ return (
12
+ <Stack spacing={2}>
13
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
14
+ {t('admin.paymentLink.confirmPage')}
15
+ </Typography>
16
+ <Controller
17
+ name="after_completion.type"
18
+ control={control}
19
+ render={({ field }) => (
20
+ <FormControlLabel
21
+ control={
22
+ <Checkbox
23
+ checked={getValues().after_completion.type === 'hosted_confirmation'}
24
+ {...field}
25
+ onChange={(_, checked) => setValue(field.name, checked ? 'hosted_confirmation' : 'redirect')}
26
+ />
27
+ }
28
+ label={t('admin.paymentLink.showConfirmPage')}
29
+ />
30
+ )}
31
+ />
32
+ {type === 'hosted_confirmation' && (
33
+ <Controller
34
+ name="after_completion.hosted_confirmation.custom_message"
35
+ control={control}
36
+ render={({ field }) => (
37
+ <TextField {...field} placeholder="Replace default success message" fullWidth size="small" />
38
+ )}
39
+ />
40
+ )}
41
+ <Controller
42
+ name="after_completion.type"
43
+ control={control}
44
+ render={({ field }) => (
45
+ <FormControlLabel
46
+ control={
47
+ <Checkbox
48
+ checked={getValues().after_completion.type === 'redirect'}
49
+ {...field}
50
+ onChange={(_, checked) => setValue(field.name, checked ? 'redirect' : 'hosted_confirmation')}
51
+ />
52
+ }
53
+ label={t('admin.paymentLink.noConfirmPage')}
54
+ />
55
+ )}
56
+ />
57
+ {type === 'redirect' && (
58
+ <Controller
59
+ name="after_completion.redirect.url"
60
+ control={control}
61
+ render={({ field }) => (
62
+ <TextField placeholder="Redirect customers to your site" {...field} fullWidth size="small" />
63
+ )}
64
+ />
65
+ )}
66
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
67
+ {t('admin.invoices')}
68
+ </Typography>
69
+ <Controller
70
+ name="invoice_creation.enabled"
71
+ control={control}
72
+ render={({ field }) => (
73
+ <FormControlLabel
74
+ control={
75
+ <Checkbox
76
+ checked={getValues().invoice_creation.enabled}
77
+ {...field}
78
+ onChange={(_, checked) => setValue(field.name, checked)}
79
+ />
80
+ }
81
+ label={t('admin.paymentLink.createInvoice')}
82
+ />
83
+ )}
84
+ />
85
+ </Stack>
86
+ );
87
+ }
@@ -0,0 +1,175 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
4
+ import { useEffect, useState } from 'react';
5
+ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
6
+
7
+ import { useProductsContext } from '../../contexts/products';
8
+ import { getProductByPriceId, isPriceRecurringAligned } from '../../libs/util';
9
+ import CreateProduct from '../product/create';
10
+ import LineItem from './item';
11
+ import ProductSelect from './product-select';
12
+
13
+ export default function BeforePay() {
14
+ const { t } = useLocaleContext();
15
+ const { products, refresh } = useProductsContext();
16
+ const { control, setValue, getValues } = useFormContext();
17
+ const items = useFieldArray({ control, name: 'line_items' });
18
+ const includeFreeTrial = useWatch({ control, name: 'include_free_trial' });
19
+ const [state, setState] = useState({ creating: false });
20
+
21
+ useEffect(() => {
22
+ if (items.fields.length) {
23
+ const selected: any[] = items.fields.map((x: any) => getProductByPriceId(products, x.price_id));
24
+ const name = selected.length > 1 ? `${selected[0].name} and ${selected.length - 1} more` : selected[0].name;
25
+ setValue('name', name);
26
+ } else {
27
+ setValue('name', '');
28
+ }
29
+ }, [items.fields, setValue, products]);
30
+
31
+ const onProductSelected = (priceId: string) => {
32
+ if (priceId === 'add') {
33
+ setState({ creating: true });
34
+ } else if (priceId) {
35
+ items.append({
36
+ price_id: priceId,
37
+ quantity: 1,
38
+ adjustable_quantity: { enabled: false, maximum: 99, minimum: 0 },
39
+ });
40
+ const product = getProductByPriceId(products, priceId);
41
+ if (product) {
42
+ const price = product.prices.find((x) => x.id === priceId);
43
+ if (price && price.type === 'recurring') {
44
+ setValue('invoice_creation.enabled', true);
45
+ }
46
+ }
47
+ }
48
+ };
49
+
50
+ const onProductCreated = () => {
51
+ setState({ creating: false });
52
+ refresh();
53
+ };
54
+
55
+ return (
56
+ <Stack spacing={2}>
57
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
58
+ {t('admin.paymentLink.products')} ({getValues().line_items.length})
59
+ </Typography>
60
+ <Stack spacing={2}>
61
+ {items.fields.map((item, index) => {
62
+ // @ts-ignore
63
+ const product = getProductByPriceId(products, item.price_id);
64
+ if (!product) {
65
+ return null;
66
+ }
67
+
68
+ return (
69
+ <LineItem
70
+ key={item.id}
71
+ // @ts-ignore
72
+ valid={isPriceRecurringAligned(items.fields, products, index)}
73
+ prefix={`line_items.${index}`}
74
+ product={product}
75
+ onRemove={() => items.remove(index)}
76
+ onUpdate={refresh}
77
+ />
78
+ );
79
+ })}
80
+ {items.fields.some((_, index) => !isPriceRecurringAligned(items.fields as any[], products, index)) && (
81
+ <Typography color="error" fontSize="small">
82
+ {t('admin.paymentLink.notAligned')}
83
+ </Typography>
84
+ )}
85
+ <ProductSelect
86
+ mode={items.fields.length ? 'waiting' : 'selecting'}
87
+ onSelect={onProductSelected}
88
+ hasSelected={(price) => items.fields.some((x: any) => x.price_id === price.id)}
89
+ />
90
+ {state.creating && <CreateProduct onCancel={() => setState({ creating: false })} onSave={onProductCreated} />}
91
+ </Stack>
92
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
93
+ {t('common.options')}
94
+ </Typography>
95
+ <Controller
96
+ name="billing_address_collection"
97
+ control={control}
98
+ render={({ field }) => (
99
+ <FormControlLabel
100
+ control={
101
+ <Checkbox
102
+ checked={getValues().billing_address_collection === 'required'}
103
+ {...field}
104
+ onChange={(_, checked) => setValue(field.name, checked ? 'required' : 'auto')}
105
+ />
106
+ }
107
+ label={t('admin.paymentLink.requireBillingAddress')}
108
+ />
109
+ )}
110
+ />
111
+ <Controller
112
+ name="phone_number_collection.enabled"
113
+ control={control}
114
+ render={({ field }) => (
115
+ <FormControlLabel
116
+ control={
117
+ <Checkbox
118
+ checked={getValues().phone_number_collection.enabled}
119
+ {...field}
120
+ onChange={(_, checked) => setValue(field.name, checked)}
121
+ />
122
+ }
123
+ label={t('admin.paymentLink.requirePhoneNumber')}
124
+ />
125
+ )}
126
+ />
127
+ <Controller
128
+ name="allow_promotion_codes"
129
+ control={control}
130
+ render={({ field }) => (
131
+ <FormControlLabel
132
+ control={
133
+ <Checkbox
134
+ checked={getValues().allow_promotion_codes}
135
+ {...field}
136
+ onChange={(_, checked) => setValue(field.name, checked)}
137
+ />
138
+ }
139
+ label={t('admin.paymentLink.allowPromotionCodes')}
140
+ />
141
+ )}
142
+ />
143
+ <Controller
144
+ name="include_free_trial"
145
+ control={control}
146
+ render={({ field }) => (
147
+ <FormControlLabel
148
+ control={
149
+ <Checkbox
150
+ checked={getValues().include_free_trial}
151
+ {...field}
152
+ onChange={(_, checked) => {
153
+ setValue(field.name, checked);
154
+ if (checked) {
155
+ setValue('subscription_data.trial_period_days', 30);
156
+ } else {
157
+ setValue('subscription_data.trial_period_days', 0);
158
+ }
159
+ }}
160
+ />
161
+ }
162
+ label={t('admin.paymentLink.includeFreeTrail')}
163
+ />
164
+ )}
165
+ />
166
+ {includeFreeTrial && (
167
+ <Controller
168
+ name="subscription_data.trial_period_days"
169
+ control={control}
170
+ render={({ field }) => <TextField {...field} size="small" InputProps={{ endAdornment: t('common.days') }} />}
171
+ />
172
+ )}
173
+ </Stack>
174
+ );
175
+ }