blob: 40d55b6c467d3d597de25ad75bdd9532f6ec0fb0 [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 */
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040018import { GppMaybe, Warning } from '@mui/icons-material';
19import {
20 IconButtonProps,
21 List,
22 ListItem,
23 ListItemIcon,
24 Stack,
25 TextField,
26 TextFieldProps,
27 Typography,
28} from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040029import { styled } from '@mui/material/styles';
simon35378692022-10-02 23:25:57 -040030import { ChangeEvent, ReactElement, useCallback, useEffect, useState } from 'react';
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040031import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040032
simonab4eec82022-11-08 20:32:43 -050033import { StrengthValueCode } from '../utils/auth';
simon35378692022-10-02 23:25:57 -040034import { InfoButton, ToggleVisibilityButton } from './Button';
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040035import RulesDialog from './RulesDialog';
simon35378692022-10-02 23:25:57 -040036import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
idillon-sfl37c18df2022-08-26 18:44:27 -040037
simond47ef9e2022-09-28 22:24:28 -040038const iconsHeight = '16px';
39const StyledCheckedIconSuccess = styled(CheckedIcon)(({ theme }) => ({
40 height: iconsHeight,
41 color: theme.palette.success.main,
42}));
43const StyledRoundSaltireIconError = styled(RoundSaltireIcon)(({ theme }) => ({
44 height: iconsHeight,
45 color: theme.palette.error.main,
46}));
47const StyledPenIconLight = styled(PenIcon)({ height: iconsHeight, color: '#03B9E9' });
48const StyledPenIconDark = styled(PenIcon)(({ theme }) => ({ height: iconsHeight, color: theme.palette.primary.dark }));
49const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
50const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
idillon-sfl37c18df2022-08-26 18:44:27 -040051
simonab4eec82022-11-08 20:32:43 -050052export type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
53export type PasswordStatus = StrengthValueCode | 'registration_failed';
54
55export type InputProps<StatusType extends NameStatus | PasswordStatus> = TextFieldProps & {
56 status: StatusType;
simon35378692022-10-02 23:25:57 -040057 infoButtonProps?: IconButtonProps;
58 success?: boolean;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040059 tooltipTitle: string;
simon35378692022-10-02 23:25:57 -040060};
61
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040062export const UsernameInput = ({
63 infoButtonProps,
64 onChange: _onChange,
65 success,
simonab4eec82022-11-08 20:32:43 -050066 status,
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040067 tooltipTitle,
68 ...props
simonab4eec82022-11-08 20:32:43 -050069}: InputProps<NameStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040070 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -040071 const [isSelected, setIsSelected] = useState(false);
72 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040073 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040074 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -040075
simond47ef9e2022-09-28 22:24:28 -040076 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040077 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040078 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040079 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040080 },
simon80b7b3b2022-09-28 17:50:10 -040081 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040082 );
idillon-sfl37c18df2022-08-26 18:44:27 -040083
simond47ef9e2022-09-28 22:24:28 -040084 useEffect(() => {
85 /* Handle startAdornment */
86 let Icon = StyledPersonIconLight;
87 let visibility = 'visible';
88 if (props.error) {
89 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040090 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -040091 Icon = StyledCheckedIconSuccess;
92 } else if (!isSelected && !input) {
93 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040094 }
simond47ef9e2022-09-28 22:24:28 -040095 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040096 }, [props.error, success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040097
simonab4eec82022-11-08 20:32:43 -050098 /*
99 t('username_input_helper_text_success')
100 t('username_input_helper_text_taken')
101 t('username_input_helper_text_invalid')
102 t('username_input_helper_text_registration_failed')
103 */
104 const helperText = t('username_input_helper_text', { context: `${status}` });
105
simond47ef9e2022-09-28 22:24:28 -0400106 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400107 <>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400108 <RulesDialog
109 openDialog={isDialogOpened}
110 title={t('username_rules_dialog_title')}
111 closeDialog={() => setIsDialogOpened(false)}
112 >
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400113 <UsernameRules />
114 </RulesDialog>
115 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400116 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400117 label={t('username_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400118 variant="standard"
simonab4eec82022-11-08 20:32:43 -0500119 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400120 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500121 onFocus={() => setIsSelected(true)}
122 onBlur={() => setIsSelected(false)}
123 {...props}
124 InputLabelProps={{
125 shrink: !!(isSelected || input),
126 ...props.InputLabelProps,
127 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400128 InputProps={{
129 startAdornment,
130 endAdornment: (
131 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
132 ),
simonab4eec82022-11-08 20:32:43 -0500133 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400134 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400135 />
136 </>
simond47ef9e2022-09-28 22:24:28 -0400137 );
138};
idillon-sfl37c18df2022-08-26 18:44:27 -0400139
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400140export const PasswordInput = ({
141 infoButtonProps,
142 onChange: _onChange,
143 success,
144 tooltipTitle,
simonab4eec82022-11-08 20:32:43 -0500145 status,
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400146 ...props
simonab4eec82022-11-08 20:32:43 -0500147}: InputProps<PasswordStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400148 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400149 const [showPassword, setShowPassword] = useState(false);
150 const [isSelected, setIsSelected] = useState(false);
151 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400152 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400153 const [isDialogOpened, setIsDialogOpened] = useState<boolean>(false);
idillon-sfl37c18df2022-08-26 18:44:27 -0400154
simond47ef9e2022-09-28 22:24:28 -0400155 const toggleShowPassword = () => {
156 setShowPassword((showPassword) => !showPassword);
157 };
158
159 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400160 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400161 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400162 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400163 },
simon80b7b3b2022-09-28 17:50:10 -0400164 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400165 );
166
167 useEffect(() => {
168 /* Handle startAdornment */
169 let Icon = StyledLockIcon;
170 let visibility = 'visible';
171 if (props.error) {
172 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400173 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400174 Icon = StyledCheckedIconSuccess;
175 } else if (!isSelected && !input) {
176 visibility = 'hidden'; // keep icon's space so text does not move
177 }
178 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400179 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400180
simonab4eec82022-11-08 20:32:43 -0500181 /*
182 t('password_input_helper_text_too_weak')
183 t('password_input_helper_text_weak')
184 t('password_input_helper_text_medium')
185 t('password_input_helper_text_strong')
186 t('password_input_helper_text_registration_failed')
187 */
188 const helperText = t('password_input_helper_text', { context: `${status}` });
189
simond47ef9e2022-09-28 22:24:28 -0400190 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400191 <>
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400192 <RulesDialog
193 openDialog={isDialogOpened}
194 title={t('password_rules_dialog_title')}
195 closeDialog={() => setIsDialogOpened(false)}
196 >
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400197 <PasswordRules />
198 </RulesDialog>
199 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400200 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400201 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400202 type={showPassword ? 'text' : 'password'}
203 variant="standard"
204 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500205 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400206 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500207 onFocus={() => setIsSelected(true)}
208 onBlur={() => setIsSelected(false)}
209 {...props}
210 InputLabelProps={{ shrink: !!(isSelected || input), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400211 InputProps={{
212 startAdornment,
213 endAdornment: (
214 <Stack direction="row" spacing="14px" alignItems="center">
215 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={() => setIsDialogOpened(true)} />
216 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
217 </Stack>
218 ),
simonab4eec82022-11-08 20:32:43 -0500219 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400220 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400221 />
222 </>
simond47ef9e2022-09-28 22:24:28 -0400223 );
224};
idillon-sfl37c18df2022-08-26 18:44:27 -0400225
simon35378692022-10-02 23:25:57 -0400226export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400227 const [isSelected, setIsSelected] = useState(false);
228 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400229 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400230
simond47ef9e2022-09-28 22:24:28 -0400231 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400232 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400233 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400234 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400235 },
simon80b7b3b2022-09-28 17:50:10 -0400236 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400237 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400238
simond47ef9e2022-09-28 22:24:28 -0400239 useEffect(() => {
240 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
241 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400242
simond47ef9e2022-09-28 22:24:28 -0400243 return (
244 <TextField
245 {...props}
246 label="Nickname, surname..."
247 variant="standard"
248 InputLabelProps={{ shrink: !!(isSelected || input) }}
249 onChange={onChange}
250 InputProps={{
251 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
252 }}
253 onFocus={() => setIsSelected(true)}
254 onBlur={() => setIsSelected(false)}
255 />
256 );
257};
idillon-sfl37c18df2022-08-26 18:44:27 -0400258
simon35378692022-10-02 23:25:57 -0400259export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400260 const [isSelected, setIsSelected] = useState(false);
261 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400262 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
263 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400264
simond47ef9e2022-09-28 22:24:28 -0400265 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400266 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400267 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400268 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400269 },
simon80b7b3b2022-09-28 17:50:10 -0400270 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400271 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400272
simond47ef9e2022-09-28 22:24:28 -0400273 useEffect(() => {
274 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
275 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
276 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400277
simond47ef9e2022-09-28 22:24:28 -0400278 return (
279 <TextField
280 {...props}
281 variant="standard"
282 InputLabelProps={{ shrink: !!(isSelected || input) }}
283 onChange={onChange}
284 InputProps={{
285 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
286 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
287 }}
288 onFocus={() => setIsSelected(true)}
289 onBlur={() => setIsSelected(false)}
290 />
291 );
292};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400293
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400294function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400295 error?: boolean,
296 success?: boolean
297): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
298 return error ? 'error' : success ? 'success' : 'primary';
299}
300
301const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400302 const { t } = useTranslation();
303
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400304 return (
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400305 <List>
306 <ListItem>
307 <ListItemIcon>
308 <GppMaybe />
309 </ListItemIcon>
310 <Typography variant="body1">{t('password_rule_one')}</Typography>
311 </ListItem>
312 <ListItem>
313 <ListItemIcon>
314 <GppMaybe />
315 </ListItemIcon>
316 <Typography variant="body1">{t('password_rule_two')}</Typography>
317 </ListItem>
318 <ListItem>
319 <ListItemIcon>
320 <GppMaybe />
321 </ListItemIcon>
322 <Typography variant="body1">{t('password_rule_three')}</Typography>
323 </ListItem>
324 <ListItem>
325 <ListItemIcon>
326 <GppMaybe />
327 </ListItemIcon>
328 <Typography variant="body1">{t('password_rule_four')}</Typography>
329 </ListItem>
330 <ListItem>
331 <ListItemIcon>
332 <GppMaybe />
333 </ListItemIcon>
334 <Typography variant="body1">{t('password_rule_five')}</Typography>
335 </ListItem>
336 </List>
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400337 );
338};
339
340const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400341 const { t } = useTranslation();
342
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400343 return (
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400344 <List>
345 <ListItem>
346 <ListItemIcon>
347 <Warning />
348 </ListItemIcon>
349 <Typography variant="body1">{t('username_rule_one')}</Typography>
350 </ListItem>
351 <ListItem>
352 <ListItemIcon>
353 <Warning />
354 </ListItemIcon>
355 <Typography variant="body1">{t('username_rule_two')}</Typography>
356 </ListItem>
357 <ListItem>
358 <ListItemIcon>
359 <Warning />
360 </ListItemIcon>
361 <Typography variant="body1">{t('username_rule_three')}</Typography>
362 </ListItem>
363 <ListItem>
364 <ListItemIcon>
365 <Warning />
366 </ListItemIcon>
367 <Typography variant="body1">{t('username_rule_four')}</Typography>
368 </ListItem>
369 </List>
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400370 );
371};