refactor: migrate account-menu to react - implement functionality

- handle login and register
- advanced options in login section
- focus/blur events with React
- cleanup password fields when required (setting the value to `undefined` for resetting the input field doesn't work well in React, thus set to empty string)
This commit is contained in:
VardanHakobyan
2021-06-01 21:49:27 +04:00
parent 09f62b68ae
commit 6a54ea90f9

View File

@@ -12,8 +12,11 @@ import {
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_E2E_ENABLED, STRING_E2E_ENABLED,
STRING_ENC_NOT_ENABLED, STRING_ENC_NOT_ENABLED,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
STRING_LOCAL_ENC_ENABLED, STRING_LOCAL_ENC_ENABLED,
STRING_NON_MATCHING_PASSCODES, STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
StringUtils StringUtils
} from '@/strings'; } from '@/strings';
import { ContentType } from '@node_modules/@standardnotes/snjs'; import { ContentType } from '@node_modules/@standardnotes/snjs';
@@ -22,6 +25,7 @@ import { JSXInternal } from '@node_modules/preact/src/jsx';
import TargetedEvent = JSXInternal.TargetedEvent; import TargetedEvent = JSXInternal.TargetedEvent;
import { alertDialog } from '@Services/alertService'; import { alertDialog } from '@Services/alertService';
import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent;
import { RefObject } from 'react';
type Props = { type Props = {
appState: AppState; appState: AppState;
@@ -55,25 +59,33 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
return null; return null;
}; };
const passcodeInput = useRef<HTMLInputElement>(); // TODO: implement what is missing for `passcodeInput`, e.g. - autofocus const passcodeInputRef = useRef<HTMLInputElement>();
const emailInputRef = useRef<HTMLInputElement>();
const passwordInputRef = useRef<HTMLInputElement>();
// TODO: Vardan `showLogin` and `showRegister` were in `formData` in Angular code, check whether I need to write similarly // TODO: Vardan `showLogin` and `showRegister` were in `formData` in Angular code, check whether I need to write similarly
const [showLogin, setShowLogin] = useState(false); const [showLogin, setShowLogin] = useState(false);
const [showRegister, setShowRegister] = useState(false); const [showRegister, setShowRegister] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false);
const [password, setPassword] = useState<string | undefined>(undefined); const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState<string | undefined>(undefined); const [passwordConfirmation, setPasswordConfirmation] = useState<string | undefined>(undefined);
const [status, setStatus] = useState(''); const [status, setStatus] = useState<string | undefined>(undefined);
const [syncError, setSyncError] = useState<string | undefined>(undefined); const [syncError, setSyncError] = useState<string | undefined>(undefined);
const [isEphemeral, setIsEphemeral] = useState(false);
const [isStrictSignIn, setIsStrictSignIn] = useState(false);
const [passcode, setPasscode] = useState<string | undefined>(undefined); const [passcode, setPasscode] = useState<string | undefined>(undefined);
const [passcodeConfirmation, setPasscodeConfirmation] = useState<string | undefined>(undefined); const [passcodeConfirmation, setPasscodeConfirmation] = useState<string | undefined>(undefined);
const [encryptionStatusString, setEncryptionStatusString] = useState<string | undefined>(undefined); const [encryptionStatusString, setEncryptionStatusString] = useState<string | undefined>(undefined);
const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false);
const [shouldMergeLocal, setShouldMergeLocal] = useState(true);
const [server, setServer] = useState<string | undefined>(undefined); const [server, setServer] = useState<string | undefined>(undefined);
const [url, setUrl] = useState<string | undefined>(undefined);
const [showPasscodeForm, setShowPasscodeForm] = useState(false); const [showPasscodeForm, setShowPasscodeForm] = useState(false);
const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState<unknown>(null); const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState<unknown>(null);
const [isLoading, setIsLoading] = useState<unknown>(false); const [isLoading, setIsLoading] = useState<unknown>(false);
@@ -103,38 +115,152 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
console.log('display registration form!'); console.log('display registration form!');
}; };
*/ */
const handleFormSubmit = () => { const focusWithTimeout = (inputElementRef: RefObject<HTMLInputElement>) => {
// TODO: Vardan: there is `novalidate` in Angular code, need to understand how to apply it here // In case the ref element is not yet available at this moment,
console.log('form submit'); // we call `focus()` after timeout.
setTimeout(() => {
inputElementRef.current && inputElementRef.current.focus();
}, 0);
}; };
const handleHostInputChange = () => { const handleSignInClick = () => {
console.log('handle host input change'); setShowLogin(true);
focusWithTimeout(emailInputRef);
}; };
const handleKeyPressKeyDown = (event: KeyboardEvent) => { const handleRegisterClick = () => {
if (event.key === 'Enter') { setShowRegister(true);
handleFormSubmit(); focusWithTimeout(emailInputRef);
};
const blurAuthFields = () => {
emailInputRef.current.blur();
passwordInputRef.current.blur();
};
/*
// TODO: move to top
type FormData = {
email: string;
password: string;
passwordConfirmation: string;
showLogin: boolean;
showRegister: boolean;
showPasscodeForm: boolean;
isStrictSignin?: boolean;
isEphemeral: boolean;
shouldMergeLocal?: boolean;
url: string;
isAuthenticating: boolean;
status: string;
passcode: string;
passcodeConfirmation: string;
};*/
const login = async() => {
setStatus(STRING_GENERATING_LOGIN_KEYS);
setIsAuthenticating(true);
const response = await application.signIn(
email as string,
password as string,
isStrictSignIn,
isEphemeral,
shouldMergeLocal
);
const error = response.error;
if (!error) {
setIsAuthenticating(false);
setPassword('');
closeAccountMenu();
return;
}
setShowLogin(true);
setStatus(undefined);
setPassword('');
if (error.message) {
await application.alertService.alert(error.message);
}
setIsAuthenticating(false);
};
const register = async () => {
if (passcodeConfirmation !== password) {
application.alertService.alert(STRING_NON_MATCHING_PASSWORDS);
return;
}
setStatus(STRING_GENERATING_REGISTER_KEYS);
setIsAuthenticating(true);
const response = await application.register(
email as string,
password as string,
isEphemeral,
shouldMergeLocal
);
const error = response.error;
if (error) {
setStatus(undefined);
setIsAuthenticating(false);
application.alertService.alert(error.message);
} else {
setIsAuthenticating(false);
closeAccountMenu();
} }
}; };
const handleChangeStrictSignIn = () => { const handleAuthFormSubmit = (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => {
console.log('handleChangeStrictSignIn'); // TODO: If I don't need `submit` form at all, get rid of `onSubmit` and thus there will be no need to `preventDefault`
event.preventDefault();
if (!email || !password) {
return;
}
blurAuthFields();
if (showLogin) {
login();
} else {
register();
}
}; };
const handlePasswordChange = () => { const handleHostInputChange = (event: TargetedEvent<HTMLInputElement>) => {
console.log('handlePasswordChange'); const { value } = event.target as HTMLInputElement;
setServer(value);
application.setHost(value);
};
// const handleKeyPressKeyDown = (event: KeyboardEvent) => {
const handleKeyPressKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
// TODO: fix TS error for `event`
handleAuthFormSubmit(event);
}
};
const handlePasswordChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setPassword(value);
};
const handleEmailChange = (event: TargetedEvent<HTMLInputElement>) => {
const { value } = event.target as HTMLInputElement;
setEmail(value);
}; };
const handlePasswordConfirmationChange = () => { const handlePasswordConfirmationChange = () => {
console.log('handlePasswordConfirmationChange'); console.log('handlePasswordConfirmationChange');
}; };
const handleChangeEphemeral = () => {
console.log('change ephemeral');
// TODO: Vardan: perhaps need to set some "global" value here
};
const handleMergeLocalData = () => { const handleMergeLocalData = () => {
console.log('handleMergeLocalData'); console.log('handleMergeLocalData');
}; };
@@ -182,16 +308,21 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
const handleAddPassCode = () => { const handleAddPassCode = () => {
setShowPasscodeForm(true); setShowPasscodeForm(true);
// At this moment the passcode input is not available, therefore the ref
// is null. Therefore we call `focus()` after timeout.
focusWithTimeout(passcodeInputRef);
}; };
const submitPasscodeForm = async (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => { const submitPasscodeForm = async (event: TargetedEvent<HTMLFormElement> | TargetedMouseEvent<HTMLButtonElement>) => {
// TODO: If I don't need `submit` form at all, get rid of `onSubmit` and thus there will be no need to `preventDefault`
event.preventDefault(); event.preventDefault();
if (passcode !== passcodeConfirmation) { if (passcode !== passcodeConfirmation) {
await alertDialog({ await alertDialog({
text: STRING_NON_MATCHING_PASSCODES, text: STRING_NON_MATCHING_PASSCODES
}); });
passcodeInput.current.focus(); passcodeInputRef.current.focus();
return; return;
} }
@@ -203,7 +334,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
: await application.addPasscode(passcode as string); : await application.addPasscode(passcode as string);
if (!successful) { if (!successful) {
passcodeInput.current.focus(); passcodeInputRef.current.focus();
} }
} }
); );
@@ -243,12 +374,12 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
const hidePasswordForm = () => { const hidePasswordForm = () => {
setShowLogin(false); setShowLogin(false);
setShowRegister(false); setShowRegister(false);
setPassword(undefined); setPassword('');
setPasswordConfirmation(undefined); setPasswordConfirmation(undefined);
}; };
const signOut = () => { const signOut = () => {
console.log('signOut'); appState.accountMenuReact.setSigningOut(true);
}; };
// TODO: Vardan: the name `changePasscodePressed` comes from original code; it is very similar to my `handlePasscodeChange`. // TODO: Vardan: the name `changePasscodePressed` comes from original code; it is very similar to my `handlePasscodeChange`.
@@ -336,6 +467,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
useEffect(() => { useEffect(() => {
const host = application.getHost(); const host = application.getHost();
setServer(host); setServer(host);
setUrl(host); // TODO: Vardan: maybe `url` is not needed at all, recheck
}, [application]); }, [application]);
useEffect(() => { useEffect(() => {
@@ -367,13 +499,13 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
<div className='flex my-1'> <div className='flex my-1'>
<button <button
className='sn-button info flex-grow text-base py-3 mr-1.5' className='sn-button info flex-grow text-base py-3 mr-1.5'
onClick={() => setShowLogin(true)} onClick={handleSignInClick}
> >
Sign In Sign In
</button> </button>
<button <button
className='sn-button info flex-grow text-base py-3 ml-1.5' className='sn-button info flex-grow text-base py-3 ml-1.5'
onClick={() => setShowRegister(true)} onClick={handleRegisterClick}
> >
Register Register
</button> </button>
@@ -389,25 +521,29 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
<div className='sk-panel-section-title'> <div className='sk-panel-section-title'>
{showLogin ? 'Sign In' : 'Register'} {showLogin ? 'Sign In' : 'Register'}
</div> </div>
<form className='sk-panel-form' onSubmit={handleFormSubmit}> <form className='sk-panel-form' onSubmit={handleAuthFormSubmit} noValidate>
<div className='sk-panel-section'> <div className='sk-panel-section'>
{/* TODO: Vardan: there are `should-focus` and `sn-autofocus`, implement them */} {/* TODO: Vardan: there are `should-focus` and `sn-autofocus`, implement them */}
<input className='sk-input contrast' <input className='sk-input contrast'
name='email' name='email'
type='email' type='email'
value={email}
onChange={handleEmailChange}
placeholder='Email' placeholder='Email'
required required
spellCheck={false} spellCheck={false}
ref={emailInputRef}
/> />
<input className='sk-input contrast' <input className='sk-input contrast'
name='password' name='password'
type='password' type='password'
value={password}
onChange={handlePasswordChange}
placeholder='Password' placeholder='Password'
required required
onKeyPress={handleKeyPressKeyDown} onKeyPress={handleKeyPressKeyDown}
onKeyDown={handleKeyPressKeyDown} onKeyDown={handleKeyPressKeyDown}
value={password} ref={passwordInputRef}
onChange={handlePasswordChange}
/> />
{showRegister && {showRegister &&
<input className='sk-input contrast' <input className='sk-input contrast'
@@ -438,14 +574,18 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
name='server' name='server'
placeholder='Server URL' placeholder='Server URL'
onChange={handleHostInputChange} onChange={handleHostInputChange}
value={server} value={url}
required required
/> />
</div> </div>
{showLogin && ( {showLogin && (
<label className='sk-label padded-row sk-panel-row justify-left'> <label className='sk-label padded-row sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'> <div className='sk-horizontal-group tight'>
<input className='sk-input' type='checkbox' onChange={handleChangeStrictSignIn} /> <input
className='sk-input'
type='checkbox'
onChange={() => setIsStrictSignIn(prevState => !prevState)}
/>
<p className='sk-p'>Use strict sign in</p> <p className='sk-p'>Use strict sign in</p>
<span> <span>
<a className='info' <a className='info'
@@ -462,7 +602,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
</div> </div>
)} )}
{!isAuthenticating && ( {!isAuthenticating && (
<div className='sk-panel-section.form-submit'> <div className='sk-panel-section form-submit'>
<button className='sn-button info text-base py-3 text-center' type='submit' <button className='sn-button info text-base py-3 text-center' type='submit'
disabled={isAuthenticating}> disabled={isAuthenticating}>
{showLogin ? 'Sign In' : 'Register'} {showLogin ? 'Sign In' : 'Register'}
@@ -482,9 +622,8 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
{status && ( {status && (
<div className='sk-panel-section no-bottom-pad'> <div className='sk-panel-section no-bottom-pad'>
<div className='sk-horizontal-group'> <div className='sk-horizontal-group'>
<div className='sk-spinner small neutral'> <div className='sk-spinner small neutral' />
<div className='sk-label'>{status}</div> <div className='sk-label'>{status}</div>
</div>
</div> </div>
</div> </div>
)} )}
@@ -492,14 +631,22 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
<div className='sk-panel-section no-bottom-pad'> <div className='sk-panel-section no-bottom-pad'>
<label className='sk-panel-row justify-left'> <label className='sk-panel-row justify-left'>
<div className='sk-horizontal-group tight'> <div className='sk-horizontal-group tight'>
<input type='checkbox' onChange={handleChangeEphemeral} /> <input
type='checkbox'
checked={!isEphemeral}
onChange={() => setIsEphemeral(prevState => !prevState)}
/>
<p className='sk-p'>Stay signed in</p> <p className='sk-p'>Stay signed in</p>
</div> </div>
</label> </label>
{notesAndTagsCount > 0 && ( {notesAndTagsCount > 0 && (
<label className='sk-panel-row.justify-left'> <label className='sk-panel-row.justify-left'>
<div className='sk-horizontal-group tight'> <div className='sk-horizontal-group tight'>
<input type='checkbox' onChange={handleMergeLocalData} /> <input
type='checkbox'
onChange={handleMergeLocalData}
checked={shouldMergeLocal}
/>
<p className='sk-p'>Merge local data ({notesAndTagsCount}) notes and tags</p> <p className='sk-p'>Merge local data ({notesAndTagsCount}) notes and tags</p>
</div> </div>
</label> </label>
@@ -626,11 +773,10 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
<input <input
className='sk-input contrast' className='sk-input contrast'
type='password' type='password'
ref={passcodeInput} ref={passcodeInputRef}
value={passcode} value={passcode}
onChange={handlePasscodeChange} onChange={handlePasscodeChange}
placeholder='Passcode' placeholder='Passcode'
autoFocus
/> />
<input <input
className='sk-input contrast' className='sk-input contrast'
@@ -793,5 +939,5 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
export const AccountMenuReact = toDirective<Props>( export const AccountMenuReact = toDirective<Props>(
AccountMenu, AccountMenu,
{ closeAccountMenu: '&'} { closeAccountMenu: '&' }
); );