payment-kit 1.18.24 → 1.18.26

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 (41) hide show
  1. package/api/src/libs/event.ts +22 -2
  2. package/api/src/libs/invoice.ts +142 -0
  3. package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
  4. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
  5. package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
  8. package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
  12. package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
  13. package/api/src/libs/queue/index.ts +6 -0
  14. package/api/src/libs/queue/store.ts +13 -1
  15. package/api/src/libs/util.ts +22 -1
  16. package/api/src/locales/en.ts +5 -0
  17. package/api/src/locales/zh.ts +5 -0
  18. package/api/src/queues/invoice.ts +21 -7
  19. package/api/src/queues/notification.ts +353 -11
  20. package/api/src/queues/payment.ts +26 -10
  21. package/api/src/queues/payout.ts +21 -7
  22. package/api/src/routes/checkout-sessions.ts +26 -12
  23. package/api/src/routes/connect/recharge-account.ts +13 -1
  24. package/api/src/routes/connect/recharge.ts +13 -1
  25. package/api/src/routes/connect/shared.ts +54 -36
  26. package/api/src/routes/customers.ts +61 -0
  27. package/api/src/routes/invoices.ts +51 -1
  28. package/api/src/routes/subscriptions.ts +1 -1
  29. package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
  30. package/api/src/store/models/customer.ts +42 -1
  31. package/api/src/store/models/types.ts +17 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +24 -24
  34. package/src/components/customer/form.tsx +21 -2
  35. package/src/components/customer/notification-preference.tsx +428 -0
  36. package/src/components/layout/user.tsx +1 -1
  37. package/src/locales/en.tsx +30 -0
  38. package/src/locales/zh.tsx +30 -0
  39. package/src/pages/customer/index.tsx +27 -23
  40. package/src/pages/customer/recharge/account.tsx +19 -17
  41. package/src/pages/customer/subscription/embed.tsx +25 -9
@@ -0,0 +1,428 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import Dialog from '@arcblock/ux/lib/Dialog';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { NotificationsOutlined, DateRangeOutlined } from '@mui/icons-material';
5
+ import {
6
+ Button,
7
+ CircularProgress,
8
+ Stack,
9
+ Typography,
10
+ FormControl,
11
+ Select,
12
+ MenuItem,
13
+ TextField,
14
+ Box,
15
+ Paper,
16
+ Alert,
17
+ Popper,
18
+ ClickAwayListener,
19
+ } from '@mui/material';
20
+ import { useRequest } from 'ahooks';
21
+ import { FormProvider, useForm, Controller, Control } from 'react-hook-form';
22
+ import { useMobile } from '@blocklet/payment-react';
23
+ import api from '../../libs/api';
24
+
25
+ export type NotificationFrequency = 'default' | 'daily' | 'weekly' | 'monthly';
26
+
27
+ export interface NotificationPreferences {
28
+ notification: {
29
+ frequency: NotificationFrequency;
30
+ schedule?: {
31
+ time: string;
32
+ date?: number;
33
+ };
34
+ };
35
+ }
36
+
37
+ interface NotificationPreferenceDialogProps {
38
+ open: boolean;
39
+ onClose: () => void;
40
+ }
41
+
42
+ const fetchPreferences = async () => {
43
+ const { data } = await api.get('/api/customers/me');
44
+ return data.preference || { notification: { frequency: 'default' } };
45
+ };
46
+
47
+ const updatePreferences = async (preferences: NotificationPreferences) => {
48
+ const { data } = await api.put('/api/customers/preference', preferences);
49
+ return data;
50
+ };
51
+
52
+ const validateTimeFormat = (time: string) => {
53
+ const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
54
+ return timeRegex.test(time);
55
+ };
56
+
57
+ function TimeSelector({
58
+ name,
59
+ control,
60
+ required,
61
+ timeFormatErrorMessage,
62
+ }: {
63
+ name: string;
64
+ control: Control<any>;
65
+ required: boolean;
66
+ timeFormatErrorMessage: string;
67
+ }) {
68
+ return (
69
+ <Controller
70
+ name={name}
71
+ control={control}
72
+ rules={{
73
+ required,
74
+ validate: (value) => validateTimeFormat(value) || timeFormatErrorMessage,
75
+ }}
76
+ render={({ field, fieldState }) => (
77
+ <TextField
78
+ type="time"
79
+ size="small"
80
+ sx={{
81
+ minWidth: 'fit-content',
82
+ }}
83
+ {...field}
84
+ error={!!fieldState.error}
85
+ helperText={fieldState.error?.message}
86
+ />
87
+ )}
88
+ />
89
+ );
90
+ }
91
+
92
+ DaySelector.defaultProps = {
93
+ onBlur: () => {},
94
+ };
95
+
96
+ function DaySelector({
97
+ value,
98
+ onChange,
99
+ onBlur,
100
+ }: {
101
+ value: number;
102
+ onChange: (day: number) => void;
103
+ onBlur?: () => void;
104
+ }) {
105
+ const { t } = useLocaleContext();
106
+ const [open, setOpen] = useState(false);
107
+ const anchorRef = useRef<HTMLButtonElement>(null);
108
+
109
+ const handleClickAway = () => {
110
+ setOpen(false);
111
+ onBlur?.();
112
+ };
113
+
114
+ return (
115
+ <Box>
116
+ <Button
117
+ ref={anchorRef}
118
+ variant="outlined"
119
+ onClick={() => setOpen(!open)}
120
+ sx={{
121
+ minWidth: 'fit-content',
122
+ display: 'flex',
123
+ alignItems: 'center',
124
+ justifyContent: 'space-between',
125
+ backgroundColor: 'var(--backgrounds-bg-field)',
126
+ '&:hover, &:focus': {
127
+ borderColor: 'primary.main',
128
+ },
129
+ }}
130
+ size="large"
131
+ startIcon={<DateRangeOutlined fontSize="small" color="action" sx={{ fontSize: '1rem !important' }} />}
132
+ endIcon={
133
+ <Typography
134
+ variant="caption"
135
+ color="text.secondary"
136
+ sx={{ fontSize: '0.75rem !important', lineHeight: 'normal' }}>
137
+ {t('notification.preferences.day')}
138
+ </Typography>
139
+ }>
140
+ {value || 1}
141
+ </Button>
142
+
143
+ <Popper
144
+ open={open}
145
+ anchorEl={anchorRef.current}
146
+ placement="bottom-start"
147
+ style={{ zIndex: 1300 }}
148
+ modifiers={[
149
+ {
150
+ name: 'offset',
151
+ options: {
152
+ offset: [0, 8],
153
+ },
154
+ },
155
+ ]}>
156
+ <ClickAwayListener onClickAway={handleClickAway}>
157
+ <Paper
158
+ variant="outlined"
159
+ sx={{
160
+ p: 1,
161
+ borderRadius: 1,
162
+ width: '280px',
163
+ boxShadow: 3,
164
+ }}>
165
+ <Box
166
+ sx={{
167
+ display: 'grid',
168
+ gridTemplateColumns: 'repeat(7, 1fr)',
169
+ gap: 0.5,
170
+ }}>
171
+ {Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
172
+ <Button
173
+ key={day}
174
+ size="small"
175
+ variant={value === day ? 'contained' : 'text'}
176
+ onClick={() => {
177
+ onChange(day);
178
+ setOpen(false);
179
+ }}
180
+ sx={{
181
+ minWidth: 30,
182
+ height: 30,
183
+ p: 0,
184
+ }}>
185
+ {day}
186
+ </Button>
187
+ ))}
188
+ </Box>
189
+ </Paper>
190
+ </ClickAwayListener>
191
+ </Popper>
192
+ </Box>
193
+ );
194
+ }
195
+
196
+ export function NotificationPreferenceDialog({ open, onClose }: NotificationPreferenceDialogProps) {
197
+ const { t } = useLocaleContext();
198
+ const [selectedDate, setSelectedDate] = useState<number>(1);
199
+
200
+ const { data, loading } = useRequest(fetchPreferences, {
201
+ manual: false,
202
+ refreshDeps: [open],
203
+ });
204
+
205
+ const methods = useForm<NotificationPreferences>({
206
+ mode: 'onChange',
207
+ defaultValues: {
208
+ notification: {
209
+ frequency: 'default',
210
+ schedule: {
211
+ time: '10:00',
212
+ date: 1,
213
+ },
214
+ },
215
+ },
216
+ });
217
+
218
+ const {
219
+ handleSubmit,
220
+ watch,
221
+ reset,
222
+ control,
223
+ setValue,
224
+ formState: { isValid },
225
+ } = methods;
226
+
227
+ const frequency = watch('notification.frequency');
228
+ const dateValue = watch('notification.schedule.date');
229
+ const needsSchedule = frequency !== 'default';
230
+
231
+ useEffect(() => {
232
+ if (data && open) {
233
+ reset(data);
234
+ if (data.notification?.schedule?.date) {
235
+ setSelectedDate(data.notification.schedule.date);
236
+ }
237
+ }
238
+ }, [data, reset, open]);
239
+
240
+ useEffect(() => {
241
+ if (dateValue) {
242
+ setSelectedDate(dateValue);
243
+ }
244
+ }, [dateValue]);
245
+
246
+ const { loading: updating, runAsync: runUpdate } = useRequest(updatePreferences, {
247
+ manual: true,
248
+ onSuccess: () => {
249
+ onClose();
250
+ },
251
+ });
252
+
253
+ const onSubmit = (formData: NotificationPreferences) => {
254
+ runUpdate(formData);
255
+ };
256
+
257
+ const handleMonthDaySelect = (day: number) => {
258
+ setValue('notification.schedule.date', day, { shouldValidate: true });
259
+ setSelectedDate(day);
260
+ };
261
+
262
+ const getDayName = (dayIndex: number) => {
263
+ const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
264
+ return t(`common.days.${days[dayIndex]}`);
265
+ };
266
+
267
+ const handleFrequencyChange = (event: React.ChangeEvent<HTMLInputElement> | any) => {
268
+ setValue('notification.frequency', event.target.value as NotificationFrequency);
269
+ };
270
+
271
+ return (
272
+ <Dialog
273
+ open={open}
274
+ onClose={onClose}
275
+ disableEscapeKeyDown
276
+ title={
277
+ <Stack direction="row" alignItems="center" spacing={1}>
278
+ <NotificationsOutlined fontSize="small" />
279
+ <Typography variant="h6" component="span">
280
+ {t('notification.preferences.title')}
281
+ </Typography>
282
+ </Stack>
283
+ }
284
+ maxWidth="sm"
285
+ fullWidth
286
+ className="base-dialog"
287
+ actions={
288
+ <Stack direction="row" spacing={2}>
289
+ <Button variant="outlined" color="inherit" onClick={onClose}>
290
+ {t('common.cancel')}
291
+ </Button>
292
+ <Button
293
+ variant="contained"
294
+ color="primary"
295
+ disabled={loading || updating || !isValid}
296
+ onClick={handleSubmit(onSubmit)}>
297
+ {updating && <CircularProgress size={20} sx={{ mr: 1 }} />}
298
+ {t('common.save')}
299
+ </Button>
300
+ </Stack>
301
+ }>
302
+ {loading ? (
303
+ <Stack alignItems="center" justifyContent="center" height={200}>
304
+ <CircularProgress />
305
+ </Stack>
306
+ ) : (
307
+ <FormProvider {...methods}>
308
+ <Box>
309
+ <Alert severity="info">{t('notification.preferences.subscriptionRenewalNote')}</Alert>
310
+ <Typography id="frequency-label" sx={{ color: 'text.primary', mt: 2, mb: 1 }}>
311
+ {t('notification.preferences.frequency.label')}
312
+ </Typography>
313
+ <Stack flexDirection="row">
314
+ <Box
315
+ sx={{
316
+ display: 'flex',
317
+ alignItems: 'center',
318
+ flexWrap: 'nowrap',
319
+ gap: { xs: 0.5, sm: 1, md: 2 },
320
+ width: '100%',
321
+ overflow: 'auto',
322
+ pb: 1,
323
+ }}>
324
+ <FormControl sx={{ width: { xs: '100px', sm: '120px' }, minWidth: 'fit-content' }}>
325
+ <Select
326
+ value={frequency}
327
+ onChange={handleFrequencyChange}
328
+ size="small"
329
+ displayEmpty
330
+ sx={{
331
+ fontSize: { xs: '0.85rem', sm: '0.9rem' },
332
+ }}>
333
+ <MenuItem value="default">{t('notification.preferences.frequency.default')}</MenuItem>
334
+ <MenuItem value="daily">{t('notification.preferences.frequency.daily')}</MenuItem>
335
+ <MenuItem value="weekly">{t('notification.preferences.frequency.weekly')}</MenuItem>
336
+ <MenuItem value="monthly">{t('notification.preferences.frequency.monthly')}</MenuItem>
337
+ </Select>
338
+ </FormControl>
339
+
340
+ {frequency !== 'default' && (
341
+ <>
342
+ {frequency === 'weekly' && (
343
+ <Controller
344
+ name="notification.schedule.date"
345
+ control={control}
346
+ render={({ field }) => (
347
+ <Select size="small" sx={{ minWidth: 120 }} value={field.value} onChange={field.onChange}>
348
+ {[0, 1, 2, 3, 4, 5, 6].map((day) => (
349
+ <MenuItem key={day} value={day}>
350
+ {getDayName(day)}
351
+ </MenuItem>
352
+ ))}
353
+ </Select>
354
+ )}
355
+ />
356
+ )}
357
+
358
+ {frequency === 'monthly' && <DaySelector value={dateValue || 1} onChange={handleMonthDaySelect} />}
359
+
360
+ <TimeSelector
361
+ name="notification.schedule.time"
362
+ control={control}
363
+ required={needsSchedule}
364
+ timeFormatErrorMessage={t('notification.preferences.timeFormatError')}
365
+ />
366
+ </>
367
+ )}
368
+ </Box>
369
+ </Stack>
370
+ {selectedDate > 28 && frequency === 'monthly' && (
371
+ <Typography
372
+ variant="caption"
373
+ color="text.secondary"
374
+ sx={{
375
+ fontStyle: 'italic',
376
+ display: 'flex',
377
+ alignItems: 'center',
378
+ gap: 0.5,
379
+ }}>
380
+ <Box component="span" sx={{ fontSize: '0.8rem' }}>
381
+ ⚠️
382
+ </Box>
383
+ {t('notification.preferences.monthlyHelp')}
384
+ </Typography>
385
+ )}
386
+ </Box>
387
+ </FormProvider>
388
+ )}
389
+ </Dialog>
390
+ );
391
+ }
392
+
393
+ export default function NotificationPreference() {
394
+ const { t } = useLocaleContext();
395
+ const { isMobile } = useMobile();
396
+ const [open, setOpen] = React.useState(false);
397
+
398
+ return (
399
+ <>
400
+ {isMobile ? (
401
+ <Button
402
+ size="small"
403
+ variant="outlined"
404
+ sx={{
405
+ minWidth: 0,
406
+ width: 32,
407
+ height: 32,
408
+ borderRadius: '50%',
409
+ }}
410
+ onClick={() => setOpen(true)}>
411
+ <NotificationsOutlined fontSize="small" />
412
+ </Button>
413
+ ) : (
414
+ <Button
415
+ startIcon={<NotificationsOutlined />}
416
+ size="small"
417
+ onClick={() => setOpen(true)}
418
+ variant="outlined"
419
+ sx={{
420
+ whiteSpace: 'nowrap',
421
+ }}>
422
+ {t('notification.preferences.button')}
423
+ </Button>
424
+ )}
425
+ <NotificationPreferenceDialog open={open} onClose={() => setOpen(false)} />
426
+ </>
427
+ );
428
+ }
@@ -19,7 +19,7 @@ export default function UserLayout(props: any) {
19
19
 
20
20
  useEffect(() => {
21
21
  events.once('logout', () => {
22
- window.location.href = `${window.location.origin}/.well-known/service/user`;
22
+ session.login(() => {}, { openMode: 'redirect', redirect: window.location.href });
23
23
  });
24
24
  }, []);
25
25
 
@@ -37,6 +37,36 @@ export default flat({
37
37
  copySuccess: 'Copy Success',
38
38
  copyFailed: 'Copy Failed',
39
39
  copyTip: 'Please copy manually',
40
+ save: 'Save',
41
+ cancel: 'Cancel',
42
+ know: 'I Know',
43
+ confirm: 'Confirm',
44
+ days: {
45
+ sunday: 'Sunday',
46
+ monday: 'Monday',
47
+ tuesday: 'Tuesday',
48
+ wednesday: 'Wednesday',
49
+ thursday: 'Thursday',
50
+ friday: 'Friday',
51
+ saturday: 'Saturday',
52
+ },
53
+ },
54
+ notification: {
55
+ preferences: {
56
+ title: 'Email Notification Settings',
57
+ button: 'Email Settings',
58
+ frequency: {
59
+ label: 'Notification Frequency',
60
+ default: 'Instant Notifications (Default)',
61
+ daily: 'Daily',
62
+ weekly: 'Weekly',
63
+ monthly: 'Monthly',
64
+ },
65
+ day: 'Day',
66
+ timeFormatError: 'Please enter a valid time in 24-hour format (HH:MM)',
67
+ monthlyHelp: 'If day is not available in a month, the last day will be used',
68
+ subscriptionRenewalNote: 'This setting applies to subscription renewal notifications.',
69
+ },
40
70
  },
41
71
  admin: {
42
72
  balances: 'Balances',
@@ -36,6 +36,36 @@ export default flat({
36
36
  copySuccess: '复制成功',
37
37
  copyFailed: '复制失败',
38
38
  copyTip: '请手动复制',
39
+ save: '保存',
40
+ cancel: '取消',
41
+ know: '知道了',
42
+ confirm: '确认',
43
+ days: {
44
+ sunday: '星期日',
45
+ monday: '星期一',
46
+ tuesday: '星期二',
47
+ wednesday: '星期三',
48
+ thursday: '星期四',
49
+ friday: '星期五',
50
+ saturday: '星期六',
51
+ },
52
+ },
53
+ notification: {
54
+ preferences: {
55
+ title: '邮件通知设置',
56
+ button: '邮件设置',
57
+ frequency: {
58
+ label: '通知频率',
59
+ default: '即时通知 (默认)',
60
+ daily: '每日',
61
+ weekly: '每周',
62
+ monthly: '每月',
63
+ },
64
+ day: '日',
65
+ timeFormatError: '请输入有效的时间,格式为24小时制 (HH:MM)',
66
+ monthlyHelp: '如果所选日期在某月不存在,将使用该月的最后一天',
67
+ subscriptionRenewalNote: '此设置适用于订阅续费通知。',
68
+ },
39
69
  },
40
70
  admin: {
41
71
  balances: '余额',
@@ -40,6 +40,7 @@ import { joinURL } from 'ufo';
40
40
 
41
41
  import { useTransitionContext } from '../../components/progress-bar';
42
42
  import CurrentSubscriptions from '../../components/subscription/portal/list';
43
+ import NotificationPreference from '../../components/customer/notification-preference';
43
44
  import { useSessionContext } from '../../contexts/session';
44
45
  import api from '../../libs/api';
45
46
  import CustomerRevenueList from '../../components/payouts/portal/list';
@@ -200,7 +201,7 @@ const isCardVisible = (type: string, config: any, data: any, currency: any, meth
200
201
  data?.summary?.[summaryKey]?.[currency.id] && data?.summary?.[summaryKey]?.[currency.id] !== '0';
201
202
 
202
203
  if (type === 'balance') {
203
- return method?.type === 'arcblock' && (config.alwaysShow || hasSummaryValue);
204
+ return method?.type === 'arcblock' || config.alwaysShow || hasSummaryValue;
204
205
  }
205
206
 
206
207
  return config.alwaysShow || hasSummaryValue;
@@ -339,28 +340,31 @@ export default function CustomerHome() {
339
340
  <Box className="base-card section section-subscription">
340
341
  <Box className="section-header">
341
342
  <Typography variant="h3">{t('admin.subscription.name')}</Typography>
342
- {subscriptionStatus && (
343
- <FormControl
344
- sx={{
345
- '.MuiInputBase-root': {
346
- background: 'none',
347
- border: 'none',
348
- },
349
- '.MuiOutlinedInput-notchedOutline': {
350
- border: 'none',
351
- },
352
- }}>
353
- <Select
354
- value={state.onlyActive ? 'active' : ''}
355
- onChange={onToggleActive}
356
- displayEmpty
357
- IconComponent={ExpandMore}
358
- inputProps={{ 'aria-label': 'Without label' }}>
359
- <MenuItem value="">All</MenuItem>
360
- <MenuItem value="active">Active</MenuItem>
361
- </Select>
362
- </FormControl>
363
- )}
343
+ <Stack direction="row" spacing={1} alignItems="center">
344
+ <NotificationPreference />
345
+ {subscriptionStatus && (
346
+ <FormControl
347
+ sx={{
348
+ '.MuiInputBase-root': {
349
+ background: 'none',
350
+ border: 'none',
351
+ },
352
+ '.MuiOutlinedInput-notchedOutline': {
353
+ border: 'none',
354
+ },
355
+ }}>
356
+ <Select
357
+ value={state.onlyActive ? 'active' : ''}
358
+ onChange={onToggleActive}
359
+ displayEmpty
360
+ IconComponent={ExpandMore}
361
+ inputProps={{ 'aria-label': 'Without label' }}>
362
+ <MenuItem value="">All</MenuItem>
363
+ <MenuItem value="active">Active</MenuItem>
364
+ </Select>
365
+ </FormControl>
366
+ )}
367
+ </Stack>
364
368
  </Box>
365
369
  <Box className="section-body">
366
370
  {subscriptionLoading ? (
@@ -26,12 +26,13 @@ import {
26
26
  api,
27
27
  formatBNStr,
28
28
  formatPrice,
29
+ formatNumber,
29
30
  } from '@blocklet/payment-react';
30
31
  import type { TPaymentCurrency, TPaymentMethod } from '@blocklet/payment-types';
31
32
  import { joinURL } from 'ufo';
32
33
  import { AccountBalanceWalletOutlined, ArrowBackOutlined, ArrowForwardOutlined } from '@mui/icons-material';
33
34
  import Empty from '@arcblock/ux/lib/Empty';
34
- import { BN } from '@ocap/util';
35
+ import { BN, fromUnitToToken } from '@ocap/util';
35
36
  import RechargeList from '../../../components/invoice/recharge';
36
37
  import { getTokenBalanceLink, goBackOrFallback } from '../../../libs/util';
37
38
  import { useSessionContext } from '../../../contexts/session';
@@ -120,14 +121,19 @@ export default function BalanceRechargePage() {
120
121
  if (data.recommendedRecharge && data.recommendedRecharge.amount && data.recommendedRecharge.amount !== '0') {
121
122
  const baseAmount = data.recommendedRecharge.amount;
122
123
  const decimal = data.currency.decimal || 0;
124
+ const calcCycleAmount = (cycle: number) => {
125
+ const cycleAmount = fromUnitToToken(new BN(baseAmount).mul(new BN(String(cycle))).toString(), decimal);
126
+ return Math.ceil(parseFloat(cycleAmount)).toString();
127
+ };
123
128
  setUnitCycle({
124
- amount: parseFloat(formatBNStr(baseAmount, decimal, 6, true)).toString(),
129
+ amount: fromUnitToToken(baseAmount, decimal),
125
130
  interval: data.recommendedRecharge.interval as TimeUnit,
126
131
  cycle: data.recommendedRecharge.cycle,
127
132
  });
128
- setPresetAmounts([
133
+
134
+ const newPresetAmounts = [
129
135
  {
130
- amount: Math.ceil(parseFloat(formatBNStr(baseAmount, decimal, 6, true))).toString(),
136
+ amount: calcCycleAmount(1),
131
137
  multiplier: data.recommendedRecharge.cycle,
132
138
  label: t('common.estimatedDuration', {
133
139
  duration: formatSmartDuration(1, data.recommendedRecharge.interval as TimeUnit, {
@@ -136,9 +142,7 @@ export default function BalanceRechargePage() {
136
142
  }),
137
143
  },
138
144
  {
139
- amount: Math.ceil(
140
- parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
141
- ).toString(),
145
+ amount: calcCycleAmount(4),
142
146
  multiplier: data.recommendedRecharge.cycle * 4,
143
147
  label: t('common.estimatedDuration', {
144
148
  duration: formatSmartDuration(4, data.recommendedRecharge.interval as TimeUnit, {
@@ -147,9 +151,7 @@ export default function BalanceRechargePage() {
147
151
  }),
148
152
  },
149
153
  {
150
- amount: Math.ceil(
151
- parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('8')).toString(), decimal, 6, true))
152
- ).toString(),
154
+ amount: calcCycleAmount(8),
153
155
  multiplier: data.recommendedRecharge.cycle * 8,
154
156
  label: t('common.estimatedDuration', {
155
157
  duration: formatSmartDuration(8, data.recommendedRecharge.interval as TimeUnit, {
@@ -157,13 +159,13 @@ export default function BalanceRechargePage() {
157
159
  }),
158
160
  }),
159
161
  },
160
- ]);
162
+ ];
161
163
 
162
- setAmount(
163
- Math.ceil(
164
- parseFloat(formatBNStr(new BN(baseAmount).mul(new BN('4')).toString(), decimal, 6, true))
165
- ).toString()
166
- );
164
+ setPresetAmounts(newPresetAmounts);
165
+ const midAmount = calcCycleAmount(4);
166
+ if (!customAmount && !newPresetAmounts.find((item) => item.amount === midAmount)) {
167
+ setAmount(midAmount);
168
+ }
167
169
  } else {
168
170
  setPresetAmounts([
169
171
  { amount: '10', multiplier: 0, label: '' },
@@ -442,7 +444,7 @@ export default function BalanceRechargePage() {
442
444
  fontWeight: 600,
443
445
  color: amount === presetAmount && !customAmount ? 'primary.main' : 'text.primary',
444
446
  }}>
445
- {presetAmount} {currency.symbol}
447
+ {formatNumber(presetAmount)} {currency.symbol}
446
448
  </Typography>
447
449
  {multiplier > 0 && label && (
448
450
  <Typography variant="caption" align="center" color="text.secondary">