From fcbfffa25de623e374d5851f1bc361af1005a179 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Wed, 10 Jul 2024 16:00:37 +0500 Subject: [PATCH] feat: persistance of login and singup forms Description: Persistance of login and signup forms VAN-1957 --- src/forms/login-popup/data/reducers.js | 12 ++++++ src/forms/login-popup/index.jsx | 24 +++++++----- .../login-popup/tests/LoginPopup.test.jsx | 26 ++++++++++++- src/forms/registration-popup/data/reducers.js | 13 +++++++ src/forms/registration-popup/index.jsx | 27 +++++++++---- .../tests/RegistrationPopup.test.jsx | 26 ++++++++++++- .../forgot-password/data/reducers.js | 8 ++++ .../forgot-password/index.jsx | 16 ++++++-- .../forgot-password/tests/index.test.jsx | 39 ++++++++++++++++++- src/onboarding-component/tests/index.test.jsx | 23 +++++++++++ 10 files changed, 190 insertions(+), 24 deletions(-) diff --git a/src/forms/login-popup/data/reducers.js b/src/forms/login-popup/data/reducers.js index cd4a823c..0fb0bd91 100644 --- a/src/forms/login-popup/data/reducers.js +++ b/src/forms/login-popup/data/reducers.js @@ -21,6 +21,14 @@ export const loginInitialState = { isLoginSSOIntent: false, loginError: {}, loginResult: {}, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, showResetPasswordSuccessBanner: false, }; @@ -59,6 +67,9 @@ export const loginSlice = createSlice({ setLoginSSOIntent: (state) => { state.isLoginSSOIntent = true; }, + backupLoginFormBegin: (state, { payload }) => { + state.loginFormData = payload; + }, }, }); @@ -69,6 +80,7 @@ export const { loginUserFailed, setShowPasswordResetBanner, setLoginSSOIntent, + backupLoginFormBegin, } = loginSlice.actions; export default loginSlice.reducer; diff --git a/src/forms/login-popup/index.jsx b/src/forms/login-popup/index.jsx index 8c8d6060..1a9ebb67 100644 --- a/src/forms/login-popup/index.jsx +++ b/src/forms/login-popup/index.jsx @@ -12,7 +12,9 @@ import AccountActivationMessage from './components/AccountActivationMessage'; import LoginFailureAlert from './components/LoginFailureAlert'; import { NUDGE_PASSWORD_CHANGE, REQUIRE_PASSWORD_CHANGE } from './data/constants'; import useGetActivationMessage from './data/hooks'; -import { loginUser, setLoginSSOIntent } from './data/reducers'; +import { + backupLoginFormBegin, loginErrorClear, loginUser, setLoginSSOIntent, +} from './data/reducers'; import messages from './messages'; import { InlineLink, SocialAuthProviders } from '../../common-ui'; import { @@ -58,6 +60,7 @@ const LoginForm = () => { const isEditingFieldRef = useRef(false); const loginResult = useSelector(state => state.login.loginResult); + const backedUpFormData = useSelector(state => state.login.loginFormData); const loginErrorCode = useSelector(state => state.login.loginError?.errorCode); const loginErrorContext = useSelector(state => state.login.loginError?.errorContext); const providers = useSelector(state => state.commonData.thirdPartyAuthContext?.providers); @@ -70,15 +73,9 @@ const LoginForm = () => { const accountActivation = useGetActivationMessage(); - const [formFields, setFormFields] = useState({ - emailOrUsername: '', - password: '', - }); + const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields }); - const [formErrors, setFormErrors] = useState({ - emailOrUsername: '', - password: '', - }); + const [formErrors, setFormErrors] = useState({ ...backedUpFormData.errors }); const [errorCode, setErrorCode] = useState({ type: '', context: {} }); useEffect(() => { @@ -193,6 +190,13 @@ const LoginForm = () => { trackForgotPasswordLinkClick(); }; + const backupFormDataHandler = () => { + dispatch(backupLoginFormBegin({ + formFields: { ...formFields }, + errors: { ...formErrors }, + })); + }; + const handleSubmit = (e) => { e.preventDefault(); @@ -303,6 +307,8 @@ const LoginForm = () => { className="mb-2" onClick={() => { trackRegisterFormToggled(); + backupFormDataHandler(); + dispatch(loginErrorClear()); dispatch(setCurrentOpenedForm(REGISTRATION_FORM)); }} linkHelpText={formatMessage(messages.loginFormRegistrationHelpText)} diff --git a/src/forms/login-popup/tests/LoginPopup.test.jsx b/src/forms/login-popup/tests/LoginPopup.test.jsx index 6d9521f8..b1ee52ca 100644 --- a/src/forms/login-popup/tests/LoginPopup.test.jsx +++ b/src/forms/login-popup/tests/LoginPopup.test.jsx @@ -15,7 +15,7 @@ import { OnboardingComponentContext } from '../../../data/storeHooks'; import getAllPossibleQueryParams from '../../../data/utils'; import { setCurrentOpenedForm } from '../../../onboarding-component/data/reducers'; import { NUDGE_PASSWORD_CHANGE, REQUIRE_PASSWORD_CHANGE } from '../data/constants'; -import { loginUser } from '../data/reducers'; +import { backupLoginFormBegin, loginUser } from '../data/reducers'; import LoginForm from '../index'; const IntlLoginForm = injectIntl(LoginForm); @@ -33,6 +33,15 @@ jest.mock('../../../tracking/trackers/login', () => ({ describe('LoginForm Test', () => { let store = {}; + const loginFormData = { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }; + const reduxWrapper = children => ( @@ -45,6 +54,7 @@ describe('LoginForm Test', () => { login: { submitState: DEFAULT_STATE, loginResult: { success: false, redirectUrl: '' }, + loginFormData, loginError: {}, }, register: { @@ -246,6 +256,20 @@ describe('LoginForm Test', () => { expect(store.dispatch).toHaveBeenCalledWith(setCurrentOpenedForm(REGISTRATION_FORM)); }); + it('should backup the login form state when switch to sign-up form', () => { + store = mockStore({ + ...initialState, + login: { + ...initialState.login, + }, + }); + + store.dispatch = jest.fn(store.dispatch); + const { getByText } = render(reduxWrapper()); + fireEvent.click(getByText('Create account')); + expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin({ ...loginFormData })); + }); + it('should dispatch setCurrentOpenedForm action on "Forgot Password?" link click', () => { store.dispatch = jest.fn(store.dispatch); const { getByText } = render(reduxWrapper()); diff --git a/src/forms/registration-popup/data/reducers.js b/src/forms/registration-popup/data/reducers.js index b77c29ac..294a245a 100644 --- a/src/forms/registration-popup/data/reducers.js +++ b/src/forms/registration-popup/data/reducers.js @@ -16,6 +16,15 @@ export const registerInitialState = { validationState: DEFAULT_STATE, registrationError: {}, registrationResult: {}, + registrationFormData: { + isFormDirty: false, + formFields: { + name: '', email: '', password: '', marketingEmailsOptIn: true, + }, + errors: { + name: '', email: '', password: '', + }, + }, registrationFields: { marketingEmailsOptIn: true }, userPipelineDataLoaded: false, validationApiRateLimited: false, @@ -65,6 +74,9 @@ export const registerSlice = createSlice({ setRegistrationFields: (state, { payload }) => { state.registrationFields = payload; }, + backupRegistrationFormBegin: (state, { payload }) => { + state.registrationFormData = payload; + }, }, }); @@ -78,6 +90,7 @@ export const { fetchRealtimeValidationsFailed, clearAllRegistrationErrors, clearRegistrationBackendError, + backupRegistrationFormBegin, } = registerSlice.actions; export default registerSlice.reducer; diff --git a/src/forms/registration-popup/index.jsx b/src/forms/registration-popup/index.jsx index 9ba8d308..e33fe03b 100644 --- a/src/forms/registration-popup/index.jsx +++ b/src/forms/registration-popup/index.jsx @@ -12,6 +12,7 @@ import classNames from 'classnames'; import HonorCodeAndPrivacyPolicyMessage from './components/honorCodeAndTOS'; import RegistrationFailureAlert from './components/RegistrationFailureAlert'; import { + backupRegistrationFormBegin, clearAllRegistrationErrors, clearRegistrationBackendError, registerUser, @@ -63,13 +64,6 @@ const RegistrationForm = () => { const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth - 1 }); - const [formFields, setFormFields] = useState({ - name: '', email: '', password: '', marketingEmailsOptIn: true, - }); - const [errors, setErrors] = useState({}); - const [errorCode, setErrorCode] = useState({ type: '', count: 0 }); - const [userPipelineDataLoaded, setUserPipelineDataLoaded] = useState(false); - const emailRef = useRef(null); const registerErrorAlertRef = useRef(null); const socialAuthButtonRef = useRef(null); @@ -80,6 +74,8 @@ const RegistrationForm = () => { const registrationResult = useSelector(state => state.register.registrationResult); + const backedUpFormData = useSelector(state => state.register.registrationFormData); + const onboardingComponentContext = useSelector(state => state.commonData.onboardingComponentContext); const thirdPartyAuthApiStatus = useSelector(state => state.commonData.thirdPartyAuthApiStatus); const thirdPartyAuthErrorMessage = useSelector(state => state.commonData.thirdPartyAuthContext.errorMessage); @@ -94,6 +90,11 @@ const RegistrationForm = () => { const backendValidations = useSelector(getBackendValidations); const submitState = useSelector(state => state.register.submitState); + const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields }); + const [errors, setErrors] = useState({ ...backedUpFormData.errors }); + const [errorCode, setErrorCode] = useState({ type: '', count: 0 }); + const [userPipelineDataLoaded, setUserPipelineDataLoaded] = useState(false); + const autoSubmitRegForm = (currentProvider && thirdPartyAuthApiStatus === COMPLETE_STATE && !isLoginSSOIntent @@ -111,7 +112,7 @@ const RegistrationForm = () => { localStorage.removeItem('marketingEmailsOptIn'); localStorage.removeItem('ssoPipelineRedirectionDone'); } - if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) { + if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0 && !backedUpFormData.isFormDirty) { const { name = '', email = '', } = pipelineUserDetails; @@ -220,6 +221,15 @@ const RegistrationForm = () => { })); }; + const backupFormDataHandler = () => { + dispatch(backupRegistrationFormBegin({ + ...backedUpFormData, + isFormDirty: true, + formFields: { ...formFields }, + errors: { ...errors }, + })); + }; + const handleUserRegistration = () => { const totalRegistrationTime = (Date.now() - formStartTime) / 1000; const userCountryCode = getCountryCookieValue(); @@ -407,6 +417,7 @@ const RegistrationForm = () => { className="mb-2" onClick={() => { trackLoginFormToggled(); + backupFormDataHandler(); dispatch(clearAllRegistrationErrors()); dispatch(setCurrentOpenedForm(LOGIN_FORM)); }} diff --git a/src/forms/registration-popup/tests/RegistrationPopup.test.jsx b/src/forms/registration-popup/tests/RegistrationPopup.test.jsx index 70f0d5b2..98361563 100644 --- a/src/forms/registration-popup/tests/RegistrationPopup.test.jsx +++ b/src/forms/registration-popup/tests/RegistrationPopup.test.jsx @@ -14,7 +14,7 @@ import { } from '../../../data/constants'; import { OnboardingComponentContext } from '../../../data/storeHooks'; import { setCurrentOpenedForm } from '../../../onboarding-component/data/reducers'; -import { clearRegistrationBackendError, registerUser } from '../data/reducers'; +import { backupRegistrationFormBegin, clearRegistrationBackendError, registerUser } from '../data/reducers'; import * as utils from '../data/utils'; import RegistrationForm from '../index'; @@ -44,6 +44,15 @@ const populateRequiredFields = ( describe('RegistrationForm Test', () => { let store = {}; + const registrationFormData = { + formFields: { + name: '', email: '', password: '', marketingEmailsOptIn: true, + }, + errors: { + name: '', email: '', password: '', + }, + }; + const reduxWrapper = children => ( @@ -57,6 +66,7 @@ describe('RegistrationForm Test', () => { submitState: DEFAULT_STATE, registrationError: {}, registrationResult: {}, + registrationFormData: { ...registrationFormData }, }, login: { isLoginSSOIntent: false, @@ -183,6 +193,20 @@ describe('RegistrationForm Test', () => { expect(store.dispatch).not.toHaveBeenCalledWith(registerUser({})); }); + it('should backup the registration form state when switch to login form', () => { + store = mockStore({ + ...initialState, + register: { + ...initialState.register, + }, + }); + + store.dispatch = jest.fn(store.dispatch); + const { getByText } = render(reduxWrapper()); + fireEvent.click(getByText('Sign In')); + expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData })); + }); + // ******** test registration form validations ******** it('should show error messages for required fields on empty form submission', () => { diff --git a/src/forms/reset-password-popup/forgot-password/data/reducers.js b/src/forms/reset-password-popup/forgot-password/data/reducers.js index 1c0b7567..9a802d59 100644 --- a/src/forms/reset-password-popup/forgot-password/data/reducers.js +++ b/src/forms/reset-password-popup/forgot-password/data/reducers.js @@ -19,6 +19,10 @@ export const FORGOT_PASSWORD_SLICE_NAME = 'forgotPassword'; export const forgotPasswordInitialState = { status: DEFAULT_STATE, + forgotPasswordFormData: { + email: '', + error: '', + }, }; export const forgotPasswordSlice = createSlice({ @@ -43,6 +47,9 @@ export const forgotPasswordSlice = createSlice({ forgotPassweordTokenInvalidFailure: (state, { payload }) => { state.status = payload; }, + setForgotPasswordFormData: (state, { payload }) => { + state.forgotPasswordFormData = payload; + }, }, }); @@ -53,6 +60,7 @@ export const { forgotPasswordFailed, forgotPasswordClearStatus, forgotPassweordTokenInvalidFailure, + setForgotPasswordFormData, } = forgotPasswordSlice.actions; export default forgotPasswordSlice.reducer; diff --git a/src/forms/reset-password-popup/forgot-password/index.jsx b/src/forms/reset-password-popup/forgot-password/index.jsx index dce44677..9bce7c8b 100644 --- a/src/forms/reset-password-popup/forgot-password/index.jsx +++ b/src/forms/reset-password-popup/forgot-password/index.jsx @@ -6,7 +6,7 @@ import { Button, Container, Form, StatefulButton, } from '@openedx/paragon'; -import { forgotPassword, forgotPasswordClearStatus } from './data/reducers'; +import { forgotPassword, forgotPasswordClearStatus, setForgotPasswordFormData } from './data/reducers'; import getValidationMessage from './data/utils'; import ForgotPasswordFailureAlert from './ForgotPasswordFailureAlert'; import ForgotPasswordSuccess from './ForgotPasswordSuccess'; @@ -25,11 +25,13 @@ import '../index.scss'; const ForgotPasswordForm = () => { const { formatMessage } = useIntl(); const dispatch = useDispatch(); + const status = useSelector(state => state.forgotPassword?.status); + const backedUpFormData = useSelector(state => state.forgotPassword?.forgotPasswordFormData); const loginErrorCode = useSelector(state => state.login.loginError?.errorCode); - const [formErrors, setFormErrors] = useState(''); - const [formFields, setFormFields] = useState({ email: '' }); + const [formErrors, setFormErrors] = useState(backedUpFormData.error); + const [formFields, setFormFields] = useState({ email: backedUpFormData.email }); const [isSuccess, setIsSuccess] = useState(false); const emailRef = useRef(null); @@ -49,9 +51,15 @@ const ForgotPasswordForm = () => { const handleErrorChange = (fieldName, error) => { setFormErrors(error); }; - + const backupFormDataHandler = () => { + dispatch(setForgotPasswordFormData({ + email: formFields.email, + error: formErrors, + })); + }; const backToLogin = (e) => { e.preventDefault(); + backupFormDataHandler(); dispatch(forgotPasswordClearStatus()); dispatch(loginErrorClear()); dispatch(setCurrentOpenedForm(LOGIN_FORM)); diff --git a/src/forms/reset-password-popup/forgot-password/tests/index.test.jsx b/src/forms/reset-password-popup/forgot-password/tests/index.test.jsx index 680c4f8e..4013e9cf 100644 --- a/src/forms/reset-password-popup/forgot-password/tests/index.test.jsx +++ b/src/forms/reset-password-popup/forgot-password/tests/index.test.jsx @@ -11,12 +11,17 @@ import { OnboardingComponentContext } from '../../../../data/storeHooks'; import { setCurrentOpenedForm } from '../../../../onboarding-component/data/reducers'; import { NUDGE_PASSWORD_CHANGE, REQUIRE_PASSWORD_CHANGE } from '../../../login-popup/data/constants'; import { loginErrorClear } from '../../../login-popup/data/reducers'; -import { forgotPassword, forgotPasswordClearStatus } from '../data/reducers'; +import { forgotPassword, forgotPasswordClearStatus, setForgotPasswordFormData } from '../data/reducers'; import ForgotPasswordPage from '../index'; const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage); const mockStore = configureStore(); +const forgotPasswordFormData = { + email: '', + error: '', +}; + const initialState = { login: { loginError: { @@ -25,6 +30,7 @@ const initialState = { }, forgotPassword: { status: DEFAULT_STATE, + forgotPasswordFormData, }, }; @@ -102,6 +108,25 @@ describe('ForgotPasswordPage', () => { ])); }); + it('Backup the forgotPassword form into redux when back to login', () => { + store = mockStore({ + ...initialState, + forgotPassword: { + ...initialState.forgotPassword, + }, + }); + render(reduxWrapper()); + + const backToLoginButton = screen.getByRole('button', { name: 'Back to login' }); + + fireEvent.click(backToLoginButton); + + const actions = store.getActions(); + expect(actions).toEqual(expect.arrayContaining([ + { type: setForgotPasswordFormData.type, payload: forgotPasswordFormData }, + ])); + }); + // TODO: skipping because failing due to useRef. will be fixed later it.skip('handles COMPLETE_STATE correctly in useEffect', () => { // Update initial state to COMPLETE_STATE @@ -188,10 +213,22 @@ describe('ForgotPasswordPage', () => { login: { loginError: { errorCode: REQUIRE_PASSWORD_CHANGE, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, }, forgotPassword: { status: DEFAULT_STATE, + forgotPasswordFormData: { + email: '', + error: '', + }, }, }); diff --git a/src/onboarding-component/tests/index.test.jsx b/src/onboarding-component/tests/index.test.jsx index 5e39b63f..b2541044 100644 --- a/src/onboarding-component/tests/index.test.jsx +++ b/src/onboarding-component/tests/index.test.jsx @@ -58,11 +58,34 @@ describe('OnBoardingComponent Test', () => { submitState: DEFAULT_STATE, loginResult: { success: false, redirectUrl: '' }, loginError: {}, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, register: { submitState: DEFAULT_STATE, registrationError: {}, registrationResult: { success: false, redirectUrl: '' }, + registrationFormData: { + formFields: { + name: '', email: '', password: '', marketingEmailsOptIn: true, + }, + errors: { + name: '', email: '', password: '', + }, + }, + }, + forgotPassword: { + status: DEFAULT_STATE, + forgotPasswordFormData: { + email: '', + error: '', + }, }, commonData: { thirdPartyAuthContext: {