Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/data/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
} from '../common-components';
import { reducer as edlyReducer, storeName as edlyStoreName } from '../edly';
import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
Expand All @@ -29,6 +30,7 @@ const createRootReducer = () => combineReducers({
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[edlyStoreName]: edlyReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
Expand Down
2 changes: 2 additions & 0 deletions src/data/sagas.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { all } from 'redux-saga/effects';

import { saga as commonComponentsSaga } from '../common-components';
import { saga as edlySaga } from '../edly';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as loginSaga } from '../login';
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
Expand All @@ -12,6 +13,7 @@ export default function* rootSaga() {
loginSaga(),
registrationSaga(),
commonComponentsSaga(),
edlySaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
authnProgressiveProfilingSaga(),
Expand Down
50 changes: 50 additions & 0 deletions src/edly/EdlyAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { useSelector } from 'react-redux';

import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';

import {
CROSS_TENANT_LOGIN,
EMAIL_REGISTERED_DIFFERENT_ORG,
REGISTRATION_REQUIRED_FOR_CURRENT_TENANT,
} from './data/constants';
import messages from './messages';

const EdlyAlert = () => {
const { formatMessage } = useIntl();
const errorCode = useSelector(state => state.edly.errorCode);
const context = useSelector(state => state.edly.context);

if (!errorCode) {
return null;
}

let alertMessage;
switch (errorCode) {
case EMAIL_REGISTERED_DIFFERENT_ORG:
alertMessage = formatMessage(messages['edly.email.registered.different.org']);
break;
case CROSS_TENANT_LOGIN:
alertMessage = formatMessage(messages['edly.cross.tenant.login.enabled'], {
registeredSite: context.registered_site || 'a partner site',
});
break;
case REGISTRATION_REQUIRED_FOR_CURRENT_TENANT:
alertMessage = formatMessage(messages['edly.registration.required.for.tenant'], {
registeredSite: context.registered_site || 'a partner site',
});
break;
default:
return null;
}

return (
<Alert id="edly-alert" className="mb-5" variant="info" icon={Info}>
<p>{alertMessage}</p>
</Alert>
);
};

export default EdlyAlert;
124 changes: 124 additions & 0 deletions src/edly/EmailCheckPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Form, StatefulButton } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';

import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { VALID_EMAIL_REGEX } from '../data/constants';
import { checkEmailRequest } from './data/actions';
import messages from './messages';

const EmailCheckPage = ({ onEmailCheckComplete }) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();

const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
const hasCompletedRef = useRef(false);

const submitState = useSelector(state => state.edly.submitState);
const redirectTo = useSelector(state => state.edly.redirectTo);
const checkedEmail = useSelector(state => state.edly.email);
const errorCode = useSelector(state => state.edly.errorCode);
const errorMessage = useSelector(state => state.edly.errorMessage);

useEffect(() => {
if (redirectTo && checkedEmail && !hasCompletedRef.current) {
hasCompletedRef.current = true;
onEmailCheckComplete(redirectTo, checkedEmail, errorCode);
}
}, [redirectTo, checkedEmail, errorCode, onEmailCheckComplete]);

const validateEmail = (emailValue) => {
if (!emailValue) {
return formatMessage(messages['email.validation.message']);
}

const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
if (!emailRegex.test(emailValue)) {
return formatMessage(messages['email.format.validation.message']);
}

return '';
};

const handleSubmit = (e) => {
e.preventDefault();

const error = validateEmail(email);
if (error) {
setEmailError(error);
return;
}

dispatch(checkEmailRequest(email));
};

const handleOnChange = (e) => {
const { value } = e.target;
setEmail(value);
if (emailError) {
setEmailError('');
}
};

const handleOnFocus = () => {
if (emailError) {
setEmailError('');
}
};

return (
<BaseContainer>
<div id="main-content" className="main-content">
<div className="mw-xs mt-3">
<h3 className="mb-3">{formatMessage(messages['email.check.heading'])}</h3>
<p className="mb-4 text-gray-700">{formatMessage(messages['email.check.subheading'])}</p>

{errorMessage && (
<Alert variant="danger" icon={Info} className="mb-3">
{errorMessage}
</Alert>
)}

<Form id="email-check-form" name="email-check-form">
<FormGroup
name="email"
value={email}
type="email"
autoComplete="email"
handleChange={handleOnChange}
handleFocus={handleOnFocus}
errorMessage={emailError}
floatingLabel="Email"
/>
<StatefulButton
id="continue-btn"
name="continue-btn"
type="submit"
variant="brand"
className="login-button-width mt-3"
state={submitState}
labels={{
default: formatMessage(messages['email.check.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
</Form>
</div>
</div>
</BaseContainer>
);
};

EmailCheckPage.propTypes = {
onEmailCheckComplete: PropTypes.func.isRequired,
};

export default EmailCheckPage;
28 changes: 28 additions & 0 deletions src/edly/data/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
EMAIL_CHECK_COMPLETE,
EMAIL_CHECK_FAILURE,
EMAIL_CHECK_REQUEST,
EMAIL_CHECK_SUCCESS,
} from './constants';

export const checkEmailRequest = email => ({
type: EMAIL_CHECK_REQUEST,
payload: { email },
});

export const checkEmailSuccess = (redirectTo, email, errorCode = null, context = {}) => ({
type: EMAIL_CHECK_SUCCESS,
payload: {
redirectTo, email, errorCode, context,
},
});

export const checkEmailFailure = errorMessage => ({
type: EMAIL_CHECK_FAILURE,
payload: { errorMessage },
});

export const emailCheckComplete = (email, errorCode = null) => ({
type: EMAIL_CHECK_COMPLETE,
payload: { email, errorCode },
});
13 changes: 13 additions & 0 deletions src/edly/data/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Action types
export const EMAIL_CHECK_REQUEST = 'EMAIL_CHECK_REQUEST';
export const EMAIL_CHECK_SUCCESS = 'EMAIL_CHECK_SUCCESS';
export const EMAIL_CHECK_FAILURE = 'EMAIL_CHECK_FAILURE';
export const EMAIL_CHECK_COMPLETE = 'EMAIL_CHECK_COMPLETE';

// Store name
export const EDLY_STORE_NAME = 'edly';

// Error codes from backend
export const EMAIL_REGISTERED_DIFFERENT_ORG = 'email_registered_different_org';
export const CROSS_TENANT_LOGIN = 'cross_tenant_login';
export const REGISTRATION_REQUIRED_FOR_CURRENT_TENANT = 'registration_required_for_current_tenant';
4 changes: 4 additions & 0 deletions src/edly/data/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as reducer } from './reducers';
export { default as saga } from './sagas';
export { EDLY_STORE_NAME as storeName } from './constants';
export * from './actions';
61 changes: 61 additions & 0 deletions src/edly/data/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
EMAIL_CHECK_COMPLETE,
EMAIL_CHECK_FAILURE,
EMAIL_CHECK_REQUEST,
EMAIL_CHECK_SUCCESS,
} from './constants';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';

const initialState = {
email: '',
redirectTo: null,
message: null,
errorMessage: null,
submitState: DEFAULT_STATE,
showEmailCheck: true,
prefilledEmail: '',
errorCode: null,
context: {},
};
Comment on lines +9 to +19
Copy link

@Anas-hameed Anas-hameed Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a sub-level that identifies the specific component this reducer belongs to. The current edly namespace doesn’t indicate which component uses this code.

src/edly/data/reducers.js is not indicating the component hierarchy


const edlyReducer = (state = initialState, action = {}) => {
switch (action.type) {
case EMAIL_CHECK_REQUEST:
return {
...state,
email: action.payload.email,
submitState: PENDING_STATE,
errorMessage: null,
message: null,
};
case EMAIL_CHECK_SUCCESS:
return {
...state,
redirectTo: action.payload.redirectTo,
email: action.payload.email,
errorCode: action.payload.errorCode,
context: action.payload.context || {},
submitState: DEFAULT_STATE,
errorMessage: null,
};
case EMAIL_CHECK_FAILURE:
return {
...state,
errorMessage: action.payload.errorMessage,
submitState: DEFAULT_STATE,
redirectTo: null,
message: null,
};
case EMAIL_CHECK_COMPLETE:
return {
...state,
showEmailCheck: false,
prefilledEmail: action.payload.email,
errorCode: action.payload.errorCode,
};
default:
return state;
}
};

export default edlyReducer;
25 changes: 25 additions & 0 deletions src/edly/data/sagas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { call, put, takeEvery } from 'redux-saga/effects';

import { checkEmailFailure, checkEmailSuccess } from './actions';
import { EMAIL_CHECK_REQUEST } from './constants';
import { checkEmailExists } from './service';

export function* handleEmailCheck(action) {
try {
const { email } = action.payload;
const result = yield call(checkEmailExists, email);
yield put(checkEmailSuccess(result.redirectTo, email, result.errorCode, result.context));
} catch (error) {
const errorMessage = error?.response?.data?.error
|| 'An error occurred. Please try again.';
yield put(checkEmailFailure(errorMessage));
}
}

export function* watchEmailCheck() {
yield takeEvery(EMAIL_CHECK_REQUEST, handleEmailCheck);
}

export default function* edlySaga() {
yield watchEmailCheck();
}
25 changes: 25 additions & 0 deletions src/edly/data/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

export async function checkEmailExists(email) {
const requestConfig = {
headers: { 'Content-Type': 'application/json' },
isPublic: true,
};

const { data } = await getAuthenticatedHttpClient()
.post(
`${getConfig().LMS_BASE_URL}/api/v1/user/validate-email/`,
{ email },
requestConfig,
)
.catch((error) => {
throw error;
});

return {
redirectTo: data.redirect_to,
errorCode: data.error_code || null,
context: data.context || {},
};
}
3 changes: 3 additions & 0 deletions src/edly/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as EmailCheckPage } from './EmailCheckPage';
export { default as EdlyAlert } from './EdlyAlert';
export * from './data';
Loading
Loading