blob: 12b468bd2cda0bda26f6db504f6450c7c2856fb5 [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';
idillonef9ab812022-11-18 13:46:24 -050022import { ChangeEvent, ReactElement, 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
simonab4eec82022-11-08 20:32:43 -050044export type NameStatus = 'default' | 'success' | 'taken' | 'invalid' | 'registration_failed';
45export 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;
50 success?: boolean;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040051 tooltipTitle: string;
simon35378692022-10-02 23:25:57 -040052};
53
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040054export const UsernameInput = ({
55 infoButtonProps,
56 onChange: _onChange,
57 success,
simon4e7445c2022-11-16 21:18:46 -050058 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040059 tooltipTitle,
60 ...props
simonab4eec82022-11-08 20:32:43 -050061}: InputProps<NameStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -040062 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -040063 const [isSelected, setIsSelected] = useState(false);
64 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -040065 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
idillonef9ab812022-11-18 13:46:24 -050066 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -040067
simond47ef9e2022-09-28 22:24:28 -040068 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -040069 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -040070 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -040071 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -040072 },
simon80b7b3b2022-09-28 17:50:10 -040073 [_onChange]
simond47ef9e2022-09-28 22:24:28 -040074 );
idillon-sfl37c18df2022-08-26 18:44:27 -040075
simond47ef9e2022-09-28 22:24:28 -040076 useEffect(() => {
77 /* Handle startAdornment */
78 let Icon = StyledPersonIconLight;
79 let visibility = 'visible';
80 if (props.error) {
81 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040082 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -040083 Icon = StyledCheckedIconSuccess;
84 } else if (!isSelected && !input) {
85 visibility = 'hidden'; // keep icon's space so text does not move
idillon-sfl37c18df2022-08-26 18:44:27 -040086 }
simond47ef9e2022-09-28 22:24:28 -040087 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -040088 }, [props.error, success, isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -040089
simonab4eec82022-11-08 20:32:43 -050090 /*
91 t('username_input_helper_text_success')
92 t('username_input_helper_text_taken')
93 t('username_input_helper_text_invalid')
94 t('username_input_helper_text_registration_failed')
95 */
96 const helperText = t('username_input_helper_text', { context: `${status}` });
97
simond47ef9e2022-09-28 22:24:28 -040098 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -040099 <>
idillonef9ab812022-11-18 13:46:24 -0500100 <InfosDialog {...dialogHandler.props} title={t('username_rules_dialog_title')} content={<UsernameRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400101 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400102 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400103 label={t('username_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400104 variant="standard"
simonab4eec82022-11-08 20:32:43 -0500105 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400106 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500107 onFocus={() => setIsSelected(true)}
108 onBlur={() => setIsSelected(false)}
109 {...props}
110 InputLabelProps={{
111 shrink: !!(isSelected || input),
112 ...props.InputLabelProps,
113 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400114 InputProps={{
115 startAdornment,
116 endAdornment: (
simon841c6bd2022-11-24 00:53:40 -0500117 <InfoButton
118 tabIndex={-1}
119 tooltipTitle={tooltipTitle}
120 {...infoButtonProps}
121 onClick={dialogHandler.openDialog}
122 />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400123 ),
simonab4eec82022-11-08 20:32:43 -0500124 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400125 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400126 />
127 </>
simond47ef9e2022-09-28 22:24:28 -0400128 );
129};
idillon-sfl37c18df2022-08-26 18:44:27 -0400130
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400131export const PasswordInput = ({
132 infoButtonProps,
133 onChange: _onChange,
134 success,
135 tooltipTitle,
simon4e7445c2022-11-16 21:18:46 -0500136 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400137 ...props
simonab4eec82022-11-08 20:32:43 -0500138}: InputProps<PasswordStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400139 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400140 const [showPassword, setShowPassword] = useState(false);
141 const [isSelected, setIsSelected] = useState(false);
142 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400143 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
idillonef9ab812022-11-18 13:46:24 -0500144 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -0400145
simond47ef9e2022-09-28 22:24:28 -0400146 const toggleShowPassword = () => {
147 setShowPassword((showPassword) => !showPassword);
148 };
149
150 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400151 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400152 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400153 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400154 },
simon80b7b3b2022-09-28 17:50:10 -0400155 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400156 );
157
158 useEffect(() => {
159 /* Handle startAdornment */
160 let Icon = StyledLockIcon;
161 let visibility = 'visible';
162 if (props.error) {
163 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400164 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400165 Icon = StyledCheckedIconSuccess;
166 } else if (!isSelected && !input) {
167 visibility = 'hidden'; // keep icon's space so text does not move
168 }
169 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400170 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400171
simonab4eec82022-11-08 20:32:43 -0500172 /*
173 t('password_input_helper_text_too_weak')
174 t('password_input_helper_text_weak')
175 t('password_input_helper_text_medium')
176 t('password_input_helper_text_strong')
177 t('password_input_helper_text_registration_failed')
178 */
179 const helperText = t('password_input_helper_text', { context: `${status}` });
180
simond47ef9e2022-09-28 22:24:28 -0400181 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400182 <>
idillonef9ab812022-11-18 13:46:24 -0500183 <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400184 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400185 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400186 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400187 type={showPassword ? 'text' : 'password'}
188 variant="standard"
189 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500190 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400191 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500192 onFocus={() => setIsSelected(true)}
193 onBlur={() => setIsSelected(false)}
194 {...props}
195 InputLabelProps={{ shrink: !!(isSelected || input), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400196 InputProps={{
197 startAdornment,
198 endAdornment: (
199 <Stack direction="row" spacing="14px" alignItems="center">
idillonef9ab812022-11-18 13:46:24 -0500200 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400201 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
202 </Stack>
203 ),
simonab4eec82022-11-08 20:32:43 -0500204 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400205 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400206 />
207 </>
simond47ef9e2022-09-28 22:24:28 -0400208 );
209};
idillon-sfl37c18df2022-08-26 18:44:27 -0400210
simon35378692022-10-02 23:25:57 -0400211export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400212 const [isSelected, setIsSelected] = useState(false);
213 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400214 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400215
simond47ef9e2022-09-28 22:24:28 -0400216 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400217 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400218 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400219 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400220 },
simon80b7b3b2022-09-28 17:50:10 -0400221 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400222 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400223
simond47ef9e2022-09-28 22:24:28 -0400224 useEffect(() => {
225 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
226 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400227
simond47ef9e2022-09-28 22:24:28 -0400228 return (
229 <TextField
230 {...props}
231 label="Nickname, surname..."
232 variant="standard"
233 InputLabelProps={{ shrink: !!(isSelected || input) }}
234 onChange={onChange}
235 InputProps={{
236 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
237 }}
238 onFocus={() => setIsSelected(true)}
239 onBlur={() => setIsSelected(false)}
240 />
241 );
242};
idillon-sfl37c18df2022-08-26 18:44:27 -0400243
simon35378692022-10-02 23:25:57 -0400244export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400245 const [isSelected, setIsSelected] = useState(false);
246 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400247 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
248 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400249
simond47ef9e2022-09-28 22:24:28 -0400250 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400251 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400252 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400253 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400254 },
simon80b7b3b2022-09-28 17:50:10 -0400255 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400256 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400257
simond47ef9e2022-09-28 22:24:28 -0400258 useEffect(() => {
259 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
260 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
261 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400262
simond47ef9e2022-09-28 22:24:28 -0400263 return (
264 <TextField
265 {...props}
266 variant="standard"
267 InputLabelProps={{ shrink: !!(isSelected || input) }}
268 onChange={onChange}
269 InputProps={{
270 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
271 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
272 }}
273 onFocus={() => setIsSelected(true)}
274 onBlur={() => setIsSelected(false)}
275 />
276 );
277};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400278
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400279function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400280 error?: boolean,
281 success?: boolean
282): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
283 return error ? 'error' : success ? 'success' : 'primary';
284}
285
286const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400287 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500288 const items = useMemo(
289 () => [
290 {
291 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500292 value: t('password_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500293 },
294 {
295 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500296 value: t('password_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500297 },
298 {
299 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500300 value: t('password_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500301 },
302 {
303 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500304 value: t('password_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500305 },
306 {
307 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500308 value: t('password_rule_5'),
idillonef9ab812022-11-18 13:46:24 -0500309 },
310 ],
311 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400312 );
idillonef9ab812022-11-18 13:46:24 -0500313 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400314};
315
316const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400317 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500318 const items = useMemo(
319 () => [
320 {
321 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500322 value: t('username_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500323 },
324 {
325 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500326 value: t('username_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500327 },
328 {
329 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500330 value: t('username_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500331 },
332 {
333 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500334 value: t('username_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500335 },
336 ],
337 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400338 );
idillonef9ab812022-11-18 13:46:24 -0500339 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400340};