blob: ddc09af59acb7431d3cf7c89b32c5deec4649800 [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
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400159 label={t('password_input_label')}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400160 type={showPassword ? 'text' : 'password'}
161 variant="standard"
162 autoComplete="current-password"
simonab4eec82022-11-08 20:32:43 -0500163 helperText={status !== 'default' ? helperText : ''}
simonab4eec82022-11-08 20:32:43 -0500164 onFocus={() => setIsSelected(true)}
165 onBlur={() => setIsSelected(false)}
166 {...props}
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500167 InputLabelProps={{ shrink: !!(isSelected || props.value), ...props.InputLabelProps }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400168 InputProps={{
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500169 startAdornment: <StartAdornmentIcon sx={{ visibility: StartAdornmentIconVisibility }} />,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400170 endAdornment: (
171 <Stack direction="row" spacing="14px" alignItems="center">
idillonef9ab812022-11-18 13:46:24 -0500172 <InfoButton tooltipTitle={tooltipTitle} {...infoButtonProps} onClick={dialogHandler.openDialog} />
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400173 <ToggleVisibilityButton visible={showPassword} onClick={toggleShowPassword} />
174 </Stack>
175 ),
simonab4eec82022-11-08 20:32:43 -0500176 ...props.InputProps,
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400177 }}
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400178 />
179 </>
simond47ef9e2022-09-28 22:24:28 -0400180 );
181};
idillon-sfl37c18df2022-08-26 18:44:27 -0400182
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500183//this component is not used anywhere
simon35378692022-10-02 23:25:57 -0400184export const NickNameInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400185 const [isSelected, setIsSelected] = useState(false);
186 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400187 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
idillon-sfl37c18df2022-08-26 18:44:27 -0400188
simond47ef9e2022-09-28 22:24:28 -0400189 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400190 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400191 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400192 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400193 },
simon80b7b3b2022-09-28 17:50:10 -0400194 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400195 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400196
simond47ef9e2022-09-28 22:24:28 -0400197 useEffect(() => {
198 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
199 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400200
simond47ef9e2022-09-28 22:24:28 -0400201 return (
202 <TextField
203 {...props}
204 label="Nickname, surname..."
205 variant="standard"
206 InputLabelProps={{ shrink: !!(isSelected || input) }}
207 onChange={onChange}
208 InputProps={{
209 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
210 }}
211 onFocus={() => setIsSelected(true)}
212 onBlur={() => setIsSelected(false)}
213 />
214 );
215};
idillon-sfl37c18df2022-08-26 18:44:27 -0400216
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500217//this component is not used anywhere
simon35378692022-10-02 23:25:57 -0400218export const RegularInput = ({ onChange: _onChange, ...props }: TextFieldProps) => {
simond47ef9e2022-09-28 22:24:28 -0400219 const [isSelected, setIsSelected] = useState(false);
220 const [input, setInput] = useState(props.defaultValue);
simon35378692022-10-02 23:25:57 -0400221 const [startAdornmentVisibility, setStartAdornmentVisibility] = useState<'visible' | 'hidden'>('hidden');
222 const [endAdornmentVisibility, setEndAdornmentVisibility] = useState<'visible' | 'hidden'>('visible');
idillon-sfl37c18df2022-08-26 18:44:27 -0400223
simond47ef9e2022-09-28 22:24:28 -0400224 const onChange = useCallback(
simon35378692022-10-02 23:25:57 -0400225 (event: ChangeEvent<HTMLInputElement>) => {
simond47ef9e2022-09-28 22:24:28 -0400226 setInput(event.target.value);
simon80b7b3b2022-09-28 17:50:10 -0400227 _onChange?.(event);
simond47ef9e2022-09-28 22:24:28 -0400228 },
simon80b7b3b2022-09-28 17:50:10 -0400229 [_onChange]
simond47ef9e2022-09-28 22:24:28 -0400230 );
idillon-sfl37c18df2022-08-26 18:44:27 -0400231
simond47ef9e2022-09-28 22:24:28 -0400232 useEffect(() => {
233 setStartAdornmentVisibility(isSelected || input ? 'visible' : 'hidden');
234 setEndAdornmentVisibility(isSelected || input ? 'hidden' : 'visible');
235 }, [isSelected, input]);
idillon-sfl37c18df2022-08-26 18:44:27 -0400236
simond47ef9e2022-09-28 22:24:28 -0400237 return (
238 <TextField
239 {...props}
240 variant="standard"
241 InputLabelProps={{ shrink: !!(isSelected || input) }}
242 onChange={onChange}
243 InputProps={{
244 startAdornment: <StyledPenIconLight sx={{ visibility: startAdornmentVisibility }} />,
245 endAdornment: <StyledPenIconDark sx={{ visibility: endAdornmentVisibility }} />,
246 }}
247 onFocus={() => setIsSelected(true)}
248 onBlur={() => setIsSelected(false)}
249 />
250 );
251};
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400252
Ziwei Wang9b4e2c12023-02-06 14:45:37 -0500253//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
254// function inputColor(
255// error?: boolean,
256// success?: boolean
257// ): 'success' | 'error' | 'primary' | 'secondary' | 'info' | 'warning' | undefined {
258// return error ? 'error' : success ? 'success' : 'primary';
259// }
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400260
261const PasswordRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400262 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500263 const items = useMemo(
264 () => [
265 {
266 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500267 value: t('password_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500268 },
269 {
270 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500271 value: t('password_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500272 },
273 {
274 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500275 value: t('password_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500276 },
277 {
278 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500279 value: t('password_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500280 },
281 {
282 Icon: GppMaybe,
idillon6e8ca412022-12-16 11:22:42 -0500283 value: t('password_rule_5'),
idillonef9ab812022-11-18 13:46:24 -0500284 },
285 ],
286 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400287 );
idillonef9ab812022-11-18 13:46:24 -0500288 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400289};
290
291const UsernameRules = () => {
Michelle Sepkap Sime559cc802022-11-05 12:06:40 -0400292 const { t } = useTranslation();
idillonef9ab812022-11-18 13:46:24 -0500293 const items = useMemo(
294 () => [
295 {
296 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500297 value: t('username_rule_1'),
idillonef9ab812022-11-18 13:46:24 -0500298 },
299 {
300 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500301 value: t('username_rule_2'),
idillonef9ab812022-11-18 13:46:24 -0500302 },
303 {
304 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500305 value: t('username_rule_3'),
idillonef9ab812022-11-18 13:46:24 -0500306 },
307 {
308 Icon: Warning,
idillon6e8ca412022-12-16 11:22:42 -0500309 value: t('username_rule_4'),
idillonef9ab812022-11-18 13:46:24 -0500310 },
311 ],
312 [t]
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400313 );
idillonef9ab812022-11-18 13:46:24 -0500314 return <DialogContentList items={items} />;
Michelle Sepkap Simef5ebc2e2022-10-27 18:30:53 -0400315};