payment-kit 1.18.25 → 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/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/notification.ts +353 -11
- package/api/src/routes/customers.ts +61 -0
- 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 +15 -1
- package/api/src/store/models/types.ts +17 -1
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- 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 +26 -22
- package/src/pages/customer/recharge/account.tsx +7 -7
- package/src/pages/customer/subscription/embed.tsx +1 -0
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import 'react-international-phone/style.css';
|
|
2
2
|
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
FormInput,
|
|
6
|
+
PhoneInput,
|
|
7
|
+
CountrySelect,
|
|
8
|
+
validatePhoneNumber,
|
|
9
|
+
getPhoneUtil,
|
|
10
|
+
validatePostalCode,
|
|
11
|
+
} from '@blocklet/payment-react';
|
|
5
12
|
import { FormLabel, Stack } from '@mui/material';
|
|
6
|
-
import { Controller, useFormContext } from 'react-hook-form';
|
|
13
|
+
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
7
14
|
import isEmail from 'validator/es/lib/isEmail';
|
|
15
|
+
import { useMount } from 'ahooks';
|
|
8
16
|
|
|
9
17
|
export default function CustomerForm() {
|
|
10
18
|
const { t } = useLocaleContext();
|
|
11
19
|
const { control } = useFormContext();
|
|
20
|
+
useMount(() => {
|
|
21
|
+
getPhoneUtil().catch((err) => {
|
|
22
|
+
console.error('Failed to preload phone validator:', err);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const country = useWatch({ control, name: 'address.country' });
|
|
12
27
|
|
|
13
28
|
return (
|
|
14
29
|
<Stack
|
|
@@ -148,6 +163,10 @@ export default function CustomerForm() {
|
|
|
148
163
|
value: 20,
|
|
149
164
|
message: t('common.maxLength', { len: 20 }),
|
|
150
165
|
},
|
|
166
|
+
validate: (x: string) => {
|
|
167
|
+
const isValid = validatePostalCode(x, country);
|
|
168
|
+
return isValid ? true : t('payment.checkout.invalid');
|
|
169
|
+
},
|
|
151
170
|
}}
|
|
152
171
|
/>
|
|
153
172
|
</Stack>
|
|
@@ -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';
|
|
@@ -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 ? (
|
|
@@ -131,7 +131,7 @@ export default function BalanceRechargePage() {
|
|
|
131
131
|
cycle: data.recommendedRecharge.cycle,
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
const newPresetAmounts = [
|
|
135
135
|
{
|
|
136
136
|
amount: calcCycleAmount(1),
|
|
137
137
|
multiplier: data.recommendedRecharge.cycle,
|
|
@@ -159,13 +159,13 @@ export default function BalanceRechargePage() {
|
|
|
159
159
|
}),
|
|
160
160
|
}),
|
|
161
161
|
},
|
|
162
|
-
]
|
|
162
|
+
];
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
setPresetAmounts(newPresetAmounts);
|
|
165
|
+
const midAmount = calcCycleAmount(4);
|
|
166
|
+
if (!customAmount && !newPresetAmounts.find((item) => item.amount === midAmount)) {
|
|
167
|
+
setAmount(midAmount);
|
|
168
|
+
}
|
|
169
169
|
} else {
|
|
170
170
|
setPresetAmounts([
|
|
171
171
|
{ amount: '10', multiplier: 0, label: '' },
|