blob: ec840c452f97971943101206a5a8dd3113b320bd [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: (
idillonef9ab812022-11-18 13:46:24 -0500116 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400117 ),
simonab4eec82022-11-08 20:32:43 -0500118 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400119 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400120 />
121 </>
simond47ef9e2022-09-28 22:24:28 -0400122 );
123};
idillon-sfl37c18df2022-08-26 18:44:27 -0400124
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400125export const PasswordInput = ({
126 infoButtonProps,
127 onChange: _onChange,
128 success,
129 tooltipTitle,
simon4e7445c2022-11-16 21:18:46 -0500130 status = 'default',
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400131 ...props
simonab4eec82022-11-08 20:32:43 -0500132}: InputProps<PasswordStatus>) => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400133 const { t } = useTranslation();
simond47ef9e2022-09-28 22:24:28 -0400134 const [showPassword, setShowPassword] = useState(false);
135 const [isSelected, setIsSelected] = useState(false);
136 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400137 const [startAdornment, setStartAdornment] = useState<ReactElement | undefined>();
idillonef9ab812022-11-18 13:46:24 -0500138 const dialogHandler = useDialogHandler();
idillon-sfl37c18df2022-08-26 18:44:27 -0400139
simond47ef9e2022-09-28 22:24:28 -0400140 const toggleShowPassword = () => {
141 setShowPassword((showPassword) => !showPassword);
142 };
143
144 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400145 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400146 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400147 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400148 },
simon80b7b3b2022-09-28 17:50:10 -0400149 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400150 );
151
152 useEffect(() => {
153 /* Handle startAdornment */
154 let Icon = StyledLockIcon;
155 let visibility = 'visible';
156 if (props.error) {
157 Icon = StyledRoundSaltireIconError;
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400158 } else if (success) {
simond47ef9e2022-09-28 22:24:28 -0400159 Icon = StyledCheckedIconSuccess;
160 } else if (!isSelected && !input) {
161 visibility = 'hidden'; // keep icon's space so text does not move
162 }
163 setStartAdornment(<Icon sx={{ visibility }} />);
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400164 }, [props.error, success, isSelected, input]);
simond47ef9e2022-09-28 22:24:28 -0400165
simonab4eec82022-11-08 20:32:43 -0500166 /*
167 t('password_input_helper_text_too_weak')
168 t('password_input_helper_text_weak')
169 t('password_input_helper_text_medium')
170 t('password_input_helper_text_strong')
171 t('password_input_helper_text_registration_failed')
172 */
173 const helperText = t('password_input_helper_text', { context: `${status}` });
174
simond47ef9e2022-09-28 22:24:28 -0400175 return (
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400176 <>
idillonef9ab812022-11-18 13:46:24 -0500177 <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400178 <TextField
Michelle Sepkap Simee580f422022-10-31 23:27:04 -0400179 color={inputColor(props.error, success)}
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400180 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400181 type={showPassword ? 'text' : 'password'}
182 variant="standard"
183 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500184 helperText={status !== 'default' ? helperText : ''}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400185 onChange={onChange}
simonab4eec82022-11-08 20:32:43 -0500186 onFocus={() => setIsSelected(true)}
187 onBlur={() => setIsSelected(false)}
188 {...props}
189 InputLabelProps={{ shrink: !!(isSelected || input), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400190 InputProps={{
191 startAdornment,
192 endAdornment: (
193 <Stack direction="row" spacing="14px" alignItems="center">
idillonef9ab812022-11-18 13:46:24 -0500194 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400195 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
196 </Stack>
197 ),
simonab4eec82022-11-08 20:32:43 -0500198 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400199 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400200 />
201 </>
simond47ef9e2022-09-28 22:24:28 -0400202 );
203};
idillon-sfl37c18df2022-08-26 18:44:27 -0400204
simon35378692022-10-02 23:25:57 -0400205export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400206 const [isSelected, setIsSelected] = useState(false);
207 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400208 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400209
simond47ef9e2022-09-28 22:24:28 -0400210 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400211 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400212 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400213 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400214 },
simon80b7b3b2022-09-28 17:50:10 -0400215 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400216 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400217
simond47ef9e2022-09-28 22:24:28 -0400218 useEffect(() => {
219 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
220 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400221
simond47ef9e2022-09-28 22:24:28 -0400222 return (
223 <TextField
224 {...props}
225 label="Nickname, surname..."
226 variant="standard"
227 InputLabelProps={{ shrink: !!(isSelected || input) }}
228 onChange={onChange}
229 InputProps={{
230 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
231 }}
232 onFocus={() => setIsSelected(true)}
233 onBlur={() => setIsSelected(false)}
234 />
235 );
236};
idillon-sfl37c18df2022-08-26 18:44:27 -0400237
simon35378692022-10-02 23:25:57 -0400238export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400239 const [isSelected, setIsSelected] = useState(false);
240 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400241 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
242 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400243
simond47ef9e2022-09-28 22:24:28 -0400244 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400245 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400246 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400247 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400248 },
simon80b7b3b2022-09-28 17:50:10 -0400249 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400250 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400251
simond47ef9e2022-09-28 22:24:28 -0400252 useEffect(() => {
253 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
254 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
255 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400256
simond47ef9e2022-09-28 22:24:28 -0400257 return (
258 <TextField
259 {...props}
260 variant="standard"
261 InputLabelProps={{ shrink: !!(isSelected || input) }}
262 onChange={onChange}
263 InputProps={{
264 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
265 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
266 }}
267 onFocus={() => setIsSelected(true)}
268 onBlur={() => setIsSelected(false)}
269 />
270 );
271};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400272
Michelle Sepkap Sime51c00452022-10-31 21:26:38 -0400273function inputColor(
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400274 error?: boolean,
275 success?: boolean
276): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
277 return error ? 'error' : success ? 'success' : 'primary';
278}
279
280const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400281 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500282 const items = useMemo(
283 () => [
284 {
285 Icon: GppMaybe,
286 value: t('password_rule_one'),
287 },
288 {
289 Icon: GppMaybe,
290 value: t('password_rule_two'),
291 },
292 {
293 Icon: GppMaybe,
294 value: t('password_rule_three'),
295 },
296 {
297 Icon: GppMaybe,
298 value: t('password_rule_four'),
299 },
300 {
301 Icon: GppMaybe,
302 value: t('password_rule_five'),
303 },
304 ],
305 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400306 );
idillonef9ab812022-11-18 13:46:24 -0500307 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400308};
309
310const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400311 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500312 const items = useMemo(
313 () => [
314 {
315 Icon: Warning,
316 value: t('username_rule_one'),
317 },
318 {
319 Icon: Warning,
320 value: t('username_rule_two'),
321 },
322 {
323 Icon: Warning,
324 value: t('username_rule_three'),
325 },
326 {
327 Icon: Warning,
328 value: t('username_rule_four'),
329 },
330 ],
331 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400332 );
idillonef9ab812022-11-18 13:46:24 -0500333 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400334};