From 6a54ea90f9d2605253be600de1aff06ef80c8293 Mon Sep 17 00:00:00 2001 From: VardanHakobyan Date: Tue, 1 Jun 2021 21:49:27 +0400 Subject: [PATCH] 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) --- .../components/AccountMenuReact.tsx | 228 ++++++++++++++---- 1 file changed, 187 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/components/AccountMenuReact.tsx b/app/assets/javascripts/components/AccountMenuReact.tsx index c139cd972..ac8199a2f 100644 --- a/app/assets/javascripts/components/AccountMenuReact.tsx +++ b/app/assets/javascripts/components/AccountMenuReact.tsx @@ -12,8 +12,11 @@ import { STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, + STRING_GENERATING_LOGIN_KEYS, + STRING_GENERATING_REGISTER_KEYS, STRING_LOCAL_ENC_ENABLED, STRING_NON_MATCHING_PASSCODES, + STRING_NON_MATCHING_PASSWORDS, StringUtils } from '@/strings'; import { ContentType } from '@node_modules/@standardnotes/snjs'; @@ -22,6 +25,7 @@ import { JSXInternal } from '@node_modules/preact/src/jsx'; import TargetedEvent = JSXInternal.TargetedEvent; import { alertDialog } from '@Services/alertService'; import TargetedMouseEvent = JSXInternal.TargetedMouseEvent; +import { RefObject } from 'react'; type Props = { appState: AppState; @@ -55,25 +59,33 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props return null; }; - const passcodeInput = useRef(); // TODO: implement what is missing for `passcodeInput`, e.g. - autofocus + const passcodeInputRef = useRef(); + const emailInputRef = useRef(); + const passwordInputRef = useRef(); // TODO: Vardan `showLogin` and `showRegister` were in `formData` in Angular code, check whether I need to write similarly const [showLogin, setShowLogin] = useState(false); const [showRegister, setShowRegister] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false); - const [password, setPassword] = useState(undefined); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); const [passwordConfirmation, setPasswordConfirmation] = useState(undefined); - const [status, setStatus] = useState(''); + const [status, setStatus] = useState(undefined); const [syncError, setSyncError] = useState(undefined); + const [isEphemeral, setIsEphemeral] = useState(false); + const [isStrictSignIn, setIsStrictSignIn] = useState(false); + const [passcode, setPasscode] = useState(undefined); const [passcodeConfirmation, setPasscodeConfirmation] = useState(undefined); const [encryptionStatusString, setEncryptionStatusString] = useState(undefined); const [isEncryptionEnabled, setIsEncryptionEnabled] = useState(false); + const [shouldMergeLocal, setShouldMergeLocal] = useState(true); const [server, setServer] = useState(undefined); + const [url, setUrl] = useState(undefined); const [showPasscodeForm, setShowPasscodeForm] = useState(false); const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -103,38 +115,152 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props console.log('display registration form!'); }; */ - const handleFormSubmit = () => { - // TODO: Vardan: there is `novalidate` in Angular code, need to understand how to apply it here - console.log('form submit'); + const focusWithTimeout = (inputElementRef: RefObject) => { + // In case the ref element is not yet available at this moment, + // we call `focus()` after timeout. + setTimeout(() => { + inputElementRef.current && inputElementRef.current.focus(); + }, 0); }; - const handleHostInputChange = () => { - console.log('handle host input change'); + const handleSignInClick = () => { + setShowLogin(true); + focusWithTimeout(emailInputRef); }; - const handleKeyPressKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - handleFormSubmit(); + const handleRegisterClick = () => { + setShowRegister(true); + 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 = () => { - console.log('handleChangeStrictSignIn'); + const handleAuthFormSubmit = (event: TargetedEvent | TargetedMouseEvent) => { + // 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 = () => { - console.log('handlePasswordChange'); + const handleHostInputChange = (event: TargetedEvent) => { + 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) => { + const { value } = event.target as HTMLInputElement; + setPassword(value); + }; + + const handleEmailChange = (event: TargetedEvent) => { + const { value } = event.target as HTMLInputElement; + setEmail(value); }; const handlePasswordConfirmationChange = () => { console.log('handlePasswordConfirmationChange'); }; - const handleChangeEphemeral = () => { - console.log('change ephemeral'); - // TODO: Vardan: perhaps need to set some "global" value here - }; - const handleMergeLocalData = () => { console.log('handleMergeLocalData'); }; @@ -182,16 +308,21 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props const handleAddPassCode = () => { 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 | TargetedMouseEvent) => { + // 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 (passcode !== passcodeConfirmation) { await alertDialog({ - text: STRING_NON_MATCHING_PASSCODES, + text: STRING_NON_MATCHING_PASSCODES }); - passcodeInput.current.focus(); + passcodeInputRef.current.focus(); return; } @@ -203,7 +334,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props : await application.addPasscode(passcode as string); if (!successful) { - passcodeInput.current.focus(); + passcodeInputRef.current.focus(); } } ); @@ -243,12 +374,12 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props const hidePasswordForm = () => { setShowLogin(false); setShowRegister(false); - setPassword(undefined); + setPassword(''); setPasswordConfirmation(undefined); }; 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`. @@ -336,6 +467,7 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props useEffect(() => { const host = application.getHost(); setServer(host); + setUrl(host); // TODO: Vardan: maybe `url` is not needed at all, recheck }, [application]); useEffect(() => { @@ -367,13 +499,13 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
@@ -389,25 +521,29 @@ const AccountMenu = observer(({ application, appState, closeAccountMenu }: Props
{showLogin ? 'Sign In' : 'Register'}
-
+
{/* TODO: Vardan: there are `should-focus` and `sn-autofocus`, implement them */} {showRegister &&
{showLogin && (