payment-kit 1.16.17 → 1.16.18

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 (66) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/hooks/pre-start.ts +2 -0
  3. package/api/src/index.ts +2 -0
  4. package/api/src/integrations/arcblock/stake.ts +7 -1
  5. package/api/src/integrations/stripe/resource.ts +1 -1
  6. package/api/src/libs/env.ts +12 -0
  7. package/api/src/libs/event.ts +8 -0
  8. package/api/src/libs/invoice.ts +585 -3
  9. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
  10. package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
  12. package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
  13. package/api/src/libs/overdraft-protection.ts +85 -0
  14. package/api/src/libs/payment.ts +1 -65
  15. package/api/src/libs/queue/index.ts +0 -1
  16. package/api/src/libs/subscription.ts +532 -2
  17. package/api/src/libs/util.ts +4 -0
  18. package/api/src/locales/en.ts +5 -0
  19. package/api/src/locales/zh.ts +5 -0
  20. package/api/src/queues/event.ts +3 -2
  21. package/api/src/queues/invoice.ts +28 -3
  22. package/api/src/queues/notification.ts +25 -3
  23. package/api/src/queues/payment.ts +154 -3
  24. package/api/src/queues/refund.ts +2 -2
  25. package/api/src/queues/subscription.ts +215 -4
  26. package/api/src/queues/webhook.ts +1 -0
  27. package/api/src/routes/connect/change-payment.ts +1 -1
  28. package/api/src/routes/connect/change-plan.ts +1 -1
  29. package/api/src/routes/connect/overdraft-protection.ts +120 -0
  30. package/api/src/routes/connect/recharge.ts +2 -1
  31. package/api/src/routes/connect/setup.ts +1 -1
  32. package/api/src/routes/connect/shared.ts +117 -350
  33. package/api/src/routes/connect/subscribe.ts +1 -1
  34. package/api/src/routes/customers.ts +2 -2
  35. package/api/src/routes/invoices.ts +9 -4
  36. package/api/src/routes/subscriptions.ts +172 -2
  37. package/api/src/store/migrate.ts +9 -10
  38. package/api/src/store/migrations/20240905-index.ts +95 -60
  39. package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
  40. package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
  41. package/api/src/store/models/customer.ts +2 -2
  42. package/api/src/store/models/invoice.ts +7 -0
  43. package/api/src/store/models/lock.ts +7 -0
  44. package/api/src/store/models/subscription.ts +15 -0
  45. package/api/src/store/sequelize.ts +6 -1
  46. package/blocklet.yml +1 -1
  47. package/package.json +23 -23
  48. package/src/components/customer/overdraft-protection.tsx +367 -0
  49. package/src/components/event/list.tsx +3 -4
  50. package/src/components/subscription/actions/cancel.tsx +3 -0
  51. package/src/components/subscription/portal/actions.tsx +324 -77
  52. package/src/components/uploader.tsx +31 -26
  53. package/src/env.d.ts +1 -0
  54. package/src/hooks/subscription.ts +30 -0
  55. package/src/libs/env.ts +4 -0
  56. package/src/locales/en.tsx +41 -0
  57. package/src/locales/zh.tsx +37 -0
  58. package/src/pages/admin/billing/invoices/detail.tsx +16 -15
  59. package/src/pages/customer/index.tsx +7 -2
  60. package/src/pages/customer/invoice/detail.tsx +29 -5
  61. package/src/pages/customer/invoice/past-due.tsx +18 -4
  62. package/src/pages/customer/recharge.tsx +2 -4
  63. package/src/pages/customer/subscription/change-payment.tsx +7 -1
  64. package/src/pages/customer/subscription/detail.tsx +69 -51
  65. package/tsconfig.json +0 -5
  66. package/api/tests/libs/payment.spec.ts +0 -168
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.16.17",
3
+ "version": "1.16.18",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,30 +43,30 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@abtnode/cron": "^1.16.33",
47
- "@arcblock/did": "^1.18.161",
46
+ "@abtnode/cron": "^1.16.36",
47
+ "@arcblock/did": "^1.18.165",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.10.88",
50
- "@arcblock/did-util": "^1.18.161",
51
- "@arcblock/jwt": "^1.18.161",
52
- "@arcblock/ux": "^2.10.88",
53
- "@arcblock/validator": "^1.18.161",
54
- "@blocklet/js-sdk": "^1.16.33",
55
- "@blocklet/logger": "^1.16.33",
56
- "@blocklet/payment-react": "1.16.17",
57
- "@blocklet/sdk": "^1.16.33",
58
- "@blocklet/ui-react": "^2.10.88",
59
- "@blocklet/uploader": "^0.1.58",
60
- "@blocklet/xss": "^0.1.15",
49
+ "@arcblock/did-connect": "^2.11.15",
50
+ "@arcblock/did-util": "^1.18.165",
51
+ "@arcblock/jwt": "^1.18.165",
52
+ "@arcblock/ux": "^2.11.15",
53
+ "@arcblock/validator": "^1.18.165",
54
+ "@blocklet/js-sdk": "^1.16.36",
55
+ "@blocklet/logger": "^1.16.36",
56
+ "@blocklet/payment-react": "1.16.18",
57
+ "@blocklet/sdk": "^1.16.36",
58
+ "@blocklet/ui-react": "^2.11.15",
59
+ "@blocklet/uploader": "^0.1.60",
60
+ "@blocklet/xss": "^0.1.17",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
63
63
  "@mui/material": "^5.16.6",
64
64
  "@mui/system": "^5.16.6",
65
- "@ocap/asset": "^1.18.161",
66
- "@ocap/client": "^1.18.161",
67
- "@ocap/mcrypto": "^1.18.161",
68
- "@ocap/util": "^1.18.161",
69
- "@ocap/wallet": "^1.18.161",
65
+ "@ocap/asset": "^1.18.165",
66
+ "@ocap/client": "^1.18.165",
67
+ "@ocap/mcrypto": "^1.18.165",
68
+ "@ocap/util": "^1.18.165",
69
+ "@ocap/wallet": "^1.18.165",
70
70
  "@stripe/react-stripe-js": "^2.7.3",
71
71
  "@stripe/stripe-js": "^2.4.0",
72
72
  "ahooks": "^3.8.0",
@@ -118,9 +118,9 @@
118
118
  "validator": "^13.12.0"
119
119
  },
120
120
  "devDependencies": {
121
- "@abtnode/types": "^1.16.33",
121
+ "@abtnode/types": "^1.16.36",
122
122
  "@arcblock/eslint-config-ts": "^0.3.3",
123
- "@blocklet/payment-types": "1.16.17",
123
+ "@blocklet/payment-types": "1.16.18",
124
124
  "@types/cookie-parser": "^1.4.7",
125
125
  "@types/cors": "^2.8.17",
126
126
  "@types/debug": "^4.1.12",
@@ -166,5 +166,5 @@
166
166
  "parser": "typescript"
167
167
  }
168
168
  },
169
- "gitHead": "38b1da1771951e6a30f3cb643fadb108aceed5b6"
169
+ "gitHead": "ea644d08efdbfba6034a3ec09eac4d7160bcba66"
170
170
  }
@@ -0,0 +1,367 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { useForm } from 'react-hook-form';
3
+ import {
4
+ Button,
5
+ CircularProgress,
6
+ Stack,
7
+ TextField,
8
+ Typography,
9
+ FormControlLabel,
10
+ Alert,
11
+ Box,
12
+ Card,
13
+ CardActionArea,
14
+ Grid,
15
+ Tooltip,
16
+ FormControl,
17
+ RadioGroup,
18
+ Radio,
19
+ } from '@mui/material';
20
+ import Dialog from '@arcblock/ux/lib/Dialog';
21
+ import { EventHandler, useState } from 'react';
22
+ import { api, Switch, useMobile } from '@blocklet/payment-react';
23
+ import { useRequest } from 'ahooks';
24
+ import { BN, fromUnitToToken } from '@ocap/util';
25
+ import type { TPaymentCurrency, TSubscriptionExpanded } from '@blocklet/payment-types';
26
+ import Currency from '../currency';
27
+
28
+ const fetchCycleAmount = (
29
+ subscriptionId: string,
30
+ params: { overdraftProtection: boolean }
31
+ ): Promise<{ amount: string; gas: string; currency: TPaymentCurrency }> => {
32
+ return api.get(`/api/subscriptions/${subscriptionId}/cycle-amount`, { params }).then((res) => res.data);
33
+ };
34
+
35
+ type OverdraftProtectionDialogProps = {
36
+ value: {
37
+ enabled: boolean;
38
+ remaining: string;
39
+ unused: string;
40
+ used: string;
41
+ };
42
+ loading: boolean;
43
+ onSave: (data: { enabled: boolean; additionalCount?: number; returnRemaining?: boolean }) => Promise<void>;
44
+ onCancel: EventHandler<any>;
45
+ open: boolean;
46
+ paymentAddress?: string;
47
+ currency: {
48
+ symbol: string;
49
+ logo: string;
50
+ decimal: number;
51
+ };
52
+ subscription: TSubscriptionExpanded;
53
+ };
54
+
55
+ OverdraftProtectionDialog.defaultProps = {
56
+ paymentAddress: '',
57
+ };
58
+
59
+ export default function OverdraftProtectionDialog({
60
+ value,
61
+ loading,
62
+ onSave,
63
+ onCancel,
64
+ open,
65
+ paymentAddress,
66
+ currency,
67
+ subscription,
68
+ }: OverdraftProtectionDialogProps) {
69
+ const { t } = useLocaleContext();
70
+ const { isMobile } = useMobile();
71
+ const [customAmount, setCustomAmount] = useState(false);
72
+ const [presetAmounts, setPresetAmounts] = useState<{ amount: string; cycles: number }[]>([]);
73
+
74
+ const {
75
+ data: cycleAmount = {
76
+ amount: '0',
77
+ gas: '0',
78
+ },
79
+ } = useRequest(
80
+ () =>
81
+ fetchCycleAmount(subscription.id, {
82
+ overdraftProtection: true,
83
+ }),
84
+ {
85
+ refreshDeps: [subscription.id],
86
+ onSuccess: (data) => {
87
+ const presets = [1, 2, 5, 10];
88
+ const getCycleAmount = (cycles: number) =>
89
+ fromUnitToToken(new BN(data.amount).mul(new BN(cycles)).toString(), data?.currency?.decimal);
90
+ setPresetAmounts(presets.map((cycles) => ({ amount: getCycleAmount(cycles), cycles })));
91
+ },
92
+ }
93
+ );
94
+
95
+ const methods = useForm<{
96
+ enabled: boolean;
97
+ return_stake: boolean;
98
+ amount: string;
99
+ }>({
100
+ defaultValues: {
101
+ enabled: !!subscription.overdraft_protection?.enabled,
102
+ return_stake: false,
103
+ amount: '0',
104
+ },
105
+ mode: 'onChange',
106
+ });
107
+
108
+ const isEnabled = methods.watch('enabled');
109
+ const amount = methods.watch('amount');
110
+
111
+ const estimateAmount = fromUnitToToken(cycleAmount.amount, currency?.decimal);
112
+ const availableAmount = fromUnitToToken(value.unused, currency?.decimal);
113
+ const dueAmount = fromUnitToToken(value.used, currency?.decimal);
114
+ const minStake = fromUnitToToken(new BN(cycleAmount.amount).sub(new BN(value.unused)).toString(), currency?.decimal);
115
+
116
+ const handleCustomSelect = () => {
117
+ setCustomAmount(true);
118
+ methods.setValue('amount', Number(minStake) < 0 ? estimateAmount : minStake);
119
+ };
120
+
121
+ const onSubmit = () => {
122
+ methods.handleSubmit(async (formData) => {
123
+ await onSave(formData);
124
+ methods.reset();
125
+ })();
126
+ };
127
+
128
+ const formatEstimatedDuration = (cycles: number) => {
129
+ const { interval, interval_count: intervalCount } = subscription.pending_invoice_item_interval;
130
+ const totalIntervals = cycles * intervalCount;
131
+ const availableUnitKeys = ['hour', 'day', 'week', 'month', 'year'];
132
+
133
+ const unitKey = availableUnitKeys.includes(interval)
134
+ ? `common.${interval}${totalIntervals > 1 ? 's' : ''}`
135
+ : 'customer.overdraftProtection.intervals';
136
+
137
+ return t('customer.overdraftProtection.estimatedDuration', {
138
+ duration: totalIntervals,
139
+ unit: t(unitKey).toLowerCase(),
140
+ });
141
+ };
142
+
143
+ return (
144
+ <Dialog
145
+ open={open}
146
+ onClose={onCancel}
147
+ maxWidth="sm"
148
+ fullWidth
149
+ title={t('customer.overdraftProtection.setting')}
150
+ actions={
151
+ <Stack direction="row" spacing={2}>
152
+ <Button variant="outlined" onClick={onCancel}>
153
+ {t('common.cancel')}
154
+ </Button>
155
+ <Button variant="contained" color="primary" disabled={loading} onClick={onSubmit}>
156
+ {loading && <CircularProgress size={20} sx={{ mr: 1 }} />}
157
+ {t('common.submit')}
158
+ </Button>
159
+ </Stack>
160
+ }>
161
+ <Stack gap={2}>
162
+ <Alert severity="info">
163
+ {value.used !== '0' && !isEnabled ? (
164
+ <>{t('customer.overdraftProtection.disableConfirm')}</>
165
+ ) : (
166
+ <Box>
167
+ {t('customer.overdraftProtection.tip')}
168
+ <Tooltip
169
+ title={t('customer.overdraftProtection.ruleTip', {
170
+ gas: fromUnitToToken(cycleAmount.gas, currency?.decimal),
171
+ symbol: currency.symbol,
172
+ })}
173
+ placement="top">
174
+ <Typography variant="body2" sx={{ color: 'text.link', cursor: 'pointer', mt: 1 }}>
175
+ {t('customer.overdraftProtection.rule')}
176
+ </Typography>
177
+ </Tooltip>
178
+ </Box>
179
+ )}
180
+ </Alert>
181
+
182
+ <Stack spacing={2} direction="row" alignItems="center">
183
+ <Typography variant="subtitle1" color="text.secondary">
184
+ {t('customer.overdraftProtection.title')}
185
+ </Typography>
186
+ <FormControlLabel
187
+ control={<Switch checked={isEnabled} onChange={(e) => methods.setValue('enabled', e.target.checked)} />}
188
+ label=""
189
+ />
190
+ </Stack>
191
+
192
+ {isEnabled ? (
193
+ <Stack gap={1} sx={{ mt: '-8px' }}>
194
+ {paymentAddress && (
195
+ <Stack direction="row" alignItems="center" gap={isMobile ? 1 : 2} flexWrap="wrap">
196
+ <Typography variant="subtitle2" color="text.secondary">
197
+ {t('customer.overdraftProtection.address')}
198
+ </Typography>
199
+ <Typography variant="body2" sx={{ wordBreak: 'break-all', fontFamily: 'monospace', fontWeight: '700' }}>
200
+ {paymentAddress}
201
+ </Typography>
202
+ </Stack>
203
+ )}
204
+ <Typography variant="body2" color="text.secondary">
205
+ {(() => {
206
+ if (Number(dueAmount) > 0 && !value.enabled) {
207
+ return t('customer.overdraftProtection.remainingNotEnough', {
208
+ due: dueAmount,
209
+ symbol: currency.symbol,
210
+ min: Number(minStake) < 0 ? estimateAmount : minStake,
211
+ unused: availableAmount,
212
+ });
213
+ }
214
+ if (value.remaining === '0') {
215
+ return t('customer.overdraftProtection.noRemaining', {
216
+ estimateAmount,
217
+ symbol: currency.symbol,
218
+ });
219
+ }
220
+ return t('customer.overdraftProtection.remaining', {
221
+ amount: availableAmount,
222
+ symbol: currency.symbol,
223
+ estimateAmount,
224
+ });
225
+ })()}
226
+ </Typography>
227
+
228
+ <Grid container spacing={2} ml={-2} sx={{ mt: -1 }}>
229
+ {presetAmounts.map(({ amount: presetAmount, cycles }) => (
230
+ <Grid item xs={6} sm={4} key={presetAmount}>
231
+ <Card
232
+ variant="outlined"
233
+ sx={{
234
+ height: '100%',
235
+ transition: 'all 0.3s',
236
+ cursor: 'pointer',
237
+ '&:hover': {
238
+ transform: 'translateY(-4px)',
239
+ boxShadow: 3,
240
+ },
241
+ ...(amount === presetAmount && !customAmount
242
+ ? { borderColor: 'primary.main', borderWidth: 2 }
243
+ : {}),
244
+ }}>
245
+ <CardActionArea
246
+ onClick={() => {
247
+ methods.setValue('amount', presetAmount);
248
+ setCustomAmount(false);
249
+ }}
250
+ sx={{ height: '100%', p: 1 }}>
251
+ <Stack spacing={1} alignItems="center">
252
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
253
+ {presetAmount} {currency.symbol}
254
+ </Typography>
255
+ <Typography variant="caption" color="text.secondary">
256
+ {formatEstimatedDuration(cycles)}
257
+ </Typography>
258
+ </Stack>
259
+ </CardActionArea>
260
+ </Card>
261
+ </Grid>
262
+ ))}
263
+ <Grid item xs={6} sm={4}>
264
+ <Card
265
+ variant="outlined"
266
+ sx={{
267
+ height: '100%',
268
+ transition: 'all 0.3s',
269
+ cursor: 'pointer',
270
+ '&:hover': {
271
+ transform: 'translateY(-4px)',
272
+ boxShadow: 3,
273
+ },
274
+ ...(customAmount ? { borderColor: 'primary.main', borderWidth: 2 } : {}),
275
+ }}>
276
+ <CardActionArea onClick={handleCustomSelect} sx={{ height: '100%', p: 2 }}>
277
+ <Stack spacing={1} alignItems="center">
278
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
279
+ {t('common.custom')}
280
+ </Typography>
281
+ </Stack>
282
+ </CardActionArea>
283
+ </Card>
284
+ </Grid>
285
+ </Grid>
286
+
287
+ {customAmount && (
288
+ <Stack spacing={2} sx={{ mt: 1 }}>
289
+ <FormControl fullWidth>
290
+ <TextField
291
+ {...methods.register('amount', {
292
+ required: true,
293
+ validate: (val) => {
294
+ const required = Number(minStake) < 0 ? Number(estimateAmount) : Number(minStake);
295
+ if (Number(val) < required) {
296
+ return t('customer.overdraftProtection.min', {
297
+ min: required,
298
+ symbol: currency.symbol,
299
+ });
300
+ }
301
+ return true;
302
+ },
303
+ })}
304
+ label={t('common.amount')}
305
+ type="number"
306
+ error={!!methods.formState.errors.amount}
307
+ helperText={methods.formState.errors.amount?.message}
308
+ fullWidth
309
+ variant="outlined"
310
+ InputProps={{
311
+ endAdornment: (
312
+ <Box sx={{ ml: 1, display: 'flex', alignItems: 'center' }}>
313
+ <Currency logo={currency.logo} name={currency.symbol} />
314
+ </Box>
315
+ ),
316
+ inputProps: {
317
+ min: 0,
318
+ step: Number(estimateAmount),
319
+ },
320
+ }}
321
+ />
322
+ </FormControl>
323
+ </Stack>
324
+ )}
325
+ {amount && Number(amount) > 0 && !methods.formState.errors.amount && (
326
+ <Typography variant="body2" sx={{ color: 'text.lighter', mt: '8px !important' }} fontSize={12}>
327
+ {t('customer.overdraftProtection.total', {
328
+ total: Number(amount) + Number(availableAmount),
329
+ symbol: currency.symbol,
330
+ })}
331
+ {formatEstimatedDuration(
332
+ Math.floor((Number(amount) + Number(availableAmount)) / Number(estimateAmount))
333
+ )}
334
+ </Typography>
335
+ )}
336
+ </Stack>
337
+ ) : (
338
+ Number(availableAmount) > 0 && (
339
+ <Stack direction="row" alignItems="flex-start" gap={2}>
340
+ <Typography
341
+ variant="subtitle2"
342
+ color="text.secondary"
343
+ sx={{ height: 39, display: 'flex', alignItems: 'center', width: 56 }}>
344
+ {t('customer.overdraftProtection.stake')}
345
+ </Typography>
346
+ <RadioGroup
347
+ aria-labelledby="return-stake-group-label"
348
+ value={methods.watch('return_stake')}
349
+ onChange={(e) => methods.setValue('return_stake', e.target.value === 'true')}>
350
+ <FormControlLabel
351
+ value={false}
352
+ control={<Radio />}
353
+ label={t('customer.overdraftProtection.keepStake')}
354
+ />
355
+ <FormControlLabel
356
+ value
357
+ control={<Radio color="warning" />}
358
+ label={t('customer.overdraftProtection.returnStake')}
359
+ />
360
+ </RadioGroup>
361
+ </Stack>
362
+ )
363
+ )}
364
+ </Stack>
365
+ </Dialog>
366
+ );
367
+ }
@@ -1,14 +1,14 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
- import { api, formatTime, Table, useDefaultPageSize } from '@blocklet/payment-react';
4
+ import { api, formatTime, getPrefix, Table, useDefaultPageSize } from '@blocklet/payment-react';
5
5
  import type { TEventExpanded } from '@blocklet/payment-types';
6
6
  import { Alert, Box, CircularProgress, Typography } from '@mui/material';
7
7
  import { useRequest } from 'ahooks';
8
8
  import { useEffect, useState } from 'react';
9
- import { useNavigate } from 'react-router-dom';
10
9
 
11
10
  import { styled } from '@mui/system';
11
+ import { joinURL } from 'ufo';
12
12
  import { useTransitionContext } from '../progress-bar';
13
13
 
14
14
  const fetchData = (params: Record<string, any> = {}): Promise<{ list: TEventExpanded[]; count: number }> => {
@@ -134,7 +134,6 @@ export default function EventList({ type, object_id, features }: ListProps) {
134
134
 
135
135
  const { t } = useLocaleContext();
136
136
  const defaultPageSize = useDefaultPageSize(persisted.rowsPerPage || 50);
137
- const navigate = useNavigate();
138
137
  const [search, setSearch] = useState<SearchProps>({
139
138
  type,
140
139
  object_id,
@@ -212,7 +211,7 @@ export default function EventList({ type, object_id, features }: ListProps) {
212
211
  onRowClick: (_: any, { dataIndex }: any) => {
213
212
  const item = data.list[dataIndex] as TEventExpanded;
214
213
  startTransition(() => {
215
- navigate(`/admin/developers/${item.id}`);
214
+ window.open(joinURL(getPrefix(), `/admin/developers/${item.id}`), '_blank');
216
215
  });
217
216
  },
218
217
  }}
@@ -220,5 +220,8 @@ export default function SubscriptionCancelForm({ data }: { data: TSubscriptionEx
220
220
  const Root = styled(Box)`
221
221
  .form-title {
222
222
  width: 60px;
223
+ height: 39px;
224
+ display: flex;
225
+ align-items: center;
223
226
  }
224
227
  `;