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,21 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+
3
+ import Actions from '../actions';
4
+
5
+ type PriceActionProps = {
6
+ onDuplicate: Function;
7
+ onRemove: Function;
8
+ };
9
+
10
+ export default function PriceActions(props: PriceActionProps) {
11
+ const { t } = useLocaleContext();
12
+
13
+ return (
14
+ <Actions
15
+ actions={[
16
+ { label: t('admin.price.duplicate'), handler: props.onDuplicate, color: 'primary' },
17
+ { label: t('admin.price.remove'), handler: props.onRemove, color: 'error' },
18
+ ]}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,292 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import type { InferFormType, PriceRecurring, TPriceExpanded } from '@did-pay/types';
4
+ import {
5
+ Box,
6
+ Checkbox,
7
+ FormControlLabel,
8
+ FormLabel,
9
+ InputAdornment,
10
+ MenuItem,
11
+ Select,
12
+ Stack,
13
+ TextField,
14
+ ToggleButton,
15
+ ToggleButtonGroup,
16
+ } from '@mui/material';
17
+ import { styled } from '@mui/system';
18
+ import { Controller, useFormContext, useWatch } from 'react-hook-form';
19
+ import type { LiteralUnion } from 'type-fest';
20
+
21
+ import { useSettingsContext } from '../../contexts/settings';
22
+ import Collapse from '../collapse';
23
+
24
+ export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | 'locked'> & {
25
+ model: LiteralUnion<'standard' | 'package' | 'graduated' | 'volume' | 'custom', string>;
26
+ recurring: Omit<PriceRecurring, 'usage_type'> & {
27
+ interval_config: string;
28
+ metered: boolean;
29
+ };
30
+ };
31
+
32
+ export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
33
+ model: 'standard',
34
+ billing_scheme: '',
35
+ currency_id: '',
36
+ nickname: '',
37
+ type: 'one_time',
38
+ unit_amount: '0',
39
+ lookup_key: '',
40
+ recurring: {
41
+ interval_config: 'month_1',
42
+ interval: 'month',
43
+ interval_count: 1,
44
+ metered: false,
45
+ usage_type: '',
46
+ aggregate_usage: 'sum',
47
+ },
48
+ transform_quantity: {
49
+ divide_by: 1,
50
+ round: 'up',
51
+ },
52
+ tiers: [],
53
+ metadata: [],
54
+ };
55
+
56
+ type PriceFormProps = {
57
+ prefix?: string;
58
+ simple?: boolean;
59
+ };
60
+
61
+ PriceForm.defaultProps = {
62
+ prefix: '',
63
+ simple: false,
64
+ };
65
+
66
+ // FIXME: @wangshijun i18n
67
+ export default function PriceForm({ prefix, simple }: PriceFormProps) {
68
+ const getFieldName = (name: string) => (prefix ? `${prefix}.${name}` : name);
69
+
70
+ const { t } = useLocaleContext();
71
+ const { control, setValue, getFieldState } = useFormContext();
72
+ const { settings, livemode } = useSettingsContext();
73
+
74
+ const isRecurring = useWatch({ control, name: getFieldName('type') }) === 'recurring';
75
+ const isMetered = useWatch({ control, name: getFieldName('recurring.metered') });
76
+ const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
77
+ const model = useWatch({ control, name: getFieldName('model') });
78
+
79
+ return (
80
+ <Root direction="column" alignItems="flex-start" spacing={2}>
81
+ <Controller
82
+ rules={{ required: true }}
83
+ name={getFieldName('model')}
84
+ control={control}
85
+ render={({ field }) => (
86
+ <Box sx={{ width: 174 }}>
87
+ <FormLabel>{t('admin.price.model')}</FormLabel>
88
+ <Select {...field} fullWidth size="small">
89
+ <MenuItem value="standard">Standard Pricing</MenuItem>
90
+ <MenuItem value="package">Package Pricing</MenuItem>
91
+ <MenuItem value="graduated" disabled>
92
+ Graduated Pricing
93
+ </MenuItem>
94
+ <MenuItem value="volume" disabled>
95
+ Volume Pricing
96
+ </MenuItem>
97
+ <MenuItem value="custom" disabled>
98
+ Customer choose price
99
+ </MenuItem>
100
+ </Select>
101
+ </Box>
102
+ )}
103
+ />
104
+ <Stack direction="row">
105
+ <Controller
106
+ name={getFieldName('unit_amount')}
107
+ control={control}
108
+ rules={{ required: t('admin.price.unit_amount.required'), min: t('admin.price.unit_amount.positive') }}
109
+ render={({ field }) => (
110
+ <Box>
111
+ <FormLabel>{t('admin.price.amount')}</FormLabel>
112
+ <TextField
113
+ {...field}
114
+ type="number"
115
+ size="small"
116
+ sx={{ width: 174 }}
117
+ error={!!getFieldState(getFieldName('unit_amount')).error}
118
+ helperText={getFieldState(getFieldName('unit_amount')).error?.message}
119
+ InputProps={{
120
+ endAdornment: <InputAdornment position="end">{settings.baseCurrency.symbol}</InputAdornment>,
121
+ }}
122
+ />
123
+ </Box>
124
+ )}
125
+ />
126
+ {model === 'package' && (
127
+ <Controller
128
+ name={getFieldName('transform_quantity.divide_by')}
129
+ control={control}
130
+ render={({ field }) => (
131
+ <Box ml={2}>
132
+ <FormLabel>&nbsp;</FormLabel>
133
+ <TextField
134
+ {...field}
135
+ type="number"
136
+ size="small"
137
+ sx={{ width: 174 }}
138
+ InputProps={{
139
+ startAdornment: <InputAdornment position="start">{t('common.per')}</InputAdornment>,
140
+ endAdornment: <InputAdornment position="end">{t('common.unit')}</InputAdornment>,
141
+ }}
142
+ />
143
+ </Box>
144
+ )}
145
+ />
146
+ )}
147
+ </Stack>
148
+ <Controller
149
+ name={getFieldName('type')}
150
+ control={control}
151
+ render={({ field }) => (
152
+ <ToggleButtonGroup {...field} onChange={(_, value: string) => setValue(field.name, value)} exclusive>
153
+ <ToggleButton value="one_time">One Time</ToggleButton>
154
+ <ToggleButton value="recurring">Recurring</ToggleButton>
155
+ </ToggleButtonGroup>
156
+ )}
157
+ />
158
+ {isRecurring && (
159
+ <Stack direction="row">
160
+ <Controller
161
+ name={getFieldName('recurring.interval_config')}
162
+ control={control}
163
+ render={({ field }) => (
164
+ <Box>
165
+ <FormLabel>{t('admin.price.recurring.interval')}</FormLabel>
166
+ <Select
167
+ {...field}
168
+ onChange={(e) => {
169
+ const [interval, count] = e.target.value.split('_');
170
+ setValue(getFieldName('recurring.interval'), interval);
171
+ setValue(getFieldName('recurring.interval_count'), count);
172
+ setValue(getFieldName('recurring.interval_config'), e.target.value);
173
+ }}
174
+ sx={{ width: 174 }}
175
+ size="small">
176
+ {!livemode && <MenuItem value="hour_1">Hourly</MenuItem>}
177
+ <MenuItem value="day_1">Daily</MenuItem>
178
+ <MenuItem value="week_1">Weekly</MenuItem>
179
+ <MenuItem value="month_1">Monthly</MenuItem>
180
+ <MenuItem value="month_3">Every 3 months</MenuItem>
181
+ <MenuItem value="month_6">Every 6 months</MenuItem>
182
+ <MenuItem value="year_1">Yearly</MenuItem>
183
+ <MenuItem value="month_2">Custom</MenuItem>
184
+ </Select>
185
+ </Box>
186
+ )}
187
+ />
188
+ {isCustomInterval && (
189
+ <Controller
190
+ name={getFieldName('recurring.interval_count')}
191
+ control={control}
192
+ render={({ field }) => (
193
+ <Box ml={2}>
194
+ <FormLabel>&nbsp;</FormLabel>
195
+ <TextField
196
+ {...field}
197
+ type="number"
198
+ size="small"
199
+ sx={{ width: 174 }}
200
+ InputProps={{
201
+ startAdornment: <InputAdornment position="start">{t('common.every')}</InputAdornment>,
202
+ endAdornment: (
203
+ <InputAdornment position="end">
204
+ <select onChange={(e) => setValue(getFieldName('recurring.interval'), e.target.value)}>
205
+ <option value="day">days</option>
206
+ <option value="week">weeks</option>
207
+ <option value="month">months</option>
208
+ </select>
209
+ </InputAdornment>
210
+ ),
211
+ }}
212
+ />
213
+ </Box>
214
+ )}
215
+ />
216
+ )}
217
+ </Stack>
218
+ )}
219
+ {isRecurring && (
220
+ <Controller
221
+ name={getFieldName('recurring.metered')}
222
+ control={control}
223
+ render={({ field }) => (
224
+ <FormControlLabel
225
+ control={
226
+ <Checkbox
227
+ checked={isMetered}
228
+ {...field}
229
+ onChange={(_, checked: boolean) => setValue(field.name, checked)}
230
+ />
231
+ }
232
+ label={t('admin.price.recurring.metered')}
233
+ />
234
+ )}
235
+ />
236
+ )}
237
+ {isRecurring && isMetered && (
238
+ <Controller
239
+ name={getFieldName('recurring.aggregate_usage')}
240
+ control={control}
241
+ render={({ field }) => (
242
+ <Box>
243
+ <FormLabel>{t('admin.price.recurring.aggregate')}</FormLabel>
244
+ <Select {...field} sx={{ width: 362 }} size="small">
245
+ <MenuItem value="sum">Sum of usage values during period</MenuItem>
246
+ <MenuItem value="max">Maximum usage value during period</MenuItem>
247
+ <MenuItem value="last_ever">Most recent usage value</MenuItem>
248
+ <MenuItem value="last_during_period">Most recent usage value during period</MenuItem>
249
+ </Select>
250
+ </Box>
251
+ )}
252
+ />
253
+ )}
254
+ {!simple && (
255
+ <Collapse trigger={t('admin.price.additional')}>
256
+ <Stack spacing={2} alignItems="flex-start">
257
+ <Controller
258
+ name={getFieldName('nickname')}
259
+ control={control}
260
+ render={({ field }) => (
261
+ <Box>
262
+ <FormLabel>{t('admin.price.nickname.label')}</FormLabel>
263
+ <TextField {...field} size="small" />
264
+ </Box>
265
+ )}
266
+ />
267
+ <Controller
268
+ name={getFieldName('lookup_key')}
269
+ control={control}
270
+ render={({ field }) => (
271
+ <Box>
272
+ <FormLabel>{t('admin.price.lookup_key.label')}</FormLabel>
273
+ <TextField {...field} size="small" />
274
+ </Box>
275
+ )}
276
+ />
277
+ </Stack>
278
+ </Collapse>
279
+ )}
280
+ </Root>
281
+ );
282
+ }
283
+
284
+ const Root = styled(Stack)`
285
+ select {
286
+ border: none;
287
+ &:active,
288
+ &:focus {
289
+ border: none;
290
+ }
291
+ }
292
+ `;
@@ -0,0 +1,125 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TProduct } 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 EditProduct from './edit';
12
+
13
+ type ProductActionProps = {
14
+ data: TProduct;
15
+ onChange: (action: string) => void;
16
+ variant?: LiteralUnion<'compact' | 'normal', string>;
17
+ };
18
+
19
+ ProductActions.defaultProps = {
20
+ variant: 'compact',
21
+ };
22
+
23
+ export default function ProductActions({ data, variant, onChange }: ProductActionProps) {
24
+ const { t } = useLocaleContext();
25
+ const [state, setState] = useSetState({
26
+ action: '',
27
+ loading: false,
28
+ });
29
+
30
+ const onUpdate = async (updates: TProduct) => {
31
+ try {
32
+ setState({ loading: true });
33
+ await api.put(`/api/products/${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/products/${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 onRemove = async () => {
57
+ try {
58
+ setState({ loading: true });
59
+ await api.delete(`/api/products/${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.product.edit'),
77
+ handler: () => setState({ action: 'edit' }),
78
+ color: 'primary',
79
+ disabled: !data.active,
80
+ divider: true,
81
+ },
82
+ data.active
83
+ ? { label: t('admin.product.archive'), handler: () => setState({ action: 'archive' }), color: 'primary' }
84
+ : { label: t('admin.product.unarchive'), handler: () => setState({ action: 'unarchive' }), color: 'primary' }, // prettier-ignore
85
+ { label: t('admin.product.remove'), handler: () => setState({ action: 'remove' }), color: 'error' },
86
+ ].filter(Boolean)}
87
+ />
88
+ {state.action === 'edit' && (
89
+ <EditProduct
90
+ product={data}
91
+ loading={state.loading}
92
+ onSave={onUpdate}
93
+ onCancel={() => setState({ action: '' })}
94
+ />
95
+ )}
96
+ {state.action === 'archive' && (
97
+ <ConfirmDialog
98
+ onConfirm={onArchive}
99
+ onCancel={() => setState({ action: '' })}
100
+ title={t('admin.product.archive')}
101
+ message={t('admin.product.archiveTip')}
102
+ loading={state.loading}
103
+ />
104
+ )}
105
+ {state.action === 'unarchive' && (
106
+ <ConfirmDialog
107
+ onConfirm={onArchive}
108
+ onCancel={() => setState({ action: '' })}
109
+ title={t('admin.product.unarchive')}
110
+ message={t('admin.product.unarchiveTip')}
111
+ loading={state.loading}
112
+ />
113
+ )}
114
+ {state.action === 'remove' && (
115
+ <ConfirmDialog
116
+ onConfirm={onRemove}
117
+ onCancel={() => setState({ action: '' })}
118
+ title={t('admin.product.remove')}
119
+ message={t('admin.product.removeTip')}
120
+ loading={state.loading}
121
+ />
122
+ )}
123
+ </>
124
+ );
125
+ }
@@ -0,0 +1,59 @@
1
+ import Dialog from '@arcblock/ux/lib/Dialog';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Button, CircularProgress, Stack } from '@mui/material';
4
+ import type { EventHandler } from 'react';
5
+ import { FormProvider, useForm } from 'react-hook-form';
6
+
7
+ import { useSettingsContext } from '../../contexts/settings';
8
+ import PriceForm, { DEFAULT_PRICE, Price } from '../price/form';
9
+
10
+ export default function AddPrice({
11
+ loading,
12
+ onSave,
13
+ onCancel,
14
+ }: {
15
+ loading: boolean;
16
+ onSave: EventHandler<any>;
17
+ onCancel: EventHandler<any>;
18
+ }) {
19
+ const { t } = useLocaleContext();
20
+ const { settings } = useSettingsContext();
21
+ const methods = useForm<Price>({
22
+ defaultValues: {
23
+ ...DEFAULT_PRICE,
24
+ currency_id: settings.baseCurrency.id,
25
+ },
26
+ });
27
+
28
+ const { handleSubmit, reset } = methods;
29
+ const onSubmit = async (data: any) => {
30
+ await handleSubmit(onSave)(data);
31
+ reset();
32
+ onCancel(null);
33
+ };
34
+
35
+ return (
36
+ <Dialog
37
+ open
38
+ disableEscapeKeyDown
39
+ fullWidth
40
+ maxWidth="md"
41
+ onClose={() => onCancel(null)}
42
+ showCloseButton={false}
43
+ title={t('admin.price.add')}
44
+ actions={
45
+ <Stack direction="row">
46
+ <Button size="small" sx={{ mr: 2 }} onClick={onCancel}>
47
+ {t('common.cancel')}
48
+ </Button>
49
+ <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
50
+ {loading && <CircularProgress size="small" />} {t('common.save')}
51
+ </Button>
52
+ </Stack>
53
+ }>
54
+ <FormProvider {...methods}>
55
+ <PriceForm />
56
+ </FormProvider>
57
+ </Dialog>
58
+ );
59
+ }
@@ -0,0 +1,97 @@
1
+ import Dialog from '@arcblock/ux/lib/Dialog';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import { Button, CircularProgress, Stack } from '@mui/material';
5
+ import { useSetState } from 'ahooks';
6
+ import type { EventHandler } from 'react';
7
+ import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
8
+
9
+ import { useSettingsContext } from '../../contexts/settings';
10
+ import api from '../../libs/api';
11
+ import { formatError } from '../../libs/util';
12
+ import PriceForm, { DEFAULT_PRICE } from '../price/form';
13
+ import ProductForm, { Product } from './form';
14
+
15
+ export default function CreateProduct({
16
+ simple,
17
+ onSave,
18
+ onCancel,
19
+ }: {
20
+ simple?: boolean;
21
+ onSave: EventHandler<any>;
22
+ onCancel: EventHandler<any>;
23
+ }) {
24
+ const { t } = useLocaleContext();
25
+ const { settings } = useSettingsContext();
26
+ const [state, setState] = useSetState({ loading: false });
27
+ const methods = useForm<Product>({
28
+ defaultValues: {
29
+ type: 'service',
30
+ name: '',
31
+ description: '',
32
+ images: [],
33
+ statement_descriptor: window.blocklet.appName,
34
+ unit_label: '',
35
+ features: [],
36
+ prices: [{ ...DEFAULT_PRICE, currency_id: settings.baseCurrency.id }],
37
+ metadata: [],
38
+ },
39
+ });
40
+
41
+ const { control, handleSubmit, reset } = methods;
42
+ const prices = useFieldArray({ control, name: 'prices' });
43
+
44
+ const onCreate = (data: Product) => {
45
+ setState({ loading: true });
46
+ api
47
+ .post('/api/products', data)
48
+ .then(() => {
49
+ Toast.success(t('admin.product.saved'));
50
+ })
51
+ .catch((err) => {
52
+ console.error(err);
53
+ Toast.error(formatError(err));
54
+ })
55
+ .finally(() => {
56
+ setState({ loading: false });
57
+ });
58
+ };
59
+
60
+ const onSubmit = async (data: any) => {
61
+ await handleSubmit(onCreate)(data);
62
+ await reset();
63
+ await onSave(null);
64
+ };
65
+
66
+ return (
67
+ <Dialog
68
+ open
69
+ disableEscapeKeyDown
70
+ fullWidth
71
+ maxWidth="md"
72
+ onClose={() => onCancel(null)}
73
+ showCloseButton={false}
74
+ title={t('admin.product.add')}
75
+ actions={
76
+ <Stack direction="row">
77
+ <Button size="small" sx={{ mr: 2 }} onClick={onCancel}>
78
+ {t('common.cancel')}
79
+ </Button>
80
+ <Button variant="contained" color="primary" size="small" disabled={state.loading} onClick={onSubmit}>
81
+ {state.loading && <CircularProgress size="small" />} {t('common.save')}
82
+ </Button>
83
+ </Stack>
84
+ }>
85
+ <FormProvider {...methods}>
86
+ <ProductForm simple={simple} />
87
+ {prices.fields.map((price, index) => (
88
+ <PriceForm key={price.id} prefix={`prices.${index}`} simple={simple} />
89
+ ))}
90
+ </FormProvider>
91
+ </Dialog>
92
+ );
93
+ }
94
+
95
+ CreateProduct.defaultProps = {
96
+ simple: true,
97
+ };
@@ -0,0 +1,75 @@
1
+ /* eslint-disable @typescript-eslint/indent */
2
+ import Dialog from '@arcblock/ux/lib/Dialog';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { Button, CircularProgress, Stack } from '@mui/material';
5
+ import { fromUnitToToken } from '@ocap/util';
6
+ import { isEmpty } from 'lodash';
7
+ import type { EventHandler } from 'react';
8
+ import { FormProvider, useForm } from 'react-hook-form';
9
+
10
+ import { getPricingModel } from '../../libs/util';
11
+ import PriceForm, { DEFAULT_PRICE, Price } from '../price/form';
12
+
13
+ export default function EditPrice({
14
+ price,
15
+ loading,
16
+ onSave,
17
+ onCancel,
18
+ }: {
19
+ price: Price;
20
+ loading: boolean;
21
+ onSave: EventHandler<any>;
22
+ onCancel: EventHandler<any>;
23
+ }) {
24
+ const { t } = useLocaleContext();
25
+ const methods = useForm<Price>({
26
+ defaultValues: {
27
+ ...price,
28
+ unit_amount: fromUnitToToken(price.unit_amount, price.currency.decimal),
29
+ model: getPricingModel(price as any),
30
+ metadata: isEmpty(price.metadata)
31
+ ? []
32
+ : // @ts-ignore
33
+ Object.keys(price.metadata).map((x) => ({ key: x, value: price.metadata[x] })),
34
+ recurring: price.recurring
35
+ ? {
36
+ ...price.recurring,
37
+ metered: price.recurring.usage_type === 'metered',
38
+ interval_config: [price.recurring.interval, price.recurring.interval_count].join('_'),
39
+ }
40
+ : DEFAULT_PRICE.recurring,
41
+ },
42
+ });
43
+
44
+ const { handleSubmit, reset } = methods;
45
+ const onSubmit = async (data: any) => {
46
+ await handleSubmit(onSave)(data);
47
+ reset();
48
+ onCancel(null);
49
+ };
50
+
51
+ return (
52
+ <Dialog
53
+ open
54
+ disableEscapeKeyDown
55
+ fullWidth
56
+ maxWidth="sm"
57
+ onClose={() => onCancel(null)}
58
+ showCloseButton={false}
59
+ title={t('admin.price.edit')}
60
+ actions={
61
+ <Stack direction="row">
62
+ <Button size="small" sx={{ mr: 2 }} onClick={onCancel}>
63
+ {t('common.cancel')}
64
+ </Button>
65
+ <Button variant="contained" color="primary" size="small" disabled={loading} onClick={onSubmit}>
66
+ {loading && <CircularProgress size="small" />} {t('common.save')}
67
+ </Button>
68
+ </Stack>
69
+ }>
70
+ <FormProvider {...methods}>
71
+ <PriceForm />
72
+ </FormProvider>
73
+ </Dialog>
74
+ );
75
+ }