Compare commits

..

1 Commits

Author SHA1 Message Date
sundasnoreen12
3355a014e6 feat: implemented restricted countries functionality 2025-02-24 13:44:39 +05:00
15 changed files with 1365 additions and 4485 deletions

View File

@@ -29,13 +29,7 @@ Getting Started
Installation
============
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
@@ -57,7 +51,7 @@ Devstack (Deprecated) instructions
Environment Variables/Setup Notes
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
The authentication micro-frontend also requires the following additional variable:
@@ -148,13 +142,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
How To Contribute
=================
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.

View File

@@ -3,7 +3,7 @@ Enable Social Auth Locally
Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.

5702
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,15 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -30,32 +33,33 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-platform": "^8.0.0",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/paragon": "^22.16.0",
"@openedx/paragon": "^22.1.1",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^16.2.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.41.0",
"core-js": "3.40.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.5",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"react-router": "6.29.0",
"react-router-dom": "6.29.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
@@ -69,12 +73,13 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "^14.4.2",
"babel-plugin-formatjs": "10.5.37",
"@openedx/frontend-build": "^14.0.3",
"babel-plugin-formatjs": "10.5.35",
"eslint-plugin-import": "2.31.0",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "9.1.7",
"jest": "29.7.0",
"react-test-renderer": "^18.3.1"
"react-test-renderer": "^17.0.2"
}
}

View File

@@ -13,9 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
export const getThirdPartyAuthContextSuccess = (
fieldDescriptions,
optionalFields,
thirdPartyAuthContext,
countries) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
payload: {
fieldDescriptions, optionalFields, thirdPartyAuthContext, countries,
},
});
export const getThirdPartyAuthContextFailure = () => ({

View File

@@ -35,6 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
countries: action.payload.countries,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:

View File

@@ -8,6 +8,7 @@ import {
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import {
getCountryList,
getThirdPartyAuthContext,
} from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
@@ -18,9 +19,10 @@ export function* fetchThirdPartyAuthContext(action) {
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const countries = (yield call(getCountryList)) || [];
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext, countries));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -1,5 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { FIELD_LABELS } from '../../data/constants';
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
@@ -23,3 +26,28 @@ export async function getThirdPartyAuthContext(urlParams) {
thirdPartyAuthContext: data.contextData || {},
};
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value, name }) => ({ code: value, name })) || [];
}
export async function getCountryList() {
try {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
requestConfig,
);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -8,6 +8,11 @@ import * as api from '../service';
const { loggingService } = initializeMockLogging();
jest.mock('../service', () => ({
getCountryList: jest.fn(),
getThirdPartyAuthContext: jest.fn(),
}));
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
@@ -31,6 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
countries: [],
}));
const dispatched = [];
@@ -44,7 +50,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
actions.getThirdPartyAuthContextSuccess({}, {}, data, []),
]);
getThirdPartyAuthContext.mockClear();
});

View File

@@ -37,3 +37,6 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';
export const FIELD_LABELS = {
COUNTRY: 'country',
};

View File

@@ -1,36 +1,27 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React, { StrictMode } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
} from '@edx/frontend-platform';
import { ErrorPage } from '@edx/frontend-platform/react';
import { createRoot } from 'react-dom/client';
import configuration from './config';
import messages from './i18n';
import MainApp from './MainApp';
subscribe(APP_READY, () => {
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<MainApp />
</StrictMode>,
ReactDOM.render(
<MainApp />,
document.getElementById('root'),
);
});
subscribe(APP_INIT_ERROR, (error) => {
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
});
initialize({

View File

@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData';

View File

@@ -85,6 +85,7 @@ const RegistrationPage = (props) => {
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
const countries = useSelector(state => state.commonComponents.countries);
const backendValidations = useSelector(getBackendValidations);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -358,6 +359,7 @@ const RegistrationPage = (props) => {
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
countries={countries}
/>
<StatefulButton
id="register-user"

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
@@ -31,13 +31,13 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
countries,
} = props;
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
@@ -70,6 +70,18 @@ const ConfigurableRegistrationForm = (props) => {
}
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
const removeDisabledCountries = useCallback((countryList) => {
if (!countries.length) {
return countryList;
}
const allowedCountries = new Set(countries.map(({ code }) => code));
return countryList.filter(({ code }) => allowedCountries.has(code));
}, [countries]);
const countryList = useMemo(() => removeDisabledCountries(
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []), [removeDisabledCountries]);
const handleErrorChange = (fieldName, error) => {
if (fieldName) {
setFieldErrors(prevErrors => ({
@@ -231,11 +243,16 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
countries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})),
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
countries: [],
};
export default ConfigurableRegistrationForm;

View File

@@ -185,6 +185,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
autoSubmitRegistrationForm: true,
countries: [{ code: 'AX', name: 'Åland Islands' }, { code: 'AL', name: 'Albania' }],
};
render(routerWrapper(reduxWrapper(