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.
- package/api/src/libs/event.ts +22 -2
- package/api/src/libs/invoice.ts +142 -0
- package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
- package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
- package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
- package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
- package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
- package/api/src/libs/queue/index.ts +6 -0
- package/api/src/libs/queue/store.ts +13 -1
- package/api/src/libs/util.ts +22 -1
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/invoice.ts +21 -7
- package/api/src/queues/notification.ts +353 -11
- package/api/src/queues/payment.ts +26 -10
- package/api/src/queues/payout.ts +21 -7
- package/api/src/routes/checkout-sessions.ts +26 -12
- package/api/src/routes/connect/recharge-account.ts +13 -1
- package/api/src/routes/connect/recharge.ts +13 -1
- package/api/src/routes/connect/shared.ts +54 -36
- package/api/src/routes/customers.ts +61 -0
- package/api/src/routes/invoices.ts +51 -1
- package/api/src/routes/subscriptions.ts +1 -1
- package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
- package/api/src/store/models/customer.ts +42 -1
- package/api/src/store/models/types.ts +17 -1
- package/blocklet.yml +1 -1
- package/package.json +24 -24
- package/src/components/customer/form.tsx +21 -2
- package/src/components/customer/notification-preference.tsx +428 -0
- package/src/components/layout/user.tsx +1 -1
- package/src/locales/en.tsx +30 -0
- package/src/locales/zh.tsx +30 -0
- package/src/pages/customer/index.tsx +27 -23
- package/src/pages/customer/recharge/account.tsx +19 -17
- 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
|
-
|
|
22
|
+
session.login(() => {}, { openMode: 'redirect', redirect: window.location.href });
|
|
23
23
|
});
|
|
24
24
|
}, []);
|
|
25
25
|
|
package/src/locales/en.tsx
CHANGED
|
@@ -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',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -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'
|
|
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
|
-
{
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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:
|
|
129
|
+
amount: fromUnitToToken(baseAmount, decimal),
|
|
125
130
|
interval: data.recommendedRecharge.interval as TimeUnit,
|
|
126
131
|
cycle: data.recommendedRecharge.cycle,
|
|
127
132
|
});
|
|
128
|
-
|
|
133
|
+
|
|
134
|
+
const newPresetAmounts = [
|
|
129
135
|
{
|
|
130
|
-
amount:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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">
|