payment-kit 1.15.20 → 1.15.22

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 (52) hide show
  1. package/api/src/crons/base.ts +69 -7
  2. package/api/src/crons/subscription-trial-will-end.ts +20 -5
  3. package/api/src/crons/subscription-will-canceled.ts +22 -6
  4. package/api/src/crons/subscription-will-renew.ts +13 -4
  5. package/api/src/index.ts +4 -1
  6. package/api/src/integrations/arcblock/stake.ts +27 -0
  7. package/api/src/libs/audit.ts +4 -1
  8. package/api/src/libs/context.ts +48 -0
  9. package/api/src/libs/invoice.ts +2 -2
  10. package/api/src/libs/middleware.ts +39 -1
  11. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
  13. package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
  15. package/api/src/libs/time.ts +13 -0
  16. package/api/src/libs/util.ts +17 -0
  17. package/api/src/locales/en.ts +12 -2
  18. package/api/src/locales/zh.ts +11 -2
  19. package/api/src/queues/checkout-session.ts +15 -0
  20. package/api/src/queues/event.ts +13 -4
  21. package/api/src/queues/invoice.ts +21 -3
  22. package/api/src/queues/payment.ts +3 -0
  23. package/api/src/queues/refund.ts +3 -0
  24. package/api/src/queues/subscription.ts +107 -2
  25. package/api/src/queues/usage-record.ts +4 -0
  26. package/api/src/queues/webhook.ts +9 -0
  27. package/api/src/routes/checkout-sessions.ts +40 -2
  28. package/api/src/routes/connect/recharge.ts +143 -0
  29. package/api/src/routes/connect/shared.ts +25 -0
  30. package/api/src/routes/customers.ts +2 -2
  31. package/api/src/routes/donations.ts +5 -1
  32. package/api/src/routes/events.ts +9 -4
  33. package/api/src/routes/payment-links.ts +40 -20
  34. package/api/src/routes/prices.ts +17 -4
  35. package/api/src/routes/products.ts +21 -2
  36. package/api/src/routes/refunds.ts +20 -3
  37. package/api/src/routes/subscription-items.ts +39 -2
  38. package/api/src/routes/subscriptions.ts +77 -40
  39. package/api/src/routes/usage-records.ts +29 -0
  40. package/api/src/store/models/event.ts +1 -0
  41. package/api/src/store/models/subscription.ts +2 -0
  42. package/api/tests/libs/time.spec.ts +54 -0
  43. package/blocklet.yml +1 -1
  44. package/package.json +19 -19
  45. package/src/app.tsx +10 -0
  46. package/src/components/subscription/actions/cancel.tsx +30 -9
  47. package/src/components/subscription/actions/index.tsx +11 -3
  48. package/src/components/webhook/attempts.tsx +122 -3
  49. package/src/locales/en.tsx +13 -0
  50. package/src/locales/zh.tsx +13 -0
  51. package/src/pages/customer/recharge.tsx +417 -0
  52. package/src/pages/customer/subscription/detail.tsx +38 -20
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.15.20
17
+ version: 1.15.22
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.15.20",
3
+ "version": "1.15.22",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,29 +43,29 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@abtnode/cron": "^1.16.32",
46
- "@arcblock/did": "^1.18.135",
46
+ "@arcblock/did": "^1.18.136",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.10.45",
49
- "@arcblock/did-util": "^1.18.135",
50
- "@arcblock/jwt": "^1.18.135",
51
- "@arcblock/ux": "^2.10.45",
52
- "@arcblock/validator": "^1.18.135",
48
+ "@arcblock/did-connect": "^2.10.51",
49
+ "@arcblock/did-util": "^1.18.136",
50
+ "@arcblock/jwt": "^1.18.136",
51
+ "@arcblock/ux": "^2.10.51",
52
+ "@arcblock/validator": "^1.18.136",
53
53
  "@blocklet/js-sdk": "^1.16.32",
54
54
  "@blocklet/logger": "^1.16.32",
55
- "@blocklet/payment-react": "1.15.20",
55
+ "@blocklet/payment-react": "1.15.22",
56
56
  "@blocklet/sdk": "^1.16.32",
57
- "@blocklet/ui-react": "^2.10.45",
58
- "@blocklet/uploader": "^0.1.43",
59
- "@blocklet/xss": "^0.1.9",
57
+ "@blocklet/ui-react": "^2.10.51",
58
+ "@blocklet/uploader": "^0.1.46",
59
+ "@blocklet/xss": "^0.1.12",
60
60
  "@mui/icons-material": "^5.16.6",
61
61
  "@mui/lab": "^5.0.0-alpha.173",
62
62
  "@mui/material": "^5.16.6",
63
63
  "@mui/system": "^5.16.6",
64
- "@ocap/asset": "^1.18.135",
65
- "@ocap/client": "^1.18.135",
66
- "@ocap/mcrypto": "^1.18.135",
67
- "@ocap/util": "^1.18.135",
68
- "@ocap/wallet": "^1.18.135",
64
+ "@ocap/asset": "^1.18.136",
65
+ "@ocap/client": "^1.18.136",
66
+ "@ocap/mcrypto": "^1.18.136",
67
+ "@ocap/util": "^1.18.136",
68
+ "@ocap/wallet": "^1.18.136",
69
69
  "@react-pdf/renderer": "^3.4.4",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
@@ -117,8 +117,8 @@
117
117
  },
118
118
  "devDependencies": {
119
119
  "@abtnode/types": "^1.16.32",
120
- "@arcblock/eslint-config-ts": "^0.3.2",
121
- "@blocklet/payment-types": "1.15.20",
120
+ "@arcblock/eslint-config-ts": "^0.3.3",
121
+ "@blocklet/payment-types": "1.15.22",
122
122
  "@types/cookie-parser": "^1.4.7",
123
123
  "@types/cors": "^2.8.17",
124
124
  "@types/debug": "^4.1.12",
@@ -160,5 +160,5 @@
160
160
  "parser": "typescript"
161
161
  }
162
162
  },
163
- "gitHead": "0499ea79c00bb48d7a2479af589c21698c6388a0"
163
+ "gitHead": "12a3cbcf04d0c8bdbbf33c04d07cecfb24bd3e9b"
164
164
  }
package/src/app.tsx CHANGED
@@ -27,6 +27,7 @@ const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/sub
27
27
  const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subscription/embed'));
28
28
  const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
29
29
  const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
30
+ const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge'));
30
31
 
31
32
  // const theme = createTheme({
32
33
  // typography: {
@@ -92,6 +93,15 @@ function App() {
92
93
  </Layout>
93
94
  }
94
95
  />
96
+ <Route
97
+ key="customer-recharge"
98
+ path="/customer/subscription/:id/recharge"
99
+ element={
100
+ <Layout>
101
+ <CustomerRecharge />
102
+ </Layout>
103
+ }
104
+ />
95
105
  <Route
96
106
  key="customer-embed"
97
107
  path="/customer/embed/subscription"
@@ -1,9 +1,9 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { api, formatAmount, formatTime } from '@blocklet/payment-react';
2
+ import { api, dayjs, formatAmount, formatTime } from '@blocklet/payment-react';
3
3
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
4
4
  import { Box, Divider, FormControlLabel, Radio, RadioGroup, Stack, TextField, Typography, styled } from '@mui/material';
5
5
  import { useRequest } from 'ahooks';
6
- import { useEffect } from 'react';
6
+ import { useEffect, useMemo } from 'react';
7
7
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
8
8
 
9
9
  const fetchData = (id: string, time: string): Promise<{ total: string; unused: string }> => {
@@ -21,13 +21,23 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
21
21
  const cancelTime = useWatch({ control, name: 'cancel.time' });
22
22
  const refundType = useWatch({ control, name: 'cancel.refund' });
23
23
  const stakingType = useWatch({ control, name: 'cancel.staking' });
24
- const {
25
- loading,
26
- data: refund,
27
- refresh,
28
- } = useRequest(() => fetchData(data.id, cancelAt === 'custom' ? cancelTime : ''));
24
+ const actualCancelAt = useMemo(() => {
25
+ if (cancelAt === 'custom') {
26
+ return cancelTime;
27
+ }
28
+ if (cancelAt === 'current_period_end') {
29
+ return new Date(data.current_period_end * 1000);
30
+ }
31
+ return '';
32
+ }, [cancelAt, cancelTime]);
33
+ const { loading, data: refund, refresh } = useRequest(() => fetchData(data.id, actualCancelAt));
29
34
 
30
- const { data: staking } = useRequest(() => fetchStakingData(data.id, cancelAt === 'custom' ? cancelTime : ''));
35
+ const { data: staking } = useRequest(() => {
36
+ if (data.paymentMethod?.type === 'arcblock') {
37
+ return fetchStakingData(data.id, actualCancelAt);
38
+ }
39
+ return Promise.resolve({ return_amount: '0', slash_amount: '0' });
40
+ });
31
41
  useEffect(() => {
32
42
  if (data) {
33
43
  refresh();
@@ -71,7 +81,18 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
71
81
  {isCustom && (
72
82
  <Controller
73
83
  name="cancel.time"
74
- rules={{ required: isCustom }}
84
+ rules={{
85
+ required: isCustom,
86
+ validate: (value) => {
87
+ const now = dayjs();
88
+ const selectedTime = dayjs(value);
89
+ const periodEndTime = dayjs.unix(data.current_period_end);
90
+ if (selectedTime.isBefore(now) || selectedTime.isAfter(periodEndTime)) {
91
+ return t('admin.subscription.cancel.at.timeError');
92
+ }
93
+ return true;
94
+ },
95
+ }}
75
96
  control={control}
76
97
  render={({ field }) => (
77
98
  <TextField
@@ -44,9 +44,17 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
44
44
  slash_amount: '0',
45
45
  },
46
46
  runAsync: fetchStakeResultAsync,
47
- } = useRequest(() => fetchStakingData(data.id, ''), {
48
- manual: true,
49
- });
47
+ } = useRequest(
48
+ () => {
49
+ if (data.paymentMethod?.type === 'arcblock') {
50
+ return fetchStakingData(data.id, '');
51
+ }
52
+ return Promise.resolve({ return_amount: '0', slash_amount: '0', total: '0' });
53
+ },
54
+ {
55
+ manual: true,
56
+ }
57
+ );
50
58
 
51
59
  const stakeValue = useMemo(() => {
52
60
  return formatBNStr(stakeResult?.slash_amount, data?.paymentCurrency?.decimal);
@@ -2,7 +2,7 @@
2
2
  import CodeBlock from '@arcblock/ux/lib/CodeBlock';
3
3
  import { api, formatTime } from '@blocklet/payment-react';
4
4
  import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
5
- import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
5
+ import { CheckCircleOutlined, ErrorOutlined, InfoOutlined } from '@mui/icons-material';
6
6
  import {
7
7
  Box,
8
8
  Button,
@@ -15,12 +15,15 @@ import {
15
15
  ListSubheader,
16
16
  Stack,
17
17
  Typography,
18
+ Popper,
19
+ Paper,
18
20
  } from '@mui/material';
19
21
  import { useInfiniteScroll } from 'ahooks';
20
22
  import React, { useEffect, useState } from 'react';
21
23
 
22
24
  import { isEmpty } from 'lodash';
23
25
  import { isSuccessAttempt } from '../../libs/util';
26
+ import InfoCard from '../info-card';
24
27
 
25
28
  const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TWebhookAttemptExpanded>> => {
26
29
  const search = new URLSearchParams();
@@ -45,7 +48,7 @@ const groupAttemptsByDate = (attempts: TWebhookAttemptExpanded[]) => {
45
48
  type Props = {
46
49
  event_id?: string;
47
50
  webhook_endpoint_id?: string;
48
- event?: TEvent;
51
+ event?: TEvent & { requestInfo?: { avatar: string; email: string; did: string } };
49
52
  };
50
53
 
51
54
  WebhookAttempts.defaultProps = {
@@ -82,6 +85,35 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
82
85
  setSelected(attempt);
83
86
  };
84
87
 
88
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
89
+
90
+ const handleClick = (e: React.MouseEvent<HTMLElement>) => {
91
+ setAnchorEl(anchorEl ? null : e.currentTarget);
92
+ };
93
+
94
+ useEffect(() => {
95
+ const handleClickOutside = (e: MouseEvent) => {
96
+ if (anchorEl && !anchorEl.contains(e.target as Node) && !(e.target as Element).closest('.popper-content')) {
97
+ setAnchorEl(null);
98
+ }
99
+ };
100
+
101
+ const handleScroll = (e: Event) => {
102
+ // @ts-ignore
103
+ if (anchorEl && !e.target?.closest('.popper-content')) {
104
+ setAnchorEl(null);
105
+ }
106
+ };
107
+
108
+ document.addEventListener('click', handleClickOutside);
109
+ window.addEventListener('scroll', handleScroll, true);
110
+
111
+ return () => {
112
+ document.removeEventListener('click', handleClickOutside);
113
+ window.removeEventListener('scroll', handleScroll, true);
114
+ };
115
+ }, [anchorEl]);
116
+
85
117
  if (loading) {
86
118
  return <CircularProgress />;
87
119
  }
@@ -146,7 +178,94 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
146
178
  )}
147
179
  {data?.list.length === 0 && event && (
148
180
  <Box>
149
- <Typography variant="h6">Event Data</Typography>
181
+ <Stack direction="row" alignItems="center" spacing={1}>
182
+ <Typography variant="h6">Event Data</Typography>
183
+ <>
184
+ {/* @ts-ignore */}
185
+ <InfoOutlined
186
+ fontSize="small"
187
+ onClick={handleClick}
188
+ sx={{
189
+ color: 'text.secondary',
190
+ opacity: 0.6,
191
+ cursor: 'pointer',
192
+ }}
193
+ />
194
+ <Popper
195
+ open={Boolean(anchorEl)}
196
+ anchorEl={anchorEl}
197
+ placement="right"
198
+ sx={{
199
+ zIndex: 1000,
200
+ '@media (max-width: 600px)': {
201
+ '& .MuiPaper-root': {
202
+ width: 'calc(100vw - 32px)',
203
+ maxWidth: 'none',
204
+ },
205
+ },
206
+ }}
207
+ modifiers={[
208
+ {
209
+ name: 'preventOverflow',
210
+ options: {
211
+ boundary: window,
212
+ altAxis: true,
213
+ padding: 16,
214
+ },
215
+ },
216
+ {
217
+ name: 'flip',
218
+ options: {
219
+ fallbackPlacements: ['bottom'],
220
+ },
221
+ },
222
+ {
223
+ name: 'matchWidth',
224
+ enabled: true,
225
+ fn: ({ state }) => {
226
+ if (window.innerWidth <= 600) {
227
+ state.styles.popper = {
228
+ ...state.styles.popper,
229
+ width: 'calc(100vw - 32px)',
230
+ maxWidth: 'none',
231
+ };
232
+ }
233
+ return state;
234
+ },
235
+ },
236
+ ]}>
237
+ <Paper
238
+ className="popper-content"
239
+ elevation={3}
240
+ sx={{
241
+ p: 2,
242
+ border: '1px solid',
243
+ borderColor: 'divider',
244
+ maxWidth: 300,
245
+ '@media (max-width: 600px)': {
246
+ maxWidth: 'none',
247
+ margin: '0 auto',
248
+ },
249
+ }}>
250
+ {event.requestInfo ? (
251
+ <>
252
+ <Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
253
+ Requested by:
254
+ </Typography>
255
+ <InfoCard
256
+ logo={event.requestInfo.avatar}
257
+ name={event.requestInfo.email}
258
+ description={event.requestInfo.did || event.request.requested_by}
259
+ size={40}
260
+ />
261
+ </>
262
+ ) : (
263
+ <Typography>Requested by: {event.request?.requested_by || 'system'}</Typography>
264
+ )}
265
+ </Paper>
266
+ </Popper>
267
+ </>
268
+ </Stack>
150
269
  <CodeBlock language="json">{JSON.stringify(event.data, null, 2)}</CodeBlock>
151
270
  </Box>
152
271
  )}
@@ -457,6 +457,7 @@ export default flat({
457
457
  now: 'Immediately ({date})',
458
458
  current_period_end: 'End of current period ({date})',
459
459
  custom: 'On a custom date',
460
+ timeError: 'Cancel time must be within the current period',
460
461
  },
461
462
  refund: {
462
463
  title: 'Refund',
@@ -616,5 +617,17 @@ export default flat({
616
617
  product: {
617
618
  empty: 'No Product',
618
619
  },
620
+ recharge: {
621
+ title: 'Recharge',
622
+ amount: 'Amount',
623
+ submit: 'Submit',
624
+ unsupported: 'Unsupported currency, please select another one',
625
+ receiveAddress: 'Receive Address',
626
+ view: 'View Subscription',
627
+ success: 'Recharge successfully',
628
+ custom: 'Custom',
629
+ estimatedDuration: '{duration} {unit} est.',
630
+ intervals: 'intervals',
631
+ },
619
632
  },
620
633
  });
@@ -447,6 +447,7 @@ export default flat({
447
447
  now: '立即取消({date})',
448
448
  current_period_end: '本周期结束后({date})',
449
449
  custom: '自定义取消日期',
450
+ timeError: '取消时间必须在当前周期内',
450
451
  },
451
452
  refund: {
452
453
  title: '退款',
@@ -604,5 +605,17 @@ export default flat({
604
605
  product: {
605
606
  empty: '没有订阅产品',
606
607
  },
608
+ recharge: {
609
+ title: '充值',
610
+ amount: '金额',
611
+ submit: '提交',
612
+ unsupported: '暂不支持该货币充值,请选择其他货币',
613
+ receiveAddress: '收款地址',
614
+ view: '查看订阅',
615
+ success: '充值成功',
616
+ estimatedDuration: '预计可用 {duration} {unit}',
617
+ custom: '自定义',
618
+ intervals: '个周期',
619
+ },
607
620
  },
608
621
  });