blob: d6529b49f91019baee2ba7f1608987926652fdc3 [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';
idillonef9ab812022-11-18 13:46:24 -050019import { IconButtonProps, Stack, TextField, TextFieldProps } from '@mui/material';
simond47ef9e2022-09-28 22:24:28 -040020import { styled } from '@mui/material/styles';
idillonef9ab812022-11-18 13:46:24 -050021import { ChangeEvent, ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040022import { useTranslation } from 'react-i18next';
simon07b4eb02022-09-29 17:50:26 -040023
simonab4eec82022-11-08 20:32:43 -050024import { StrengthValueCode } from '../utils/auth';
simon35378692022-10-02 23:25:57 -040025import { InfoButton, ToggleVisibilityButton } from './Button';
idillonef9ab812022-11-18 13:46:24 -050026import { DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
simon35378692022-10-02 23:25:57 -040027import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
idillon-sfl37c18df2022-08-26 18:44:27 -040028
simond47ef9e2022-09-28 22:24:28 -040029const iconsHeight = '16px';
30const StyledCheckedIconSuccess = styled(CheckedIcon)(({ theme }) => ({
31 height: iconsHeight,
32 color: theme.palette.success.main,
33}));
34const StyledRoundSaltireIconError = styled(RoundSaltireIcon)(({ theme }) => ({
35 height: iconsHeight,
36 color: theme.palette.error.main,
37}));
38const StyledPenIconLight = styled(PenIcon)({ height: iconsHeight, color: '#03B9E9' });
39const StyledPenIconDark = styled(PenIcon)(({ theme }) => ({ height: iconsHeight, color: theme.palette.primary.dark }));
40const StyledPersonIconLight = styled(PersonIcon)({ height: iconsHeight, color: '#03B9E9' });
41const StyledLockIcon = styled(LockIcon)({ height: iconsHeight, color: '#03B9E9' });
idillon-sfl37c18df2022-08-26 18:44:27 -040042
simonab4eec82022-11-08 20:32:43 -050043export type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
44export type PasswordStatus = StrengthValueCode | 'registration_failed';
45
46export type InputProps<StatusType extends NameStatus | PasswordStatus> = TextFieldProps & {
simon4e7445c2022-11-16 21:18:46 -050047 status?: StatusType;
simon35378692022-10-02 23:25:57 -040048 infoButtonProps?: IconButtonProps;
49 success?: boolean;
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,
55 onChange: _onChange,
56 success,
simon4e7445c2022-11-16 21:18:46 -050057 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040058 tooltipTitle,
59 ...props
simonab4eec82022-11-08 20:32:43 -050060}: InputProps<NameStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040061 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -040062 const [isSelected, setIsSelected] = useState(false);
63 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040064 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
idillonef9ab812022-11-18 13:46:24 -050065 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -040066
simond47ef9e2022-09-28 22:24:28 -040067 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040068 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040069 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040070 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040071 },
simon80b7b3b2022-09-28 17:50:10 -040072 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040073 );
idillon-sfl37c18df2022-08-26 18:44:27 -040074
simond47ef9e2022-09-28 22:24:28 -040075 useEffect(() => {
76 /* Handle startAdornment */
77 let Icon = StyledPersonIconLight;
78 let visibility = 'visible';
79 if (props.error) {
80 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040081 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -040082 Icon = StyledCheckedIconSuccess;
83 } else if (!isSelected && !input) {
84 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040085 }
simond47ef9e2022-09-28 22:24:28 -040086 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040087 }, [props.error, success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040088
simonab4eec82022-11-08 20:32:43 -050089 /*
90 t('username_input_helper_text_success')
91 t('username_input_helper_text_taken')
92 t('username_input_helper_text_invalid')
93 t('username_input_helper_text_registration_failed')
94 */
95 const helperText = t('username_input_helper_text', { context: `${status}` });
96
simond47ef9e2022-09-28 22:24:28 -040097 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040098 <>
idillonef9ab812022-11-18 13:46:24 -050099 <InfosDialog {...dialogHandler.props} title={t('username_rules_dialog_title')} content={<UsernameRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400100 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400101 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400102 label={t('username_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400103 variant="standard"
simonab4eec82022-11-08 20:32:43 -0500104 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400105 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500106 onFocus={() => setIsSelected(true)}
107 onBlur={() => setIsSelected(false)}
108 {...props}
109 InputLabelProps={{
110 shrink: !!(isSelected || input),
111 ...props.InputLabelProps,
112 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400113 InputProps={{
114 startAdornment,
115 endAdornment: (
simon841c6bd2022-11-24 00:53:40 -0500116 <InfoButton
117 tabIndex={-1}
118 tooltipTitle={tooltipTitle}
119 {...infoButtonProps}
120 onClick={dialogHandler.openDialog}
121 />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400122 ),
simonab4eec82022-11-08 20:32:43 -0500123 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400124 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400125 />
126 </>
simond47ef9e2022-09-28 22:24:28 -0400127 );
128};
idillon-sfl37c18df2022-08-26 18:44:27 -0400129
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400130export const PasswordInput = ({
131 infoButtonProps,
132 onChange: _onChange,
133 success,
134 tooltipTitle,
simon4e7445c2022-11-16 21:18:46 -0500135 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400136 ...props
simonab4eec82022-11-08 20:32:43 -0500137}: InputProps<PasswordStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400138 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400139 const [showPassword, setShowPassword] = useState(false);
140 const [isSelected, setIsSelected] = useState(false);
141 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400142 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
idillonef9ab812022-11-18 13:46:24 -0500143 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -0400144
simond47ef9e2022-09-28 22:24:28 -0400145 const toggleShowPassword = () => {
146 setShowPassword((showPassword) => !showPassword);
147 };
148
149 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400150 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400151 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400152 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400153 },
simon80b7b3b2022-09-28 17:50:10 -0400154 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400155 );
156
157 useEffect(() => {
158 /* Handle startAdornment */
159 let Icon = StyledLockIcon;
160 let visibility = 'visible';
161 if (props.error) {
162 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400163 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400164 Icon = StyledCheckedIconSuccess;
165 } else if (!isSelected && !input) {
166 visibility = 'hidden'; // keep icon's space so text does not move
167 }
168 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400169 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400170
simonab4eec82022-11-08 20:32:43 -0500171 /*
172 t('password_input_helper_text_too_weak')
173 t('password_input_helper_text_weak')
174 t('password_input_helper_text_medium')
175 t('password_input_helper_text_strong')
176 t('password_input_helper_text_registration_failed')
177 */
178 const helperText = t('password_input_helper_text', { context: `${status}` });
179
simond47ef9e2022-09-28 22:24:28 -0400180 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400181 <>
idillonef9ab812022-11-18 13:46:24 -0500182 <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400183 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400184 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400185 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400186 type={showPassword ? 'text' : 'password'}
187 variant="standard"
188 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500189 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400190 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500191 onFocus={() => setIsSelected(true)}
192 onBlur={() => setIsSelected(false)}
193 {...props}
194 InputLabelProps={{ shrink: !!(isSelected || input), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400195 InputProps={{
196 startAdornment,
197 endAdornment: (
198 <Stack direction="row" spacing="14px" alignItems="center">
idillonef9ab812022-11-18 13:46:24 -0500199 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400200 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
201 </Stack>
202 ),
simonab4eec82022-11-08 20:32:43 -0500203 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400204 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400205 />
206 </>
simond47ef9e2022-09-28 22:24:28 -0400207 );
208};
idillon-sfl37c18df2022-08-26 18:44:27 -0400209
simon35378692022-10-02 23:25:57 -0400210export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400211 const [isSelected, setIsSelected] = useState(false);
212 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400213 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400214
simond47ef9e2022-09-28 22:24:28 -0400215 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400216 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400217 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400218 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400219 },
simon80b7b3b2022-09-28 17:50:10 -0400220 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400221 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400222
simond47ef9e2022-09-28 22:24:28 -0400223 useEffect(() => {
224 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
225 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400226
simond47ef9e2022-09-28 22:24:28 -0400227 return (
228 <TextField
229 {...props}
230 label="Nickname, surname..."
231 variant="standard"
232 InputLabelProps={{ shrink: !!(isSelected || input) }}
233 onChange={onChange}
234 InputProps={{
235 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
236 }}
237 onFocus={() => setIsSelected(true)}
238 onBlur={() => setIsSelected(false)}
239 />
240 );
241};
idillon-sfl37c18df2022-08-26 18:44:27 -0400242
simon35378692022-10-02 23:25:57 -0400243export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400244 const [isSelected, setIsSelected] = useState(false);
245 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400246 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
247 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400248
simond47ef9e2022-09-28 22:24:28 -0400249 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400250 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400251 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400252 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400253 },
simon80b7b3b2022-09-28 17:50:10 -0400254 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400255 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400256
simond47ef9e2022-09-28 22:24:28 -0400257 useEffect(() => {
258 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
259 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
260 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400261
simond47ef9e2022-09-28 22:24:28 -0400262 return (
263 <TextField
264 {...props}
265 variant="standard"
266 InputLabelProps={{ shrink: !!(isSelected || input) }}
267 onChange={onChange}
268 InputProps={{
269 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
270 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
271 }}
272 onFocus={() => setIsSelected(true)}
273 onBlur={() => setIsSelected(false)}
274 />
275 );
276};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400277
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400278function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400279 error?: boolean,
280 success?: boolean
281): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
282 return error ? 'error' : success ? 'success' : 'primary';
283}
284
285const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400286 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500287 const items = useMemo(
288 () => [
289 {
290 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500291 value: t('password_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500292 },
293 {
294 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500295 value: t('password_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500296 },
297 {
298 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500299 value: t('password_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500300 },
301 {
302 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500303 value: t('password_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500304 },
305 {
306 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500307 value: t('password_rule_5'),
idillonef9ab812022-11-18 13:46:24 -0500308 },
309 ],
310 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400311 );
idillonef9ab812022-11-18 13:46:24 -0500312 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400313};
314
315const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400316 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500317 const items = useMemo(
318 () => [
319 {
320 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500321 value: t('username_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500322 },
323 {
324 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500325 value: t('username_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500326 },
327 {
328 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500329 value: t('username_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500330 },
331 {
332 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500333 value: t('username_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500334 },
335 ],
336 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400337 );
idillonef9ab812022-11-18 13:46:24 -0500338 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400339};