blob: 20da5e79984da1f1e98229723e7a384b8fab4446 [file] [log] [blame]
simon26e79f72022-10-05 22:16:08 -04001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
Ziwei Wanga41e1662023-01-27 14:56:32 -050018import GppMaybe from '@mui/icons-material/GppMaybe';
19import Warning from '@mui/icons-material/Warning';
idillonef9ab812022-11-18 13:46:24 -050020import { IconButtonProps, Stack, TextField, TextFieldProps } from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040021import { styled } from '@mui/material/styles';
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050022import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040023import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040024
simonab4eec82022-11-08 20:32:43 -050025import { StrengthValueCode } from '../utils/auth';
simon35378692022-10-02 23:25:57 -040026import { InfoButton, ToggleVisibilityButton } from './Button';
idillonef9ab812022-11-18 13:46:24 -050027import { DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
simon35378692022-10-02 23:25:57 -040028import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
idillon-sfl37c18df2022-08-26 18:44:27 -040029
simond47ef9e2022-09-28 22:24:28 -040030const iconsHeight = '16px';
31const StyledCheckedIconSuccess = styled(CheckedIcon)(({ theme }) => ({
32 height: iconsHeight,
33 color: theme.palette.success.main,
34}));
35const StyledRoundSaltireIconError = styled(RoundSaltireIcon)(({ theme }) => ({
36 height: iconsHeight,
37 color: theme.palette.error.main,
38}));
39const StyledPenIconLight = styled(PenIcon)({ height: iconsHeight, color: '#03B9E9' });
40const StyledPenIconDark = styled(PenIcon)(({ theme }) => ({ height: iconsHeight, color: theme.palette.primary.dark }));
41const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
42const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
idillon-sfl37c18df2022-08-26 18:44:27 -040043
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050044export type NameStatus = 'default' | 'success' | 'taken' | 'valid' | 'invalid' | 'registration_failed';
simonab4eec82022-11-08 20:32:43 -050045export type PasswordStatus = StrengthValueCode | 'registration_failed';
46
47export type InputProps<StatusType extends NameStatus | PasswordStatus> = TextFieldProps & {
simon4e7445c2022-11-16 21:18:46 -050048 status?: StatusType;
simon35378692022-10-02 23:25:57 -040049 infoButtonProps?: IconButtonProps;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040050 tooltipTitle: string;
simon35378692022-10-02 23:25:57 -040051};
52
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040053export const UsernameInput = ({
54 infoButtonProps,
simon4e7445c2022-11-16 21:18:46 -050055 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040056 tooltipTitle,
57 ...props
simonab4eec82022-11-08 20:32:43 -050058}: InputProps<NameStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040059 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -040060 const [isSelected, setIsSelected] = useState(false);
idillonef9ab812022-11-18 13:46:24 -050061 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -040062
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050063 const usernameError = status === 'taken' || status === 'invalid' || status === 'registration_failed';
64 const usernameSuccess = status === 'success';
idillon-sfl37c18df2022-08-26 18:44:27 -040065
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050066 let StartAdornmentIcon = StyledPersonIconLight;
67 let visibility = 'visible';
68 if (usernameError) {
69 StartAdornmentIcon = StyledRoundSaltireIconError;
70 } else if (usernameSuccess) {
71 StartAdornmentIcon = StyledCheckedIconSuccess;
72 } else if (!isSelected && !props.value) {
73 visibility = 'hidden'; // keep icon's space so text does not move
74 }
idillon-sfl37c18df2022-08-26 18:44:27 -040075
simonab4eec82022-11-08 20:32:43 -050076 /*
77 t('username_input_helper_text_success')
78 t('username_input_helper_text_taken')
79 t('username_input_helper_text_invalid')
80 t('username_input_helper_text_registration_failed')
81 */
82 const helperText = t('username_input_helper_text', { context: `${status}` });
83
simond47ef9e2022-09-28 22:24:28 -040084 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040085 <>
idillonef9ab812022-11-18 13:46:24 -050086 <InfosDialog {...dialogHandler.props} title={t('username_rules_dialog_title')} content={<UsernameRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040087 <TextField
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050088 required
89 error={usernameError}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040090 label={t('username_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040091 variant="standard"
simonab4eec82022-11-08 20:32:43 -050092 helperText={status !== 'default' ? helperText : ''}
simonab4eec82022-11-08 20:32:43 -050093 onFocus={() => setIsSelected(true)}
94 onBlur={() => setIsSelected(false)}
95 {...props}
96 InputLabelProps={{
Ziwei Wang9b4e2c12023-02-06 14:45:37 -050097 shrink: !!(isSelected || props.value),
simonab4eec82022-11-08 20:32:43 -050098 ...props.InputLabelProps,
99 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400100 InputProps={{
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500101 startAdornment: <StartAdornmentIcon sx={{ visibility }} />,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400102 endAdornment: (
simon841c6bd2022-11-24 00:53:40 -0500103 <InfoButton
104 tabIndex={-1}
105 tooltipTitle={tooltipTitle}
106 {...infoButtonProps}
107 onClick={dialogHandler.openDialog}
108 />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400109 ),
simonab4eec82022-11-08 20:32:43 -0500110 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400111 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400112 />
113 </>
simond47ef9e2022-09-28 22:24:28 -0400114 );
115};
idillon-sfl37c18df2022-08-26 18:44:27 -0400116
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400117export const PasswordInput = ({
118 infoButtonProps,
simon4e7445c2022-11-16 21:18:46 -0500119 status = 'default',
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500120 tooltipTitle,
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400121 ...props
simonab4eec82022-11-08 20:32:43 -0500122}: InputProps<PasswordStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400123 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400124 const [showPassword, setShowPassword] = useState(false);
125 const [isSelected, setIsSelected] = useState(false);
idillonef9ab812022-11-18 13:46:24 -0500126 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -0400127
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500128 const passwordError = status !== 'strong' && status !== 'default';
129 const passwordSuccess = status === 'strong';
130
simond47ef9e2022-09-28 22:24:28 -0400131 const toggleShowPassword = () => {
132 setShowPassword((showPassword) => !showPassword);
133 };
134
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500135 let StartAdornmentIcon = StyledLockIcon;
136 let StartAdornmentIconVisibility = 'visible';
137 if (passwordError) {
138 StartAdornmentIcon = StyledRoundSaltireIconError;
139 } else if (passwordSuccess) {
140 StartAdornmentIcon = StyledCheckedIconSuccess;
141 } else if (!isSelected && !props.value) {
142 StartAdornmentIconVisibility = 'hidden'; // keep icon's space so text does not move
143 }
simond47ef9e2022-09-28 22:24:28 -0400144
simonab4eec82022-11-08 20:32:43 -0500145 /*
146 t('password_input_helper_text_too_weak')
147 t('password_input_helper_text_weak')
148 t('password_input_helper_text_medium')
149 t('password_input_helper_text_strong')
150 t('password_input_helper_text_registration_failed')
151 */
152 const helperText = t('password_input_helper_text', { context: `${status}` });
153
simond47ef9e2022-09-28 22:24:28 -0400154 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400155 <>
idillonef9ab812022-11-18 13:46:24 -0500156 <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400157 <TextField
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500158 required
Ziwei Wang49765ec2023-02-13 16:35:32 -0500159 error={passwordError}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400160 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400161 type={showPassword ? 'text' : 'password'}
162 variant="standard"
163 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500164 helperText={status !== 'default' ? helperText : ''}
simonab4eec82022-11-08 20:32:43 -0500165 onFocus={() => setIsSelected(true)}
166 onBlur={() => setIsSelected(false)}
167 {...props}
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500168 InputLabelProps={{ shrink: !!(isSelected || props.value), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400169 InputProps={{
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500170 startAdornment: <StartAdornmentIcon sx={{ visibility: StartAdornmentIconVisibility }} />,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400171 endAdornment: (
172 <Stack direction="row" spacing="14px" alignItems="center">
idillonef9ab812022-11-18 13:46:24 -0500173 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400174 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
175 </Stack>
176 ),
simonab4eec82022-11-08 20:32:43 -0500177 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400178 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400179 />
180 </>
simond47ef9e2022-09-28 22:24:28 -0400181 );
182};
idillon-sfl37c18df2022-08-26 18:44:27 -0400183
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500184//this component is not used anywhere
simon35378692022-10-02 23:25:57 -0400185export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400186 const [isSelected, setIsSelected] = useState(false);
187 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400188 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400189
simond47ef9e2022-09-28 22:24:28 -0400190 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400191 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400192 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400193 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400194 },
simon80b7b3b2022-09-28 17:50:10 -0400195 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400196 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400197
simond47ef9e2022-09-28 22:24:28 -0400198 useEffect(() => {
199 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
200 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400201
simond47ef9e2022-09-28 22:24:28 -0400202 return (
203 <TextField
204 {...props}
205 label="Nickname, surname..."
206 variant="standard"
207 InputLabelProps={{ shrink: !!(isSelected || input) }}
208 onChange={onChange}
209 InputProps={{
210 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
211 }}
212 onFocus={() => setIsSelected(true)}
213 onBlur={() => setIsSelected(false)}
214 />
215 );
216};
idillon-sfl37c18df2022-08-26 18:44:27 -0400217
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500218//this component is not used anywhere
simon35378692022-10-02 23:25:57 -0400219export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400220 const [isSelected, setIsSelected] = useState(false);
221 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400222 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
223 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400224
simond47ef9e2022-09-28 22:24:28 -0400225 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400226 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400227 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400228 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400229 },
simon80b7b3b2022-09-28 17:50:10 -0400230 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400231 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400232
simond47ef9e2022-09-28 22:24:28 -0400233 useEffect(() => {
234 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
235 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
236 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400237
simond47ef9e2022-09-28 22:24:28 -0400238 return (
239 <TextField
240 {...props}
241 variant="standard"
242 InputLabelProps={{ shrink: !!(isSelected || input) }}
243 onChange={onChange}
244 InputProps={{
245 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
246 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
247 }}
248 onFocus={() => setIsSelected(true)}
249 onBlur={() => setIsSelected(false)}
250 />
251 );
252};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400253
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500254//this was used for altering the color of input fields, but somehow it didn't work at all. Only the `error` prop of the input is taking effect
255// function inputColor(
256// error?: boolean,
257// success?: boolean
258// ): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
259// return error ? 'error' : success ? 'success' : 'primary';
260// }
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400261
262const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400263 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500264 const items = useMemo(
265 () => [
266 {
267 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500268 value: t('password_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500269 },
270 {
271 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500272 value: t('password_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500273 },
274 {
275 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500276 value: t('password_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500277 },
278 {
279 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500280 value: t('password_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500281 },
282 {
283 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500284 value: t('password_rule_5'),
idillonef9ab812022-11-18 13:46:24 -0500285 },
286 ],
287 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400288 );
idillonef9ab812022-11-18 13:46:24 -0500289 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400290};
291
292const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400293 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500294 const items = useMemo(
295 () => [
296 {
297 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500298 value: t('username_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500299 },
300 {
301 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500302 value: t('username_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500303 },
304 {
305 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500306 value: t('username_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500307 },
308 {
309 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500310 value: t('username_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500311 },
312 ],
313 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400314 );
idillonef9ab812022-11-18 13:46:24 -0500315 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400316};