Compare commits

..

56 Commits

Author SHA1 Message Date
Adolfo R. Brandes
c0cf4623a4 refactor: decouple PasswordField from RegisterContext via props
PasswordField is a shared component used across login, registration,
and reset-password flows, but it was reaching directly into
RegisterContext for validation state and callbacks.  Replace context
coupling with explicit props (validateField, clearRegistrationBackendError,
validationApiRateLimited) passed by RegistrationPage, and remove the
now-unused useRegisterContextOptional hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
12670240b3 style: add explanatory comments for late imports after jest.mock()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
444e825fde test: restore missing test coverage for items 6, 8, 9
Adds tests that were removed or simplified during the Redux-to-React-Query
migration, as identified in PR #1641 review.

Logistration: CSRF token on mount, institution login page events for both
login and register pages, institution login track+page event assertions.

LoginPage: reset password banner dismissal on form submit, SSO redirect
to finishAuthUrl, and redirectUrl precedence over finishAuthUrl.

ProgressiveProfiling: sendPageEvent('login_and_registration', 'welcome')
assertion on component mount.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
c2a3c70c9d fix: address review nits for test mock cleanup
Remove unused useThirdPartyAuthHook mock body, inline dead
mockSetShowError variable, and extract shared TPA context mock
constant in Logistration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
56d8a10694 fix: add missing deps to registrationResult useEffect
Add USER_RETENTION_COOKIE_NAME and SESSION_COOKIE_DOMAIN to the
dependency array instead of suppressing react-hooks/exhaustive-deps.
Both are config constants from useAppConfig() that never change at
runtime, so behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
86b4ea79de perf: add enabled flag to TPA query hook to skip unnecessary fetches
In ProgressiveProfiling, the TPA query fired even when
registrationEmbedded was false and the result was ignored. Added an
enabled option to useThirdPartyAuthHook and set it to
registrationEmbedded in ProgressiveProfiling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
8dcf55254e fix: call setThirdPartyAuthContextBegin only on mount in LoginPage
It was being called on every data sync effect run, causing a flash of
PENDING state even when TPA data was already available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
e52d3d89fd fix: include payload in TPA query key to prevent stale cache
The query key only included pageId, so if the payload (tpa_hint, query
params, etc.) changed while pageId stayed the same, React Query would
serve stale cached data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
bb3ab6cba4 perf: add staleTime to third-party auth context query
The TPA context is effectively static per session. Adding a 5-minute
staleTime prevents unnecessary background refetches when navigating
between login/register tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
fc40952da3 fix: correct LoginContext setter types and remove dead PASSWORD_RESET_ERROR code
LoginContext setFormFields/setErrors are useState setters that accept
updater functions, so their types should be Dispatch<SetStateAction<...>>
rather than plain function signatures.

PASSWORD_RESET_ERROR was checked in ResetPasswordPage but no code path
ever set status to that value, making the conditions dead code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
1733f6ec01 fix: remove duplicate RedirectLogistration in ProgressiveProfiling
When shouldRedirect was true and welcomePageContext.nextUrl was truthy,
both RedirectLogistration components rendered simultaneously, causing a
double redirect attempt. The first block was a subset of the second, so
removing it is sufficient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
5d8743fb29 fix: use functional state updater in validateInput to prevent stale closure
validateInput was directly mutating the formErrors state object and
then spreading it. When handleSubmit called validateInput twice in
succession, the second call operated on stale closure values because
React batches state updates. Also fixed handleOnFocus for the same
issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
70ffc552b5 fix: move navigate() calls from render to useEffect in ResetPasswordPage
Calling navigate() during the render phase triggers state updates in
the router, causing React warnings and potential infinite render loops
in strict mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
3dbd6a76df fix: pass correct arguments to setEmailSuggestionContext in handleSuggestionClick
The function expects two string arguments (suggestion, type) but was
being called with a single object, corrupting the email suggestion state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
cb3ad5c53a feat: migrate from Redux to React Query and React Context
Replace Redux + Redux-Saga with React Query (useMutation/useQuery) for
server state and React Context for UI/form state across all modules:
login, registration, forgot-password, reset-password, progressive-
profiling, and common-components.

Port of master commits 0d709d15 and 93bd0f24, adapted for
@openedx/frontend-base:
- getSiteConfig() instead of getConfig()
- useAppConfig() for per-app configuration
- @tanstack/react-query as peerDependency (shell provides QueryClient)
- CurrentAppProvider instead of AppProvider

Also fixes EnvironmentTypes circular dependency in site.config.test.tsx
by using string literal instead of enum import.

Co-Authored-By: Jesus Balderrama <jesus.balderrama.wgu@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Max Sokolski
65462e7d80 fix: adding value length check for full name field
Co-authored-by: Artur Filippovskii <118079918+filippovskii09@users.noreply.github.com>
Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
bydawen
7bfb5d16d0 fix: add missing space symbol to the 'additional.help.text' field
Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
kshitij.sobti
c31c397c61 feat: add Slot for login page
This change adds a Slot for the login page allowing it to be customised.

Since this touched the Login Page, LoginPage and Logistration have also
been refactored to move away from redux connect.

Adapted for frontend-base: uses Slot from @openedx/frontend-base instead
of PluginSlot from @openedx/frontend-plugin-framework, slot files live
under src/slots/, and the slot ID follows the frontend-base naming
convention (org.openedx.frontend.slot.authn.loginComponent.v1).

Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Ihor Romaniuk
4fc41b0fe7 fix: username suggestions alignment
Port of master commit ffb8a2d4.

Co-Authored-By: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Hassan Raza
df9454bbe6 chore: handle forbidden username exceptions on registration
Port of master commit 58ec90ac.

Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Diana Villalvazo
5e41382f24 refactor: replace injectIntl with useIntl
Port of master commits db3d007c, 43a584eb, 5bd6926f, and 76e400f0.

Co-Authored-By: Adolfo R. Brandes <adolfo@axim.org>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:36:34 -03:00
Adolfo R. Brandes
fe44896856 chore: bump frontend-base to 1.0.0-alpha.14 2026-03-13 16:36:34 -03:00
javier ontiveros
ed58e02eae chore: update route base path 2026-03-12 07:18:17 -03:00
Adolfo R. Brandes
2337a7105f 1.0.0-alpha.6 2026-03-04 10:58:29 -03:00
Adolfo R. Brandes
b9f2c5da43 feat!: compile to JS before publishing
Configure the package to compile TypeScript and copy SCSS files to dist/
before publishing, rather than publishing raw source files. This allows
us to use tsc-alias for @src imports.

Also use a more modern export map to decouple the internal file
structure from the package's API, and add a build step to CI.

BREAKING CHANGE: Consuming projects may need to update their imports or
SASS @use lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 16:02:42 -03:00
Javier Ontiveros
1a0a6e3179 feat: added code spliting on main module (#1627) 2026-02-12 16:15:08 -03:00
Adolfo R. Brandes
92208a0a7c 1.0.0-alpha.5 2026-02-09 20:21:28 -03:00
Adolfo R. Brandes
45535ee055 build: Add minimum node version warning
An issue was found when using Node version 24.9 with the latest
package-lock (see #123), one not reproducible with versions 24.12 and
above.  Add a warning that will be shown when running `npm ci`.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
e53c4997bb fix: missing styles
The shell's SCSS must be explicitly loaded by site.config.dev.tsx.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
f311539e12 fix: CI
App repos are no longer built.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
c1070930bf fix: bump frontend-base
Bump frontend-base so we stop failing linting due to the nullish
coalescing rule we can't follow.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
dad2887eed fix: linting errors 2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
4f79099eca build: Upgrade to Node 24 2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
49f42a8857 feat!: add design tokens support
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.
2026-01-15 09:49:57 -03:00
Adolfo R. Brandes
2d6b401312 1.0.0-alpha.4 2025-08-20 13:12:29 -03:00
Adolfo R. Brandes
c602136798 chore: bump frontend-base and regenerate package-lock 2025-08-20 13:12:08 -03:00
Adolfo R. Brandes
9a48fe323b fix: work around inotify handle starvation
Bump frontend-base to avoid the inotify handle resource starvation.
2025-07-24 11:59:18 -03:00
Adolfo R. Brandes
967c52bde9 1.0.0-alpha.3 2025-06-29 13:20:08 +02:00
Adolfo R. Brandes
3252b593fd fix: bump frontend-base
Bump frontend-base to alpha.4 to fix a bug with the route role
redirection mechanism.
2025-06-29 13:19:33 +02:00
Adolfo R. Brandes
c98b9a1408 1.0.0-alpha.2 2025-06-29 13:11:02 +02:00
Adolfo R. Brandes
b6c659ccb8 feat: redirect to route role on successful login
Now we have a way to discover app URLs dynamically; do so for the
(default) redirect on login.
2025-06-29 13:09:17 +02:00
Adolfo R. Brandes
7efa279a29 1.0.0-alpha.1 2025-06-28 22:16:19 +02:00
Adolfo R. Brandes
f83f24af89 chore: bump frontend-base 2025-06-28 22:16:07 +02:00
Adolfo R. Brandes
b8116485b0 v1.0.0-alpha.0 2025-06-28 19:31:31 +02:00
Adolfo R. Brandes
762ff75fc4 Merge pull request #1515 from arbrandes/frontend-base-fixes
feat: Prepare for publication to NPM
2025-06-28 14:28:54 -03:00
Adolfo R. Brandes
095b156b95 chore: bump frontend-base 2025-06-28 19:27:26 +02:00
Adolfo R. Brandes
033e0fd7c5 feat: handle authentication roles 2025-06-28 19:18:34 +02:00
Adolfo R. Brandes
1ecdb0b6af chore: gitignore npm pack artifacts 2025-06-26 23:05:50 -03:00
Adolfo R. Brandes
18951cc4d0 chore: clean up npmignore 2025-06-26 23:05:50 -03:00
Adolfo R. Brandes
4dd5ddcc8b refactor: use appId from constants 2025-06-26 23:05:50 -03:00
Adolfo R. Brandes
6acbf64a71 chore: prepare for publication
Update package.json for publication as a "buildless" library.

(Also upgrade openedx-atlas.)
2025-06-26 19:52:56 -03:00
Adolfo R. Brandes
aea12a6a37 fix: remove catch-all route
Apps should avoid having a catch-all route, instead relying on the shell
to provide it.
2025-06-26 19:46:12 -03:00
Adolfo R. Brandes
3d778807f1 fix: update index.html
The only purpose of the included index.html is to serve the dev
environment, so modify it to make that clear.
2025-06-26 19:28:02 -03:00
Adolfo R. Brandes
014a990c22 docs: remove reference to devstack 2025-06-26 19:27:40 -03:00
Adolfo R. Brandes
7c8051a440 test: fix a couple of tests 2025-06-26 19:27:23 -03:00
Adolfo R. Brandes
8f8531a242 refactor: migrate to frontend-base
BREAKING CHANGE: refactors the MFE for frontend-base.
2025-06-24 15:30:07 -03:00
198 changed files with 6857 additions and 16243 deletions

45
.env
View File

@@ -1,45 +0,0 @@
NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=''
SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
USER_RETENTION_COOKIE_NAME=null
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL=''
# ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN=''
ENABLE_AUTO_GENERATED_USERNAME=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
MARKETING_EMAILS_OPT_IN=''
SHOW_CONFIGURABLE_EDX_FIELDS=''
ENABLE_IMAGE_LAYOUT=''
# ***** Zendesk related keys *****
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# ***** Base Container Images *****
BANNER_IMAGE_LARGE=''
BANNER_IMAGE_MEDIUM=''
BANNER_IMAGE_SMALL=''
BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -41,5 +41,3 @@ APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
ZENDESK_KEY='' ZENDESK_KEY=''
ZENDESK_LOGO_URL='' ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -1,4 +0,0 @@
# Copy these to the .env.private to enable edX specific functionality on local system
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
MARKETING_EMAILS_OPT_IN='true'
SHOW_CONFIGURABLE_EDX_FIELDS='true'

View File

@@ -1,21 +0,0 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='http://localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

View File

@@ -1,6 +0,0 @@
coverage/*
dist/
docs
node_modules/
__mocks__/
__snapshots__/

View File

@@ -1,52 +0,0 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('eslint', {
rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint
// to fail for no apparent reason since upgrading
// @openedx/frontend-build from v3 to v5:
// - TypeError: Cannot read property 'range' of null
indent: [
'error',
2,
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
],
'template-curly-spacing': 'off',
'jsx-a11y/label-has-associated-control': ['error', {
labelComponents: [],
labelAttributes: [],
controlComponents: [],
assert: 'htmlFor',
depth: 25,
}],
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
['sibling', 'parent'],
'index',
],
pathGroups: [
{
pattern: '@(react|react-dom|react-redux)',
group: 'external',
position: 'before',
},
],
pathGroupsExcludedImportTypes: ['react'],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
'function-paren-newline': 'off',
},
});

16
.gitignore vendored
View File

@@ -1,21 +1,15 @@
.DS_Store
.eslintcache
.idea
node_modules node_modules
npm-debug.log npm-debug.log
coverage coverage
module.config.js module.config.js
.env.private
dist/ dist/
/*.tgz
### i18n ###
src/i18n/transifex_input.json src/i18n/transifex_input.json
temp/babel-plugin-react-intl
### pyenv ### ### Editors ###
.python-version .DS_Store
### Emacs ###
*~ *~
/temp /temp
/.vscode /.vscode
src/i18n/messages

View File

@@ -1,11 +1,6 @@
.eslintignore __mocks__
.eslintrc.json
.gitignore
docker-compose.yml
Dockerfile
Makefile
npm-debug.log
coverage
node_modules node_modules
public *.test.js
*.test.jsx
*.test.ts
*.test.tsx

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/2u-infinity

View File

@@ -13,6 +13,19 @@ precommit:
requirements: requirements:
npm ci npm ci
clean:
rm -rf dist
build: clean
tsc --project tsconfig.build.json
tsc-alias -p tsconfig.build.json
find src -type f -name '*.scss' -exec sh -c '\
for f in "$$@"; do \
d="dist/$${f#src/}"; \
mkdir -p "$$(dirname "$$d")"; \
cp "$$f" "$$d"; \
done' sh {} +
i18n.extract: i18n.extract:
# Pulling display strings from .jsx files into .json files... # Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp) rm -rf $(transifex_temp)

View File

@@ -26,49 +26,14 @@ This is a micro-frontend application responsible for the login, registration and
Getting Started Getting Started
*************** ***************
Prerequisites 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`_ 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 .. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development .. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Cloning and Startup
===================
1. Clone your new repo:
.. code-block:: bash
git clone https://github.com/edx/frontend-app-authn.git
2. Use the version of Node specified in the ``.nvmrc`` file.
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes a ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
3. Install npm dependencies:
.. code-block:: bash
cd frontend-app-authn && npm install
4. Update the application port to use for local development:
The default port is 1999. If this does not work for you, update the line
``PORT=1999`` to your port in all ``.env.*`` files
5. Start the devserver. The app will be running at ``localhost:1999``, or whatever port you change it too.
.. code-block:: bash
npm run dev
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
Environment Variables/Setup Notes Environment Variables/Setup Notes
================================= =================================
@@ -130,7 +95,7 @@ The authentication micro-frontend also requires the following additional variabl
* - ``MFE_CONFIG_API_URL`` * - ``MFE_CONFIG_API_URL``
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings. - Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy) - ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
* - ``APP_ID`` * - ``APP_ID``
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID. - Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
@@ -158,10 +123,6 @@ Furthermore, there are several edX-specific environment variables that enable in
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails. - Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
- ``true`` | ``''`` (empty strings are falsy) - ``true`` | ``''`` (empty strings are falsy)
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open 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://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
@@ -240,4 +201,4 @@ Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/L
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml :target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
:alt: Continuous Integration :alt: Continuous Integration
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg .. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release :target: https://github.com/semantic-release/semantic-release

5
app.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="@openedx/frontend-base" />
declare module 'site.config' {
export default SiteConfig;
}

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('babel');

22
eslint.config.js Normal file
View File

@@ -0,0 +1,22 @@
// @ts-check
const { createLintConfig } = require('@openedx/frontend-base/tools');
module.exports = createLintConfig(
{
files: [
'src/**/*',
'site.config.*',
],
},
{
ignores: [
'coverage/*',
'dist/*',
'docs/*',
'node_modules/*',
'**/__mocks__/*',
'**/__snapshots__/*',
],
},
);

View File

@@ -1,14 +1,15 @@
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('jest', { module.exports = createConfig('test', {
setupFiles: [ setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js', '<rootDir>/src/setupTest.js',
], ],
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'src/setupTest.js', 'src/setupTest.js',
'src/i18n', 'src/i18n',
'src/index.jsx',
'MainApp.jsx',
], ],
testEnvironment: 'jsdom', moduleNameMapper: {
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
},
}); });

16397
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,40 @@
{ {
"name": "@edx/frontend-app-authn", "name": "@openedx/frontend-app-authn",
"version": "0.1.0", "version": "1.0.0-alpha.6",
"description": "Frontend application template", "description": "Frontend authentication",
"engines": {
"node": "^24.12"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/openedx/frontend-app-authn.git" "url": "git+https://github.com/openedx/frontend-app-authn.git"
}, },
"exports": {
".": "./dist/index.js",
"./app.scss": "./dist/app.scss"
},
"files": [
"/dist"
],
"browserslist": [ "browserslist": [
"extends @edx/browserslist-config" "extends @edx/browserslist-config"
], ],
"sideEffects": [
"*.css",
"*.scss"
],
"scripts": { "scripts": {
"build": "fedx-scripts webpack", "build": "make build",
"i18n_extract": "fedx-scripts formatjs extract", "clean": "make clean",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "dev": "PORT=1999 PUBLIC_PATH=/authn openedx dev",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "i18n_extract": "openedx formatjs extract",
"snapshot": "fedx-scripts jest --updateSnapshot", "lint": "openedx lint .",
"start": "fedx-scripts webpack-dev-server --progress", "lint:fix": "openedx lint --fix .",
"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", "prepack": "npm run build",
"test": "fedx-scripts jest --coverage --passWithNoTests" "snapshot": "openedx test --updateSnapshot",
"test": "openedx test --coverage --passWithNoTests"
}, },
"author": "edX", "author": "Open edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme", "homepage": "https://github.com/openedx/frontend-app-authn#readme",
"publishConfig": { "publishConfig": {
@@ -29,48 +44,41 @@
"url": "https://github.com/openedx/frontend-app-authn/issues" "url": "https://github.com/openedx/frontend-app-authn/issues"
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/frontend-platform": "^8.3.1", "@edx/openedx-atlas": "^0.7.0",
"@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/fontawesome-svg-core": "6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@fortawesome/react-fontawesome": "0.2.6", "classnames": "^2.5.1",
"@openedx/frontend-plugin-framework": "^1.7.0", "fastest-levenshtein": "^1.0.16",
"@openedx/paragon": "^23.4.2", "form-urlencoded": "^6.1.5",
"@optimizely/react-sdk": "^2.9.1", "i18n-iso-countries": "^7.13.0",
"@tanstack/react-query": "^5.90.19", "prop-types": "^15.8.1",
"@testing-library/react": "^16.2.0", "query-string": "^7.1.3",
"algoliasearch": "^4.14.3", "react-helmet": "^6.1.0",
"algoliasearch-helper": "^3.26.0", "react-loading-skeleton": "^3.5.0",
"classnames": "2.5.1", "react-responsive": "^8.2.0",
"core-js": "3.43.0", "universal-cookie": "^8.0.1"
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.6",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-responsive": "8.2.0",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-zendesk": "^0.1.13",
"regenerator-runtime": "0.14.1",
"universal-cookie": "7.2.2"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.5.0",
"@edx/typescript-config": "^1.1.0", "@testing-library/react": "^16.3.0",
"@openedx/frontend-build": "^14.6.2", "@types/jest": "^29.5.14",
"@testing-library/jest-dom": "^6.9.1", "babel-plugin-formatjs": "10.5.38",
"babel-plugin-formatjs": "10.5.41", "eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.32.0", "jest": "^29.7.0",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.3.0",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0" "ts-jest": "^29.4.0",
"tsc-alias": "^1.8.16"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.14",
"@openedx/paragon": "^23",
"@tanstack/react-query": "^5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6",
"react-router-dom": "^6"
} }
} }

View File

@@ -1,24 +1,9 @@
<!doctype html> <!doctype html>
<html lang="en-us"> <html lang="en-us">
<head> <head>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title> <title>Authentication Development Site></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

19
site.config.dev.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
import { authnApp } from './src';
import './src/app.scss';
const siteConfig: SiteConfig = {
siteId: 'authn-dev',
siteName: 'Authn Dev',
baseUrl: 'http://apps.local.openedx.io:8080',
lmsBaseUrl: 'http://local.openedx.io:8000',
loginUrl: 'http://local.openedx.io:8000/login',
logoutUrl: 'http://local.openedx.io:8000/logout',
environment: EnvironmentTypes.DEVELOPMENT,
apps: [authnApp],
};
export default siteConfig;

52
site.config.test.tsx Normal file
View File

@@ -0,0 +1,52 @@
import type { SiteConfig } from '@openedx/frontend-base';
import { appId } from './src/constants';
const siteConfig: SiteConfig = {
siteId: 'test-site',
siteName: 'Test Site',
baseUrl: 'http://localhost:1996',
lmsBaseUrl: 'http://localhost:8000',
loginUrl: 'http://localhost:8000/login',
logoutUrl: 'http://localhost:8000/logout',
// Use 'test' instead of EnvironmentTypes.TEST to break a circular dependency
// when mocking `@openedx/frontend-base` itself.
environment: 'test' as SiteConfig['environment'],
apps: [{
appId,
config: {
ACTIVATION_EMAIL_SUPPORT_LINK: null,
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
BANNER_IMAGE_EXTRA_SMALL: '',
BANNER_IMAGE_LARGE: '',
BANNER_IMAGE_MEDIUM: '',
BANNER_IMAGE_SMALL: '',
DISABLE_ENTERPRISE_LOGIN: true,
ENABLE_AUTO_GENERATED_USERNAME: false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
ENABLE_IMAGE_LAYOUT: false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
INFO_EMAIL: '',
LOGIN_ISSUE_SUPPORT_LINK: null,
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
MARKETING_EMAILS_OPT_IN: '',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
PASSWORD_RESET_SUPPORT_LINK: null,
POST_REGISTRATION_REDIRECT_URL: '',
PRIVACY_POLICY: null,
SEARCH_CATALOG_URL: null,
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
SHOW_REGISTRATION_LINKS: false,
TOS_AND_HONOR_CODE: null,
TOS_LINK: null,
USER_RETENTION_COOKIE_NAME: '',
},
}],
};
export default siteConfig;

19
src/Main.tsx Executable file
View File

@@ -0,0 +1,19 @@
import { Outlet } from 'react-router-dom';
import { CurrentAppProvider } from '@openedx/frontend-base';
import { appId } from './constants';
import {
registerIcons,
} from './common-components';
import './sass/_style.scss';
registerIcons();
const Main = () => (
<CurrentAppProvider appId={appId}>
<Outlet />
</CurrentAppProvider>
);
export default Main;

View File

@@ -1,76 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
PAGE_NOT_FOUND,
PASSWORD_RESET_CONFIRM,
RECOMMENDATIONS,
REGISTER_EMBEDDED_PAGE,
REGISTER_PAGE,
RESET_PAGE,
} from './data/constants';
import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password';
import './index.scss';
registerIcons();
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
});
const MainApp = () => (
<QueryClientProvider client={queryClient}>
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route
path={REGISTER_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={REGISTER_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
</AppProvider>
</QueryClientProvider>
);
export default MainApp;

1
src/__mocks__/file.js Normal file
View File

@@ -0,0 +1 @@
module.exports = 'FileMock';

1
src/__mocks__/svg.js Normal file
View File

@@ -0,0 +1 @@
module.exports = 'SvgURL';

2
src/app.scss Executable file
View File

@@ -0,0 +1,2 @@
@use "@openedx/frontend-base/shell/app.scss";
@use "./sass/style";

43
src/app.ts Normal file
View File

@@ -0,0 +1,43 @@
import { App } from '@openedx/frontend-base';
import { appId } from './constants';
import routes from './routes';
import messages from './i18n';
const app: App = {
appId,
routes,
messages,
config: {
ACTIVATION_EMAIL_SUPPORT_LINK: null,
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
BANNER_IMAGE_EXTRA_SMALL: '',
BANNER_IMAGE_LARGE: '',
BANNER_IMAGE_MEDIUM: '',
BANNER_IMAGE_SMALL: '',
DISABLE_ENTERPRISE_LOGIN: true,
ENABLE_AUTO_GENERATED_USERNAME: false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
ENABLE_IMAGE_LAYOUT: false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
INFO_EMAIL: '',
LOGIN_ISSUE_SUPPORT_LINK: null,
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
MARKETING_EMAILS_OPT_IN: '',
MARKETING_SITE_BASE_URL: 'http://local.openedx.io',
PASSWORD_RESET_SUPPORT_LINK: null,
POST_REGISTRATION_REDIRECT_URL: '',
PRIVACY_POLICY: null,
SEARCH_CATALOG_URL: null,
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
SHOW_REGISTRATION_LINKS: true,
TOS_AND_HONOR_CODE: null,
TOS_LINK: null,
USER_RETENTION_COOKIE_NAME: '',
},
};
export default app;

View File

@@ -1,4 +1,4 @@
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@openedx/frontend-base';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index'; import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -11,20 +10,20 @@ const LargeLayout = () => {
return ( return (
<div className="w-50 d-flex"> <div className="w-50 d-flex">
<div className="col-md-9 bg-primary-400"> <div className="col-md-9 bg-primary-400">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="min-vh-100 d-flex align-items-center"> <div className="min-vh-100 d-flex align-items-center">
<div className={classNames({ 'large-yellow-line mr-n4.5': getConfig().SITE_NAME === 'edX' })} /> <div className={classNames({ 'large-yellow-line mr-n4.5': getSiteConfig().siteName === 'edX' })} />
<h1 <h1
className={classNames( className={classNames(
'display-2 text-white mw-xs', 'display-2 text-white mw-xs',
{ 'ml-6': getConfig().SITE_NAME !== 'edX' }, { 'ml-6': getSiteConfig().siteName !== 'edX' },
)} )}
> >
{formatMessage(messages['start.learning'])} {formatMessage(messages['start.learning'])}
<div className="text-accent-a"> <div className="text-accent-a">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} {formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
</div> </div>
</h1> </h1>
</div> </div>

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -13,22 +12,22 @@ const MediumLayout = () => {
<div className="w-100 medium-screen-top-stripe" /> <div className="w-100 medium-screen-top-stripe" />
<div className="w-100 p-0 mb-3 d-flex"> <div className="w-100 p-0 mb-3 d-flex">
<div className="col-md-10 bg-primary-400"> <div className="col-md-10 bg-primary-400">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} /> <Image alt={getSiteConfig().siteName} className="logo" src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="d-flex align-items-center justify-content-center mb-4 "> <div className="d-flex align-items-center justify-content-center mb-4 ">
<div className={classNames({ 'mt-1 medium-yellow-line': getConfig().SITE_NAME === 'edX' })} /> <div className={classNames({ 'mt-1 medium-yellow-line': getSiteConfig().siteName === 'edX' })} />
<div> <div>
<h1 <h1
className={classNames( className={classNames(
'display-1 text-white mt-5 mb-5 mr-2 main-heading', 'display-1 text-white mt-5 mb-5 mr-2 main-heading',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' }, { 'ml-4.5': getSiteConfig().siteName !== 'edX' },
)} )}
> >
<span> <span>
{formatMessage(messages['start.learning'])}{' '} {formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block"> <span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} {formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
</span> </span>
</span> </span>
</h1> </h1>

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -12,11 +11,11 @@ const SmallLayout = () => {
<span className="bg-primary-400 w-100"> <span className="bg-primary-400 w-100">
<div className="col-md-12 small-screen-top-stripe" /> <div className="col-md-12 small-screen-top-stripe" />
<div> <div>
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="d-flex align-items-center m-3.5"> <div className="d-flex align-items-center m-3.5">
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} /> <div className={classNames({ 'small-yellow-line mr-n2.5': getSiteConfig().siteName === 'edX' })} />
<h1 <h1
className={classNames( className={classNames(
'text-white mt-3.5 mb-3.5', 'text-white mt-3.5 mb-3.5',
@@ -25,7 +24,7 @@ const SmallLayout = () => {
<span> <span>
{formatMessage(messages['start.learning'])}{' '} {formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block"> <span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })} {formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
</span> </span>
</span> </span>
</h1> </h1>

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@openedx/frontend-base';
const messages = defineMessages({ const messages = defineMessages({
'start.learning': { 'start.learning': {

View File

@@ -1,7 +1,4 @@
import React from 'react'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import messages from './messages'; import messages from './messages';
@@ -12,10 +9,10 @@ const ExtraSmallLayout = () => {
return ( return (
<span <span
className="w-100 bg-primary-500 banner__image extra-small-layout" className="w-100 bg-primary-500 banner__image extra-small-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }} style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
> >
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5"> <div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
<h1 className="banner__heading"> <h1 className="banner__heading">

View File

@@ -1,7 +1,4 @@
import React from 'react'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import './index.scss'; import './index.scss';
@@ -13,10 +10,10 @@ const LargeLayout = () => {
return ( return (
<div <div
className="w-50 bg-primary-500 banner__image large-layout" className="w-50 bg-primary-500 banner__image large-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }} style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_LARGE})` }}
> >
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="company-logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="min-vh-100 p-5 d-flex align-items-end"> <div className="min-vh-100 p-5 d-flex align-items-end">
<h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center"> <h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center">

View File

@@ -1,7 +1,4 @@
import React from 'react'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import './index.scss'; import './index.scss';
@@ -13,10 +10,10 @@ const MediumLayout = () => {
return ( return (
<div <div
className="w-100 mb-3 bg-primary-500 banner__image medium-layout" className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }} style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_MEDIUM})` }}
> >
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="ml-5 pb-4 pt-4"> <div className="ml-5 pb-4 pt-4">
<h1 className="display-2 banner__heading"> <h1 className="display-2 banner__heading">

View File

@@ -1,7 +1,4 @@
import React from 'react'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import messages from './messages'; import messages from './messages';
@@ -12,10 +9,10 @@ const SmallLayout = () => {
return ( return (
<span <span
className="w-100 bg-primary-500 banner__image small-layout" className="w-100 bg-primary-500 banner__image small-layout"
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }} style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_SMALL})` }}
> >
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} /> <Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
</Hyperlink> </Hyperlink>
<div className="ml-5 mr-1 pb-3.5 pt-3.5"> <div className="ml-5 mr-1 pb-3.5 pt-3.5">
<h1 className="display-2"> <h1 className="display-2">

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@openedx/frontend-base';
const messages = defineMessages({ const messages = defineMessages({
'your.career.turning.point': { 'your.career.turning.point': {

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -11,14 +10,14 @@ const LargeLayout = ({ fullName }) => {
return ( return (
<div className="w-50 d-flex"> <div className="w-50 d-flex">
<div className="col-md-10 bg-light-200 p-0"> <div className="col-md-10 bg-light-200 p-0">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} /> <Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
</Hyperlink> </Hyperlink>
<div className="min-vh-100 d-flex align-items-center"> <div className="min-vh-100 d-flex align-items-center">
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" /> <div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
<div> <div>
<h1 className="welcome-to-platform data-hj-suppress"> <h1 className="welcome-to-platform data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })} {formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
</h1> </h1>
<h2 className="complete-your-profile"> <h2 className="complete-your-profile">
{formatMessage(messages['complete.your.profile.1'])} {formatMessage(messages['complete.your.profile.1'])}

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -13,14 +12,14 @@ const MediumLayout = ({ fullName }) => {
<div className="w-100 medium-screen-top-stripe" /> <div className="w-100 medium-screen-top-stripe" />
<div className="w-100 p-0 mb-3 d-flex"> <div className="w-100 p-0 mb-3 d-flex">
<div className="col-md-10 bg-light-200"> <div className="col-md-10 bg-light-200">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} /> <Image className="logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
</Hyperlink> </Hyperlink>
<div className="d-flex align-items-center justify-content-center mb-4 ml-5"> <div className="d-flex align-items-center justify-content-center mb-4 ml-5">
<div className="medium-yellow-line mt-5 mr-n2" /> <div className="medium-yellow-line mt-5 mr-n2" />
<div> <div>
<h1 className="h3 data-hj-suppress mw-320"> <h1 className="h3 data-hj-suppress mw-320">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })} {formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
</h1> </h1>
<h2 className="display-1"> <h2 className="display-1">
{formatMessage(messages['complete.your.profile.1'])} {formatMessage(messages['complete.your.profile.1'])}

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon'; import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -11,14 +10,14 @@ const SmallLayout = ({ fullName }) => {
return ( return (
<div className="min-vw-100 bg-light-200"> <div className="min-vw-100 bg-light-200">
<div className="col-md-12 small-screen-top-stripe" /> <div className="col-md-12 small-screen-top-stripe" />
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}> <Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} /> <Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
</Hyperlink> </Hyperlink>
<div className="d-flex align-items-center m-3.5"> <div className="d-flex align-items-center m-3.5">
<div className="small-yellow-line mt-4.5" /> <div className="small-yellow-line mt-4.5" />
<div> <div>
<h1 className="h5 data-hj-suppress"> <h1 className="h5 data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })} {formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
</h1> </h1>
<h2 className="h1"> <h2 className="h1">
{formatMessage(messages['complete.your.profile.1'])} {formatMessage(messages['complete.your.profile.1'])}

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@openedx/frontend-base';
const messages = defineMessages({ const messages = defineMessages({
'welcome.to.platform': { 'welcome.to.platform': {

View File

@@ -1,4 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig } from '@openedx/frontend-base';
import { breakpoints } from '@openedx/paragon'; import { breakpoints } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -11,7 +11,7 @@ import {
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout'; import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => { const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT; const enableImageLayout = useAppConfig().ENABLE_IMAGE_LAYOUT;
if (enableImageLayout) { if (enableImageLayout) {
return ( return (

View File

@@ -1,9 +1,9 @@
import { mergeConfig } from '@edx/frontend-platform'; import { IntlProvider, mergeAppConfig } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { Context as ResponsiveContext } from 'react-responsive'; import { Context as ResponsiveContext } from 'react-responsive';
import BaseContainer from '../index'; import BaseContainer from '../index';
import { appId } from '../../constants';
const LargeScreen = { const LargeScreen = {
wrappingComponent: ResponsiveContext.Provider, wrappingComponent: ResponsiveContext.Provider,
@@ -26,7 +26,7 @@ describe('Base component tests', () => {
}); });
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => { it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
mergeConfig({ mergeAppConfig(appId, {
ENABLE_IMAGE_LAYOUT: true, ENABLE_IMAGE_LAYOUT: true,
}); });

View File

@@ -1,6 +1,5 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { import {
Button, Form, Button, Form,
Icon, Icon,
@@ -8,8 +7,8 @@ import {
import { Login } from '@openedx/paragon/icons'; import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants'; import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
/** /**
* This component renders the Single sign-on (SSO) button only for the tpa provider passed * This component renders the Single sign-on (SSO) button only for the tpa provider passed
@@ -17,12 +16,12 @@ import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
const EnterpriseSSO = (props) => { const EnterpriseSSO = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const tpaProvider = props.provider; const tpaProvider = props.provider;
const hideRegistrationLink = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false const hideRegistrationLink = useAppConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|| getConfig().SHOW_REGISTRATION_LINKS === false; || useAppConfig().SHOW_REGISTRATION_LINKS === false;
const handleSubmit = (e, url) => { const handleSubmit = (e, url) => {
e.preventDefault(); e.preventDefault();
window.location.href = getConfig().LMS_BASE_URL + url; window.location.href = getSiteConfig().lmsBaseUrl + url;
}; };
const handleClick = (e) => { const handleClick = (e) => {
@@ -48,7 +47,7 @@ const EnterpriseSSO = (props) => {
{tpaProvider.iconImage ? ( {tpaProvider.iconImage ? (
<div aria-hidden="true"> <div aria-hidden="true">
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} /> <img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span> <span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
</div> </div>
) )
: ( : (
@@ -60,7 +59,7 @@ const EnterpriseSSO = (props) => {
<Icon className="h-75" src={Login} /> <Icon className="h-75" src={Login} />
)} )}
</div> </div>
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span> <span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
</> </>
)} )}
</Button> </Button>

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink, Icon } from '@openedx/paragon'; import { Button, Hyperlink, Icon } from '@openedx/paragon';
import { Institution } from '@openedx/paragon/icons'; import { Institution } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -29,7 +28,7 @@ export const RenderInstitutionButton = props => {
* This component renders the page list of available institutions for login * This component renders the page list of available institutions for login
* */ * */
const InstitutionLogistration = props => { const InstitutionLogistration = props => {
const lmsBaseUrl = getConfig().LMS_BASE_URL; const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { const {
secondaryProviders, secondaryProviders,

View File

@@ -1,4 +1,4 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@openedx/frontend-base';
const NotFoundPage = () => ( const NotFoundPage = () => (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center"> <div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@openedx/frontend-base';
import { import {
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle, Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -9,11 +9,11 @@ import {
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants'; import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { useRegisterContext } from '../register/components/RegisterContext';
import { useFieldValidations } from '../register/data/apiHook';
import { validatePasswordField } from '../register/data/utils'; import { validatePasswordField } from '../register/data/utils';
import messages from './messages';
const noopFn = () => {};
const PasswordField = (props) => { const PasswordField = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -21,20 +21,10 @@ const PasswordField = (props) => {
const [showTooltip, setShowTooltip] = useState(false); const [showTooltip, setShowTooltip] = useState(false);
const { const {
setValidationsSuccess, validationApiRateLimited = false,
setValidationsFailure, clearRegistrationBackendError = noopFn,
validationApiRateLimited, validateField = noopFn,
clearRegistrationBackendError, } = props;
} = useRegisterContext();
const fieldValidationsMutation = useFieldValidations({
onSuccess: (data) => {
setValidationsSuccess(data);
},
onError: () => {
setValidationsFailure();
},
});
const handleBlur = (e) => { const handleBlur = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -63,7 +53,7 @@ const PasswordField = (props) => {
if (fieldError) { if (fieldError) {
props.handleErrorChange('password', fieldError); props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) { } else if (!validationApiRateLimited) {
fieldValidationsMutation.mutate({ password: passwordValue }); validateField({ password: passwordValue });
} }
} }
}; };
@@ -168,6 +158,9 @@ PasswordField.defaultProps = {
showRequirements: true, showRequirements: true,
showScreenReaderText: true, showScreenReaderText: true,
autoComplete: null, autoComplete: null,
clearRegistrationBackendError: noopFn,
validateField: noopFn,
validationApiRateLimited: false,
}; };
PasswordField.propTypes = { PasswordField.propTypes = {
@@ -183,6 +176,9 @@ PasswordField.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
autoComplete: PropTypes.string, autoComplete: PropTypes.string,
showScreenReaderText: PropTypes.bool, showScreenReaderText: PropTypes.bool,
clearRegistrationBackendError: PropTypes.func,
validateField: PropTypes.func,
validationApiRateLimited: PropTypes.bool,
}; };
export default PasswordField; export default PasswordField;

View File

@@ -1,9 +1,9 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT, AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
} from '../data/constants'; } from '../data/constants';
import { setCookie } from '../data/utils'; import { setCookie } from '../data/utils';
@@ -15,20 +15,20 @@ const RedirectLogistration = (props) => {
redirectToProgressiveProfilingPage, redirectToProgressiveProfilingPage,
success, success,
optionalFields, optionalFields,
redirectToRecommendationsPage,
educationLevel, educationLevel,
userId, userId,
registrationEmbedded, registrationEmbedded,
host, host,
} = props; } = props;
let finalRedirectUrl = ''; let finalRedirectUrl = '';
if (success) { if (success) {
// If we're in a third party auth pipeline, we must complete the pipeline // If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url. // once user has successfully logged in. Otherwise, redirect to the specified redirect url.
// Note: For multiple enterprise use case, we need to make sure that user first visits the // Note: For multiple enterprise use case, we need to make sure that user first visits the
// enterprise selection page and then complete the auth workflow // enterprise selection page and then complete the auth workflow
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) { if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl; finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
} else { } else {
finalRedirectUrl = redirectUrl; finalRedirectUrl = redirectUrl;
} }
@@ -36,12 +36,12 @@ const RedirectLogistration = (props) => {
// Redirect to Progressive Profiling after successful registration // Redirect to Progressive Profiling after successful registration
if (redirectToProgressiveProfilingPage) { if (redirectToProgressiveProfilingPage) {
// TODO: Do we still need this cookie? // TODO: Do we still need this cookie?
setCookie('van-504-returning-user', true); setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
if (registrationEmbedded) { if (registrationEmbedded) {
window.parent.postMessage({ window.parent.postMessage({
action: REDIRECT, action: REDIRECT,
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL, redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
}, host); }, host);
return null; return null;
} }
@@ -59,22 +59,6 @@ const RedirectLogistration = (props) => {
); );
} }
// Redirect to Recommendation page
if (redirectToRecommendationsPage) {
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Navigate
to={RECOMMENDATIONS}
state={{
registrationResult,
educationLevel,
userId,
}}
replace
/>
);
}
window.location.href = finalRedirectUrl; window.location.href = finalRedirectUrl;
} }
@@ -89,7 +73,6 @@ RedirectLogistration.defaultProps = {
redirectUrl: '', redirectUrl: '',
redirectToProgressiveProfilingPage: false, redirectToProgressiveProfilingPage: false,
optionalFields: {}, optionalFields: {},
redirectToRecommendationsPage: false,
userId: null, userId: null,
registrationEmbedded: false, registrationEmbedded: false,
host: '', host: '',
@@ -103,7 +86,6 @@ RedirectLogistration.propTypes = {
redirectUrl: PropTypes.string, redirectUrl: PropTypes.string,
redirectToProgressiveProfilingPage: PropTypes.bool, redirectToProgressiveProfilingPage: PropTypes.bool,
optionalFields: PropTypes.shape({}), optionalFields: PropTypes.shape({}),
redirectToRecommendationsPage: PropTypes.bool,
userId: PropTypes.number, userId: PropTypes.number,
registrationEmbedded: PropTypes.bool, registrationEmbedded: PropTypes.bool,
host: PropTypes.string, host: PropTypes.string,

View File

@@ -1,12 +1,11 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import { Icon } from '@openedx/paragon'; import { Icon } from '@openedx/paragon';
import { Login } from '@openedx/paragon/icons'; import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants'; import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
const SocialAuthProviders = (props) => { const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -16,7 +15,7 @@ const SocialAuthProviders = (props) => {
e.preventDefault(); e.preventDefault();
const url = e.currentTarget.dataset.providerUrl; const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url; window.location.href = getSiteConfig().lmsBaseUrl + url;
} }
const socialAuth = socialAuthProviders.map((provider, index) => ( const socialAuth = socialAuthProviders.map((provider, index) => (

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
Hyperlink, Icon, Hyperlink, Icon,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -8,10 +7,10 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import messages from './messages';
import { import {
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
import messages from './messages';
import { import {
RenderInstitutionButton, RenderInstitutionButton,
@@ -33,8 +32,8 @@ const ThirdPartyAuth = (props) => {
} = props; } = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider; const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider; const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; const isEnterpriseLoginDisabled = useAppConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL; const enterpriseLoginURL = getSiteConfig().lmsBaseUrl + ENTERPRISE_LOGIN_URL;
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive); const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return ( return (

View File

@@ -1,15 +1,14 @@
import { getConfig } from '@edx/frontend-platform'; import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import messages from './messages';
const ThirdPartyAuthAlert = (props) => { const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { currentProvider, referrer } = props; const { currentProvider, referrer } = props;
const platformName = getConfig().SITE_NAME; const platformName = getSiteConfig().siteName;
let message; let message;
if (referrer === LOGIN_PAGE) { if (referrer === LOGIN_PAGE) {
@@ -28,7 +27,7 @@ const ThirdPartyAuthAlert = (props) => {
{referrer === REGISTER_PAGE ? ( {referrer === REGISTER_PAGE ? (
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading> <Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
) : null} ) : null}
<p>{ message }</p> <p>{message}</p>
</Alert> </Alert>
{referrer === REGISTER_PAGE ? ( {referrer === REGISTER_PAGE ? (
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4> <h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
@@ -25,7 +24,7 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) { if (isReady) {
if (authUser && authUser.username) { if (authUser && authUser.username) {
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
return null; return null;
} }

View File

@@ -1,59 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';
import messages from './messages';
import { REGISTER_EMBEDDED_PAGE } from '../data/constants';
const ZendeskHelp = () => {
const { formatMessage } = useIntl();
const setting = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
departments: {
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
},
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [{ id: 'description', prefill: { '*': '' } }],
},
],
selectTicketForm: {
'*': formatMessage(messages.selectTicketForm),
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': formatMessage(messages.supportTitle) },
avatar: {
url: getConfig().ZENDESK_LOGO_URL,
name: { '*': formatMessage(messages.supportTitle) },
},
},
},
};
if (window.location.pathname === REGISTER_EMBEDDED_PAGE) {
return null;
}
return (
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
);
};
export default ZendeskHelp;

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext'; import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext';
const TestComponent = () => { const TestComponent = () => {
@@ -29,7 +28,7 @@ describe('ThirdPartyAuthContext', () => {
</ThirdPartyAuthProvider>, </ThirdPartyAuthProvider>,
); );
expect(screen.getByText('Test Child')).toBeInTheDocument(); expect(screen.getByText('Test Child')).toBeTruthy();
}); });
it('should provide all context values to children', () => { it('should provide all context values to children', () => {
@@ -39,10 +38,10 @@ describe('ThirdPartyAuthContext', () => {
</ThirdPartyAuthProvider>, </ThirdPartyAuthProvider>,
); );
expect(screen.getByText('FieldDescriptions Available')).toBeInTheDocument(); expect(screen.getByText('FieldDescriptions Available')).toBeTruthy();
expect(screen.getByText('OptionalFields Available')).toBeInTheDocument(); expect(screen.getByText('OptionalFields Available')).toBeTruthy();
expect(screen.getByText('AuthApiStatus Not Available')).toBeInTheDocument(); // Initially null expect(screen.getByText('AuthApiStatus Not Available')).toBeTruthy(); // Initially null
expect(screen.getByText('AuthContext Available')).toBeInTheDocument(); expect(screen.getByText('AuthContext Available')).toBeTruthy();
}); });
it('should render multiple children', () => { it('should render multiple children', () => {
@@ -54,8 +53,8 @@ describe('ThirdPartyAuthContext', () => {
</ThirdPartyAuthProvider>, </ThirdPartyAuthProvider>,
); );
expect(screen.getByText('First Child')).toBeInTheDocument(); expect(screen.getByText('First Child')).toBeTruthy();
expect(screen.getByText('Second Child')).toBeInTheDocument(); expect(screen.getByText('Second Child')).toBeTruthy();
expect(screen.getByText('Third Child')).toBeInTheDocument(); expect(screen.getByText('Third Child')).toBeTruthy();
}); });
}); });

View File

@@ -5,34 +5,34 @@ import {
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants'; import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
interface ThirdPartyAuthContextType { interface ThirdPartyAuthContextType {
fieldDescriptions: any; fieldDescriptions: any,
optionalFields: { optionalFields: {
fields: any; fields: any,
extended_profile: any[]; extended_profile: any[],
}; },
thirdPartyAuthApiStatus: string | null; thirdPartyAuthApiStatus: string | null,
thirdPartyAuthContext: { thirdPartyAuthContext: {
platformName: string | null; platformName: string | null,
autoSubmitRegForm: boolean; autoSubmitRegForm: boolean,
currentProvider: string | null; currentProvider: string | null,
finishAuthUrl: string | null; finishAuthUrl: string | null,
countryCode: string | null; countryCode: string | null,
providers: any[]; providers: any[],
secondaryProviders: any[]; secondaryProviders: any[],
pipelineUserDetails: any | null; pipelineUserDetails: any | null,
errorMessage: string | null; errorMessage: string | null,
welcomePageRedirectUrl: string | null; welcomePageRedirectUrl: string | null,
}; },
setThirdPartyAuthContextBegin: () => void; setThirdPartyAuthContextBegin: () => void,
setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void; setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void,
setThirdPartyAuthContextFailure: () => void; setThirdPartyAuthContextFailure: () => void,
clearThirdPartyAuthErrorMessage: () => void; clearThirdPartyAuthErrorMessage: () => void,
} }
const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined); const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined);
interface ThirdPartyAuthProviderProps { interface ThirdPartyAuthProviderProps {
children: ReactNode; children: ReactNode,
} }
export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => { export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => {

View File

@@ -1,7 +1,6 @@
import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getThirdPartyAuthContext = async (urlParams : string) => { const getThirdPartyAuthContext = async (urlParams: string) => {
const requestConfig = { const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams, params: urlParams,
@@ -10,7 +9,7 @@ const getThirdPartyAuthContext = async (urlParams : string) => {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get( .get(
`${getConfig().LMS_BASE_URL}/api/mfe_context`, `${getSiteConfig().lmsBaseUrl}/api/mfe_context`,
requestConfig, requestConfig,
); );
return { return {

View File

@@ -6,10 +6,12 @@ import { ThirdPartyAuthQueryKeys } from './queryKeys';
// Error constants // Error constants
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error'; export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';
const useThirdPartyAuthHook = (pageId, payload) => useQuery({ const useThirdPartyAuthHook = (pageId, payload, { enabled = true } = {}) => useQuery({
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId), queryKey: ThirdPartyAuthQueryKeys.byPage(pageId, payload),
queryFn: () => getThirdPartyAuthContext(payload), queryFn: () => getThirdPartyAuthContext(payload),
retry: false, retry: false,
staleTime: 5 * 60 * 1000, // 5 minutes — TPA context is effectively static per session
enabled,
}); });
export { export {

View File

@@ -2,5 +2,5 @@ import { appId } from '../../constants';
export const ThirdPartyAuthQueryKeys = { export const ThirdPartyAuthQueryKeys = {
all: [appId, 'ThirdPartyAuth'] as const, all: [appId, 'ThirdPartyAuth'] as const,
byPage: (pageId: string) => [appId, 'ThirdPartyAuth', pageId] as const, byPage: (pageId: string, payload?: unknown) => [appId, 'ThirdPartyAuth', pageId, payload] as const,
}; };

View File

@@ -9,4 +9,3 @@ export { default as InstitutionLogistration } from './InstitutionLogistration';
export { RenderInstitutionButton } from './InstitutionLogistration'; export { RenderInstitutionButton } from './InstitutionLogistration';
export { default as FormGroup } from './FormGroup'; export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField'; export { default as PasswordField } from './PasswordField';
export { default as Zendesk } from './Zendesk';

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@openedx/frontend-base';
const messages = defineMessages({ const messages = defineMessages({
// institution login strings // institution login strings
@@ -85,33 +85,23 @@ const messages = defineMessages({
'login.third.party.auth.account.not.linked': { 'login.third.party.auth.account.not.linked': {
id: 'login.third.party.auth.account.not.linked', id: 'login.third.party.auth.account.not.linked',
defaultMessage: 'You have successfully signed into {currentProvider}, but your {currentProvider} ' defaultMessage: 'You have successfully signed into {currentProvider}, but your {currentProvider} '
+ 'account does not have a linked {platformName} account. To link your accounts, ' + 'account does not have a linked {platformName} account. To link your accounts, '
+ 'sign in now using your {platformName} password.', + 'sign in now using your {platformName} password.',
description: 'Message that appears on login page if user has successfully authenticated with social ' description: 'Message that appears on login page if user has successfully authenticated with social '
+ 'auth but no associated platform account exists', + 'auth but no associated platform account exists',
}, },
'register.third.party.auth.account.not.linked': { 'register.third.party.auth.account.not.linked': {
id: 'register.third.party.auth.account.not.linked', id: 'register.third.party.auth.account.not.linked',
defaultMessage: 'You\'ve successfully signed into {currentProvider}! We just need a little more information ' defaultMessage: 'You\'ve successfully signed into {currentProvider}! We just need a little more information '
+ 'before you start learning with {platformName}.', + 'before you start learning with {platformName}.',
description: 'Message that appears on register page if user has successfully authenticated with TPA ' description: 'Message that appears on register page if user has successfully authenticated with TPA '
+ 'but no associated platform account exists', + 'but no associated platform account exists',
}, },
'registration.using.tpa.form.heading': { 'registration.using.tpa.form.heading': {
id: 'registration.using.tpa.form.heading', id: 'registration.using.tpa.form.heading',
defaultMessage: 'Finish creating your account', defaultMessage: 'Finish creating your account',
description: 'Heading that appears above form when user is trying to create account using social auth', description: 'Heading that appears above form when user is trying to create account using social auth',
}, },
supportTitle: {
id: 'zendesk.supportTitle',
description: 'Title for the support button',
defaultMessage: 'edX Support',
},
selectTicketForm: {
id: 'zendesk.selectTicketForm',
description: 'Select ticket form',
defaultMessage: 'Please choose your request type:',
},
'registration.other.options.heading': { 'registration.other.options.heading': {
id: 'registration.other.options.heading', id: 'registration.other.options.heading',
defaultMessage: 'Or register with:', defaultMessage: 'Or register with:',

View File

@@ -1,8 +1,7 @@
/* eslint-disable import/no-import-module-exports */ /* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */ /* eslint-disable react/function-component-definition */
import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getSiteConfig } from '@openedx/frontend-base';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { import {
@@ -15,7 +14,7 @@ import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
const RRD = require('react-router-dom'); const RRD = require('react-router-dom');
// Just render plain div with its children // Just render plain div with its children
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types
RRD.BrowserRouter = ({ children }) => <div>{ children }</div>; RRD.BrowserRouter = ({ children }) => <div>{children}</div>;
module.exports = RRD; module.exports = RRD;
const TestApp = () => ( const TestApp = () => (
@@ -60,7 +59,7 @@ describe('EmbeddedRegistrationRoute', () => {
it('should render embedded register page if host query param is available in the url (embedded)', async () => { it('should render embedded register page if host query param is available in the url (embedded)', async () => {
delete window.location; delete window.location;
window.location = { window.location = {
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE), href: getSiteConfig().baseUrl.concat(REGISTER_EMBEDDED_PAGE),
search: '?host=http://localhost/host-websit', search: '?host=http://localhost/host-websit',
}; };

View File

@@ -1,20 +1,10 @@
import React from 'react'; import { IntlProvider } from '@openedx/frontend-base';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import { RegisterProvider } from '../../register/components/RegisterContext';
import { useFieldValidations } from '../../register/data/apiHook';
import FormGroup from '../FormGroup'; import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField'; import PasswordField from '../PasswordField';
// Mock the useFieldValidations hook
jest.mock('../../register/data/apiHook', () => ({
useFieldValidations: jest.fn(),
}));
describe('FormGroup', () => { describe('FormGroup', () => {
const props = { const props = {
@@ -42,51 +32,24 @@ describe('FormGroup', () => {
describe('PasswordField', () => { describe('PasswordField', () => {
let props = {}; let props = {};
let queryClient;
let mockMutate;
const renderWrapper = (children) => ( const wrapper = children => (
<QueryClientProvider client={queryClient}> <IntlProvider locale="en">
<IntlProvider locale="en"> {children}
<MemoryRouter> </IntlProvider>
<RegisterProvider>
{children}
</RegisterProvider>
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
); );
beforeEach(() => { beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
mockMutate = jest.fn();
useFieldValidations.mockReturnValue({
mutate: mockMutate,
isPending: false,
});
props = { props = {
floatingLabel: 'Password', floatingLabel: 'Password',
name: 'password', name: 'password',
value: 'password123', value: 'password123',
handleFocus: jest.fn(), handleFocus: jest.fn(),
}; };
jest.clearAllMocks();
}); });
it('should show/hide password on icon click', () => { it('should show/hide password on icon click', () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password'); const showPasswordButton = getByLabelText('Show password');
@@ -99,7 +62,7 @@ describe('PasswordField', () => {
}); });
it('should show password requirement tooltip on focus', async () => { it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -116,7 +79,7 @@ describe('PasswordField', () => {
...props, ...props,
value: '', value: '',
}; };
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -139,7 +102,7 @@ describe('PasswordField', () => {
}); });
it('should update password requirement checks', async () => { it('should update password requirement checks', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -162,7 +125,7 @@ describe('PasswordField', () => {
}); });
it('should not run validations when blur is fired on password icon click', () => { it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { container, getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -183,7 +146,7 @@ describe('PasswordField', () => {
...props, ...props,
handleBlur: jest.fn(), handleBlur: jest.fn(),
}; };
const { container } = render(renderWrapper(<PasswordField {...props} />)); const { container } = render(wrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, { fireEvent.blur(passwordInput, {
@@ -201,7 +164,7 @@ describe('PasswordField', () => {
...props, ...props,
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { container } = render(renderWrapper(<PasswordField {...props} />)); const { container } = render(wrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, { fireEvent.blur(passwordInput, {
@@ -224,7 +187,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -244,7 +207,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -263,11 +226,13 @@ describe('PasswordField', () => {
}); });
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => { it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
const mockValidateField = jest.fn();
props = { props = {
...props, ...props,
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
validateField: mockValidateField,
}; };
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password'); const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, { fireEvent.blur(passwordField, {
target: { target: {
@@ -276,7 +241,7 @@ describe('PasswordField', () => {
}, },
}); });
expect(mockMutate).toHaveBeenCalledWith({ password: 'password123' }); expect(mockValidateField).toHaveBeenCalledWith({ password: 'password123' });
}); });
it('should use password value from prop when password icon is focused out (blur due to icon)', () => { it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
@@ -286,7 +251,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
handleBlur: jest.fn(), handleBlur: jest.fn(),
}; };
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />)); const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');

View File

@@ -1,4 +1,4 @@
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@openedx/frontend-base';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import registerIcons from '../RegisterFaIcons'; import registerIcons from '../RegisterFaIcons';

View File

@@ -1,4 +1,4 @@
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@openedx/frontend-base';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants'; import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';

View File

@@ -1,6 +1,7 @@
/* eslint-disable import/no-import-module-exports */ /* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */ /* eslint-disable react/function-component-definition */
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@openedx/frontend-base';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { import {
@@ -10,7 +11,8 @@ import {
import { UnAuthOnlyRoute } from '..'; import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants'; import { REGISTER_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
getAuthenticatedUser: jest.fn(), getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(), fetchAuthenticatedUser: jest.fn(),
})); }));

View File

@@ -1,17 +0,0 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import Zendesk from '../Zendesk';
jest.mock('react-zendesk', () => 'Zendesk');
describe('Zendesk Help', () => {
it('should match login page third party auth alert message snapshot', () => {
const tree = renderer.create(
<IntlProvider locale="en">
<Zendesk />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -13,7 +13,7 @@ exports[`ThirdPartyAuthAlert renders skeleton for pending third-party auth 1`] =
className="alert-message-content" className="alert-message-content"
> >
<p> <p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password. You have successfully signed into Google, but your Google account does not have a linked Test Site account. To link your accounts, sign in now using your Test Site password.
</p> </p>
</div> </div>
</div> </div>
@@ -33,7 +33,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
className="alert-message-content" className="alert-message-content"
> >
<p> <p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password. You have successfully signed into Google, but your Google account does not have a linked Test Site account. To link your accounts, sign in now using your Test Site password.
</p> </p>
</div> </div>
</div> </div>
@@ -59,7 +59,7 @@ exports[`ThirdPartyAuthAlert should match register page third party auth alert m
Almost done! Almost done!
</div> </div>
<p> <p>
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here. You've successfully signed into Google! We just need a little more information before you start learning with Test Site.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Zendesk Help should match login page third party auth alert message snapshot 1`] = `
<Zendesk
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false,
},
"contactForm": {
"attachments": true,
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": {
"enabled": false,
},
"helpCenter": {
"originalArticleButton": true,
},
}
}
/>
`;

View File

@@ -1,40 +0,0 @@
const configuration = {
// Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
POST_REGISTRATION_REDIRECT_URL: process.env.POST_REGISTRATION_REDIRECT_URL || '',
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
TOS_LINK: process.env.TOS_LINK || null,
// Base container images
BANNER_IMAGE_LARGE: process.env.BANNER_IMAGE_LARGE || '',
BANNER_IMAGE_MEDIUM: process.env.BANNER_IMAGE_MEDIUM || '',
BANNER_IMAGE_SMALL: process.env.BANNER_IMAGE_SMALL || '',
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
// Recommendation constants
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
// Miscellaneous
INFO_EMAIL: process.env.INFO_EMAIL || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
};
export default configuration;

View File

@@ -1,20 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import algoliasearch from 'algoliasearch';
// initialize Algolia workers
const initializeSearchClient = () => algoliasearch(
getConfig().ALGOLIA_APP_ID,
getConfig().ALGOLIA_SEARCH_API_KEY,
);
const getLocationRestrictionFilter = (userCountry) => {
if (userCountry) {
return `NOT blocked_in:"${userCountry}" AND (allowed_in:"null" OR allowed_in:"${userCountry}")`;
}
return '';
};
export {
initializeSearchClient,
getLocationRestrictionFilter,
};

View File

@@ -5,7 +5,6 @@ export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
export const RESET_PAGE = '/reset'; export const RESET_PAGE = '/reset';
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome'; export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
export const DEFAULT_REDIRECT_URL = '/dashboard'; export const DEFAULT_REDIRECT_URL = '/dashboard';
export const RECOMMENDATIONS = '/recommendations';
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/'; export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
export const PAGE_NOT_FOUND = '/notfound'; export const PAGE_NOT_FOUND = '/notfound';
export const ENTERPRISE_LOGIN_URL = '/enterprise/login'; export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
@@ -29,9 +28,9 @@ export const EMBEDDED = 'embedded';
export const LETTER_REGEX = /[a-zA-Z]/; export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/; export const NUMBER_REGEX = /\d/;
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*' export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"' + '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})' + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$'; + '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
// Query string parameters that can be passed to LMS to manage // Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration. // things like auto-enrollment upon login and registration.

59
src/data/countries.ts Normal file
View File

@@ -0,0 +1,59 @@
import { getPrimaryLanguageSubtag } from '@openedx/frontend-base';
import COUNTRIES, { langs as countryLangs } from 'i18n-iso-countries';
import arLocale from 'i18n-iso-countries/langs/ar.json';
import caLocale from 'i18n-iso-countries/langs/ca.json';
import enLocale from 'i18n-iso-countries/langs/en.json';
import esLocale from 'i18n-iso-countries/langs/es.json';
import frLocale from 'i18n-iso-countries/langs/fr.json';
import heLocale from 'i18n-iso-countries/langs/he.json';
import idLocale from 'i18n-iso-countries/langs/id.json';
import koLocale from 'i18n-iso-countries/langs/ko.json';
import plLocale from 'i18n-iso-countries/langs/pl.json';
import ptLocale from 'i18n-iso-countries/langs/pt.json';
import ruLocale from 'i18n-iso-countries/langs/ru.json';
import ukLocale from 'i18n-iso-countries/langs/uk.json';
import zhLocale from 'i18n-iso-countries/langs/zh.json';
COUNTRIES.registerLocale(arLocale);
COUNTRIES.registerLocale(enLocale);
COUNTRIES.registerLocale(esLocale);
COUNTRIES.registerLocale(frLocale);
COUNTRIES.registerLocale(zhLocale);
COUNTRIES.registerLocale(caLocale);
COUNTRIES.registerLocale(heLocale);
COUNTRIES.registerLocale(idLocale);
COUNTRIES.registerLocale(koLocale);
COUNTRIES.registerLocale(plLocale);
COUNTRIES.registerLocale(ptLocale);
COUNTRIES.registerLocale(ruLocale);
COUNTRIES.registerLocale(ukLocale);
/**
* Provides a lookup table of country IDs to country names for the current locale.
*
* @memberof module:I18n
*/
export function getCountryMessages(locale) {
const primaryLanguageSubtag = getPrimaryLanguageSubtag(locale);
const languageCode = countryLangs().includes(primaryLanguageSubtag) ? primaryLanguageSubtag : 'en';
return COUNTRIES.getNames(languageCode);
}
/**
* Provides a list of countries represented as objects of the following shape:
*
* {
* key, // The ID of the country
* name // The localized name of the country
* }
*
* TODO: ARCH-878: The list should be sorted alphabetically in the current locale.
* This is useful for populating dropdowns.
*
* @memberof module:I18n
*/
export function getCountryList(locale) {
const countryMessages = getCountryMessages(locale);
return Object.entries(countryMessages).map(([code, name]) => ({ code, name }));
}

View File

@@ -1,17 +0,0 @@
import {
createInstance,
} from '@optimizely/react-sdk';
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
const getOptimizelyInstance = () => {
if (OPTIMIZELY_SDK_KEY) {
return createInstance({
sdkKey: OPTIMIZELY_SDK_KEY,
});
}
return null;
};
export default getOptimizelyInstance();

View File

@@ -1,16 +0,0 @@
import { getLocationRestrictionFilter } from '../algolia';
describe('algoliaUtilsTests', () => {
it('test getLocationRestrictionFilter returns filter if country is passed', () => {
const countryCode = 'PK';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = `NOT blocked_in:"${countryCode}" AND (allowed_in:"null" OR allowed_in:"${countryCode}")`;
expect(filter).toEqual(expectedFilter);
});
it('test getLocationRestrictionFilter returns empty string if country is not passed', () => {
const countryCode = '';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = '';
expect(filter).toEqual(expectedFilter);
});
});

View File

@@ -1,13 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import { setCookie } from '../utils'; import { setCookie } from '../utils';
// Mock getConfig function
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
// Mock Cookies class // Mock Cookies class
jest.mock('universal-cookie'); jest.mock('universal-cookie');
@@ -17,9 +11,7 @@ describe('setCookie function', () => {
}); });
it('should set a cookie with default options', () => { it('should set a cookie with default options', () => {
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' }); setCookie('testCookie', 'testValue', 'example.com');
setCookie('testCookie', 'testValue');
expect(Cookies).toHaveBeenCalled(); expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith(); expect(Cookies).toHaveBeenCalledWith();
@@ -30,10 +22,8 @@ describe('setCookie function', () => {
}); });
it('should set a cookie with specified expiry', () => { it('should set a cookie with specified expiry', () => {
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
const expiry = new Date('2023-12-31'); const expiry = new Date('2023-12-31');
setCookie('testCookie', 'testValue', expiry); setCookie('testCookie', 'testValue', 'example.com', expiry);
expect(Cookies).toHaveBeenCalled(); expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith(); expect(Cookies).toHaveBeenCalledWith();
@@ -45,7 +35,7 @@ describe('setCookie function', () => {
}); });
it('should not set a cookie if cookieName is undefined', () => { it('should not set a cookie if cookieName is undefined', () => {
setCookie(undefined, 'testValue'); setCookie(undefined, 'testValue', 'example.com');
expect(Cookies).not.toHaveBeenCalled(); expect(Cookies).not.toHaveBeenCalled();
}); });

View File

@@ -1,10 +1,9 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
export default function setCookie(cookieName, cookieValue, cookieExpiry) { export default function setCookie(cookieName, cookieValue, cookieDomain, cookieExpiry) {
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names. if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
const cookies = new Cookies(); const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' }; const options = { domain: cookieDomain, path: '/' };
if (cookieExpiry) { if (cookieExpiry) {
options.expires = cookieExpiry; options.expires = cookieExpiry;
} }

View File

@@ -40,10 +40,8 @@ export const updatePathWithQueryParams = (path) => {
return path; return path;
} }
if (queryParams.indexOf('track=pwreset') > -1) { if (queryParams.includes('track=pwreset')) {
queryParams = queryParams.replace( queryParams = queryParams.replace('?track=pwreset&', '?',).replace('?track=pwreset', '').replace('&track=pwreset', '').replace('?&', '?');
'?track=pwreset&', '?',
).replace('?track=pwreset', '').replace('&track=pwreset', '').replace('?&', '?');
} }
return `${path}${queryParams}`; return `${path}${queryParams}`;
@@ -53,7 +51,7 @@ export const getAllPossibleQueryParams = (locationURl = null) => {
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search); const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
const params = {}; const params = {};
Object.entries(urlParams).forEach(([key, value]) => { Object.entries(urlParams).forEach(([key, value]) => {
if (AUTH_PARAMS.indexOf(key) > -1) { if (AUTH_PARAMS.includes(key)) {
params[key] = value; params[key] = value;
} }
}); });

View File

@@ -10,7 +10,7 @@ import { breakpoints } from '@openedx/paragon';
const useMobileResponsive = (breakpoint) => { const useMobileResponsive = (breakpoint) => {
const [isMobileWindow, setIsMobileWindow] = useState(); const [isMobileWindow, setIsMobileWindow] = useState();
const checkForMobile = () => { const checkForMobile = () => {
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches); setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint ?? breakpoints.small.maxWidth}px)`).matches);
}; };
useEffect(() => { useEffect(() => {
checkForMobile(); checkForMobile();

View File

@@ -1,4 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { getSiteConfig } from '@openedx/frontend-base';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import FieldRenderer from '../FieldRenderer'; import FieldRenderer from '../FieldRenderer';
@@ -43,7 +43,7 @@ describe('FieldRendererTests', () => {
name: 'yob-field', name: 'yob-field',
}; };
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />); const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
expect(container.innerHTML).toEqual(''); expect(container.innerHTML).toEqual('');
}); });
@@ -84,7 +84,7 @@ describe('FieldRendererTests', () => {
it('should render checkbox field', () => { it('should render checkbox field', () => {
const fieldData = { const fieldData = {
type: 'checkbox', type: 'checkbox',
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`, label: `I agree that ${getSiteConfig().siteName} may send me marketing messages.`,
name: 'marketing-emails-opt-in-field', name: 'marketing-emails-opt-in-field',
}; };
@@ -103,7 +103,7 @@ describe('FieldRendererTests', () => {
type: 'unknown', type: 'unknown',
}; };
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />); const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
expect(container.innerHTML).toContain(''); expect(container.innerHTML).toContain('');
}); });

View File

@@ -1,14 +1,13 @@
import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, useAppConfig, useIntl } from '@openedx/frontend-base';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons'; import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages';
import { import {
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
} from '../data/constants'; } from '../data/constants';
import { PASSWORD_RESET } from '../reset-password/data/constants'; import { PASSWORD_RESET } from '../reset-password/data/constants';
import messages from './messages';
const ForgotPasswordAlert = (props) => { const ForgotPasswordAlert = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -34,7 +33,7 @@ const ForgotPasswordAlert = (props) => {
values={{ values={{
email: <span className="data-hj-suppress">{email}</span>, email: <span className="data-hj-suppress">{email}</span>,
supportLink: ( supportLink: (
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank"> <Alert.Link href={useAppConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
{formatMessage(messages['confirmation.support.link'])} {formatMessage(messages['confirmation.support.link'])}
</Alert.Link> </Alert.Link>
), ),

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import {
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; getSiteConfig, sendPageEvent, sendTrackEvent, useAppConfig, useIntl,
import { useIntl } from '@edx/frontend-platform/i18n'; } from '@openedx/frontend-base';
import { import {
Form, Form,
Hyperlink, Hyperlink,
@@ -24,11 +24,12 @@ import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils'; import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
const ForgotPasswordPage = () => { const ForgotPasswordPage = () => {
const platformName = getConfig().SITE_NAME; const platformName = getSiteConfig().siteName;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i'); const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const appConfig = useAppConfig();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [bannerEmail, setBannerEmail] = useState(''); const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState(''); const [formErrors, setFormErrors] = useState('');
@@ -110,7 +111,7 @@ const ForgotPasswordPage = () => {
<BaseContainer> <BaseContainer>
<Helmet> <Helmet>
<title>{formatMessage(messages['forgot.password.page.title'], <title>{formatMessage(messages['forgot.password.page.title'],
{ siteName: getConfig().SITE_NAME })} { siteName: getSiteConfig().siteName })}
</title> </title>
</Helmet> </Helmet>
<div> <div>
@@ -151,12 +152,12 @@ const ForgotPasswordPage = () => {
onClick={handleSubmit} onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
/> />
{(getConfig().LOGIN_ISSUE_SUPPORT_LINK) && ( {(appConfig.LOGIN_ISSUE_SUPPORT_LINK) && (
<Hyperlink <Hyperlink
id="forgot-password" id="forgot-password"
name="forgot-password" name="forgot-password"
className="ml-4 font-weight-500 text-body" className="ml-4 font-weight-500 text-body"
destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK} destination={appConfig.LOGIN_ISSUE_SUPPORT_LINK}
target="_blank" target="_blank"
showLaunchIcon={false} showLaunchIcon={false}
> >
@@ -166,7 +167,7 @@ const ForgotPasswordPage = () => {
<p className="mt-5.5 small text-gray-700"> <p className="mt-5.5 small text-gray-700">
{formatMessage(messages['additional.help.text'], { platformName })} {formatMessage(messages['additional.help.text'], { platformName })}
<span className="mx-1"> <span className="mx-1">
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink> <Hyperlink isInline destination={`mailto:${appConfig.INFO_EMAIL}`}>{appConfig.INFO_EMAIL}</Hyperlink>
</span> </span>
</p> </p>
</Form> </Form>

View File

@@ -1,21 +1,17 @@
import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded'; import formurlencoded from 'form-urlencoded';
import { forgotPassword } from './api'; import { forgotPassword } from './api';
// Mock the platform dependencies // Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({ jest.mock('@openedx/frontend-base', () => ({
getConfig: jest.fn(), getSiteConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(), getAuthenticatedHttpClient: jest.fn(),
})); }));
jest.mock('form-urlencoded', () => jest.fn()); jest.mock('form-urlencoded', () => jest.fn());
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>; const mockGetSiteConfig = getSiteConfig as jest.MockedFunction<typeof getSiteConfig>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>; jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>; const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>;
@@ -26,19 +22,19 @@ describe('forgot-password api', () => {
}; };
const mockConfig = { const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000', lmsBaseUrl: 'http://localhost:18000',
}; } as ReturnType<typeof getSiteConfig>;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig); mockGetSiteConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any); mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`); mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`);
}); });
describe('forgotPassword', () => { describe('forgotPassword', () => {
const testEmail = 'test@example.com'; const testEmail = 'test@example.com';
const expectedUrl = `${mockConfig.LMS_BASE_URL}/account/password`; const expectedUrl = `${mockConfig.lmsBaseUrl}/account/password`;
const expectedConfig = { const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true, isPublic: true,
@@ -60,7 +56,7 @@ describe('forgot-password api', () => {
expect(mockHttpClient.post).toHaveBeenCalledWith( expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl, expectedUrl,
`encoded=${JSON.stringify({ email: testEmail })}`, `encoded=${JSON.stringify({ email: testEmail })}`,
expectedConfig expectedConfig,
); );
expect(result).toEqual(mockResponse.data); expect(result).toEqual(mockResponse.data);
}); });
@@ -71,7 +67,7 @@ describe('forgot-password api', () => {
data: { data: {
message: 'Email is required', message: 'Email is required',
success: false, success: false,
} },
}; };
mockHttpClient.post.mockResolvedValueOnce(mockResponse); mockHttpClient.post.mockResolvedValueOnce(mockResponse);
@@ -96,7 +92,7 @@ describe('forgot-password api', () => {
expect(mockHttpClient.post).toHaveBeenCalledWith( expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl, expectedUrl,
expect.any(String), expect.any(String),
expectedConfig expectedConfig,
); );
}); });
@@ -110,7 +106,6 @@ describe('forgot-password api', () => {
it('should handle response with no data field', async () => { it('should handle response with no data field', async () => {
const mockResponse = { const mockResponse = {
// No data field
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
}; };

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded'; import formurlencoded from 'form-urlencoded';
const forgotPassword = async (email: string) => { const forgotPassword = async (email: string) => {
@@ -10,7 +9,7 @@ const forgotPassword = async (email: string) => {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.post( .post(
`${getConfig().LMS_BASE_URL}/account/password`, `${getSiteConfig().lmsBaseUrl}/account/password`,
formurlencoded({ email }), formurlencoded({ email }),
requestConfig, requestConfig,
) )

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging'; import { logError, logInfo } from '@openedx/frontend-base';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
@@ -8,7 +8,7 @@ import * as api from './api';
import { useForgotPassword } from './apiHook'; import { useForgotPassword } from './apiHook';
// Mock the logging functions // Mock the logging functions
jest.mock('@edx/frontend-platform/logging', () => ({ jest.mock('@openedx/frontend-base', () => ({
logError: jest.fn(), logError: jest.fn(),
logInfo: jest.fn(), logInfo: jest.fn(),
})); }));

View File

@@ -1,23 +1,23 @@
import { logError, logInfo } from '@edx/frontend-platform/logging'; import { logError, logInfo } from '@openedx/frontend-base';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { forgotPassword } from './api'; import { forgotPassword } from './api';
interface ForgotPasswordResult { interface ForgotPasswordResult {
success: boolean; success: boolean,
message?: string; message?: string,
} }
interface UseForgotPasswordOptions { interface UseForgotPasswordOptions {
onSuccess?: (data: ForgotPasswordResult, email: string) => void; onSuccess?: (data: ForgotPasswordResult, email: string) => void,
onError?: (error: Error) => void; onError?: (error: Error) => void,
} }
interface ApiError { interface ApiError extends Error {
response?: { response?: {
status: number; status: number,
data: Record<string, unknown>; data: Record<string, unknown>,
}; },
} }
const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutation({ const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutation({
@@ -31,13 +31,13 @@ const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutatio
}, },
onError: (error: ApiError) => { onError: (error: ApiError) => {
// Handle different error types like the saga did // Handle different error types like the saga did
if (error.response && error.response.status === 403) { if (error.response?.status === 403) {
logInfo(error); logInfo(error);
} else { } else {
logError(error); logError(error);
} }
if (options.onError) { if (options.onError) {
options.onError(error as Error); options.onError(error);
} }
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@edx/frontend-platform/i18n'; import { defineMessages } from '@openedx/frontend-base';
const messages = defineMessages({ const messages = defineMessages({
'forgot.password.page.title': { 'forgot.password.page.title': {

View File

@@ -1,11 +1,13 @@
import { mergeConfig } from '@edx/frontend-platform'; import {
import { configure, IntlProvider } from '@edx/frontend-platform/i18n'; CurrentAppProvider, IntlProvider, mergeAppConfig,
} from '@openedx/frontend-base';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { import {
fireEvent, render, screen, waitFor, fireEvent, render, screen, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { appId } from '../../constants';
import { import {
FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, LOGIN_PAGE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, LOGIN_PAGE,
} from '../../data/constants'; } from '../../data/constants';
@@ -16,11 +18,15 @@ import ForgotPasswordPage from '../ForgotPasswordPage';
const mockedNavigator = jest.fn(); const mockedNavigator = jest.fn();
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
sendPageEvent: jest.fn(), sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(), sendTrackEvent: jest.fn(),
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
})); }));
jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')), ...(jest.requireActual('react-router-dom')),
useNavigate: () => mockedNavigator, useNavigate: () => mockedNavigator,
@@ -31,7 +37,7 @@ jest.mock('../data/apiHook', () => ({
})); }));
describe('ForgotPasswordPage', () => { describe('ForgotPasswordPage', () => {
mergeConfig({ mergeAppConfig(appId, {
LOGIN_ISSUE_SUPPORT_LINK: '', LOGIN_ISSUE_SUPPORT_LINK: '',
INFO_EMAIL: '', INFO_EMAIL: '',
}); });
@@ -65,7 +71,9 @@ describe('ForgotPasswordPage', () => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <MemoryRouter>
{component} <CurrentAppProvider appId={appId}>
{component}
</CurrentAppProvider>
</MemoryRouter> </MemoryRouter>
</IntlProvider> </IntlProvider>
</QueryClientProvider> </QueryClientProvider>
@@ -85,21 +93,6 @@ describe('ForgotPasswordPage', () => {
}, },
}); });
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
}));
configure({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
// Clear mock calls between tests // Clear mock calls between tests
jest.clearAllMocks(); jest.clearAllMocks();
}); });
@@ -111,7 +104,7 @@ describe('ForgotPasswordPage', () => {
}); });
it('should display need other help signing in button', () => { it('should display need other help signing in button', () => {
mergeConfig({ mergeAppConfig(appId, {
LOGIN_ISSUE_SUPPORT_LINK: '/support', LOGIN_ISSUE_SUPPORT_LINK: '/support',
}); });
render(renderWrapper(<ForgotPasswordPage />)); render(renderWrapper(<ForgotPasswordPage />));
@@ -354,7 +347,9 @@ describe('ForgotPasswordAlert', () => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <MemoryRouter>
<ForgotPasswordAlert {...props} /> <CurrentAppProvider appId={appId}>
<ForgotPasswordAlert {...props} />
</CurrentAppProvider>
</MemoryRouter> </MemoryRouter>
</IntlProvider> </IntlProvider>
</QueryClientProvider>, </QueryClientProvider>,

View File

@@ -1 +1,25 @@
export default []; // Placeholder be overridden by `make pull_translations`
export default {
ar: {},
'zh-hk': {},
'zh-cn': {},
uk: {},
'tr-tr': {},
th: {},
te: {},
ru: {},
'pt-pt': {},
'pt-br': {},
'it-it': {},
id: {},
hi: {},
he: {},
'fr-ca': {},
fa: {},
'es-es': {},
'es-419': {},
el: {},
'de-de': {},
da: {},
bo: {},
};

View File

@@ -1,43 +0,0 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { StrictMode } from 'react';
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>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
});
initialize({
handlers: {
config: () => {
mergeConfig(configuration);
},
},
messages,
});

View File

@@ -1,75 +0,0 @@
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "sass/style";
//commit color - Andal Learning Brand Colors - Override Paragon variables
:root {
--pgn-color-primary: #ff4f00;
--pgn-color-primary-100: #ffe6cc;
--pgn-color-primary-200: #ffcc99;
--pgn-color-primary-300: #ffb366;
--pgn-color-primary-400: #ff9933;
--pgn-color-primary-500: #ff4f00;
--pgn-color-primary-600: #cc3f00;
--pgn-color-primary-700: #992f00;
--pgn-color-primary-800: #661f00;
--pgn-color-primary-900: #330f00;
--pgn-color-action-primary: #ff4f00;
--pgn-color-action-primary-hover: #cc3f00;
--pgn-color-action-primary-focus: #992f00;
--pgn-color-action-primary-active: #992f00;
--pgn-color-primary-base: #ff4f00;
}
// Override all button variants to use Andal orange
.btn-primary {
background-color: #ff4f00 !important;
border-color: #ff4f00 !important;
&:hover {
background-color: #cc3f00 !important;
border-color: #cc3f00 !important;
}
}
.btn-outline-primary {
color: #ff4f00 !important;
border-color: #ff4f00 !important;
&:hover {
background-color: #ff4f00 !important;
color: #fff !important;
}
}
.btn-brand {
background-color: #ff4f00 !important;
border-color: #ff4f00 !important;
color: #fff !important;
}
// Override link colors
a {
color: #ff4f00;
&:hover {
color: #cc3f00;
}
}
// Override active states
.active {
background-color: #ff4f00 !important;
color: #fff !important;
}
// Hide Register tab, only show Sign in
.nav-tabs {
.nav-link[href*="register"] {
display: none !important;
}
}
// Change "with Andal LND" text color from blue to black
.text-accent-a {
color: #000000 !important;
}

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { default as authnApp } from './app';
export { default as authnRoutes } from './routes';
export { default as authnMessages } from './i18n';

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, useAppConfig, useIntl } from '@openedx/frontend-base';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons'; import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -15,7 +14,7 @@ const AccountActivationMessage = ({ messageType }) => {
} }
const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType; const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType;
const activationOrConfirmation = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation'; const activationOrConfirmation = useAppConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation';
const iconMapping = { const iconMapping = {
[ACCOUNT_ACTIVATION_MESSAGE.SUCCESS]: CheckCircle, [ACCOUNT_ACTIVATION_MESSAGE.SUCCESS]: CheckCircle,
[ACCOUNT_ACTIVATION_MESSAGE.ERROR]: Error, [ACCOUNT_ACTIVATION_MESSAGE.ERROR]: Error,
@@ -35,7 +34,7 @@ const AccountActivationMessage = ({ messageType }) => {
} }
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: { case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
const supportLink = ( const supportLink = (
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}> <Alert.Link href={useAppConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
{formatMessage(messages['account.activation.support.link'])} {formatMessage(messages['account.activation.support.link'])}
</Alert.Link> </Alert.Link>
); );

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, ModalDialog, useToggle, ActionRow, ModalDialog, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -9,10 +8,10 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import messages from './messages';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants'; import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils'; import { updatePathWithQueryParams } from '../data/utils';
import useMobileResponsive from '../data/utils/useMobileResponsive'; import useMobileResponsive from '../data/utils/useMobileResponsive';
import messages from './messages';
const ChangePasswordPrompt = ({ variant, redirectUrl }) => { const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
const isMobileView = useMobileResponsive(); const isMobileView = useMobileResponsive();
@@ -22,11 +21,11 @@ const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
if (variant === 'block') { if (variant === 'block') {
setRedirectToResetPasswordPage(true); setRedirectToResetPasswordPage(true);
} else { } else {
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); window.location.href = redirectUrl || getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
} }
}, },
}; };
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isOpen, open, close] = useToggle(true, handlers); const [isOpen, open, close] = useToggle(true, handlers);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -1,12 +1,13 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform'; import {
import { getAuthService } from '@edx/frontend-platform/auth'; FormattedMessage, getAuthService, getSiteConfig, useIntl
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; } from '@openedx/frontend-base';
import { Alert, Hyperlink } from '@openedx/paragon'; import { Alert, Hyperlink } from '@openedx/paragon';
import { Error } from '@openedx/paragon/icons'; import { Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { windowScrollTo } from '../data/utils';
import ChangePasswordPrompt from './ChangePasswordPrompt'; import ChangePasswordPrompt from './ChangePasswordPrompt';
import { import {
ACCOUNT_LOCKED_OUT, ACCOUNT_LOCKED_OUT,
@@ -23,7 +24,6 @@ import {
TPA_AUTHENTICATION_FAILURE, TPA_AUTHENTICATION_FAILURE,
} from './data/constants'; } from './data/constants';
import messages from './messages'; import messages from './messages';
import { windowScrollTo } from '../data/utils';
const LoginFailureMessage = (props) => { const LoginFailureMessage = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -75,6 +75,7 @@ const LoginFailureMessage = (props) => {
defaultMessage="In order to sign in, you need to activate your account.{lineBreak} defaultMessage="In order to sign in, you need to activate your account.{lineBreak}
{lineBreak}We just sent an activation link to {email}. If you do not receive an email, {lineBreak}We just sent an activation link to {email}. If you do not receive an email,
check your spam folders or {supportLink}." check your spam folders or {supportLink}."
description="An error message shown to users when they sign in if they have not yet activated their account. It attempts to explain to them how to activate their account by looking for an email from the system."
values={{ values={{
lineBreak: <br />, lineBreak: <br />,
email: <strong className="data-hj-suppress">{context.email}</strong>, email: <strong className="data-hj-suppress">{context.email}</strong>,
@@ -86,7 +87,7 @@ const LoginFailureMessage = (props) => {
break; break;
} }
case ALLOWED_DOMAIN_LOGIN_ERROR: { case ALLOWED_DOMAIN_LOGIN_ERROR: {
const url = `${getConfig().LMS_BASE_URL}/dashboard/?tpa_hint=${context.tpaHint}`; const url = `${getSiteConfig().lmsBaseUrl}/dashboard/?tpa_hint=${context.tpaHint}`;
const tpaLink = ( const tpaLink = (
<a href={url}> <a href={url}>
{formatMessage(messages['tpa.account.link'], { provider: context.provider })} {formatMessage(messages['tpa.account.link'], { provider: context.provider })}
@@ -161,6 +162,7 @@ const LoginFailureMessage = (props) => {
<FormattedMessage <FormattedMessage
id="login.incorrect.credentials.error.with.reset.link" id="login.incorrect.credentials.error.with.reset.link"
defaultMessage="The username, email, or password you entered is incorrect. Please try again or {resetLink}." defaultMessage="The username, email, or password you entered is incorrect. Please try again or {resetLink}."
description="An error message shown to users if some part of their login information was incorrect."
values={{ resetLink }} values={{ resetLink }}
/> />
</p> </p>
@@ -184,7 +186,7 @@ const LoginFailureMessage = (props) => {
errorMessage = ( errorMessage = (
<p> <p>
{formatMessage(messages['login.tpa.authentication.failure'], { {formatMessage(messages['login.tpa.authentication.failure'], {
platform_name: getConfig().SITE_NAME, platform_name: getSiteConfig().siteName,
lineBreak: <br />, lineBreak: <br />,
errorMessage: context.errorMessage, errorMessage: context.errorMessage,
})} })}
@@ -200,7 +202,7 @@ const LoginFailureMessage = (props) => {
return ( return (
<Alert id="login-failure-alert" className="mb-5" variant="danger" icon={Error}> <Alert id="login-failure-alert" className="mb-5" variant="danger" icon={Error}>
<Alert.Heading>{formatMessage(messages['login.failure.header.title'])}</Alert.Heading> <Alert.Heading>{formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
{ errorMessage } {errorMessage}
</Alert> </Alert>
); );
}; };

View File

@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import {
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; getSiteConfig, sendPageEvent, sendTrackEvent, useIntl
import { useIntl } from '@edx/frontend-platform/i18n'; } from '@openedx/frontend-base';
import { Form, StatefulButton } from '@openedx/paragon'; import { Form, StatefulButton } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
@@ -100,11 +100,11 @@ const LoginPage = ({
useEffect(() => { useEffect(() => {
sendPageEvent('login_and_registration', 'login'); sendPageEvent('login_and_registration', 'login');
}, []);
// Fetch third-party auth context data
useEffect(() => {
setThirdPartyAuthContextBegin(); setThirdPartyAuthContextBegin();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Sync third-party auth context data
useEffect(() => {
if (isSuccess && data) { if (isSuccess && data) {
setThirdPartyAuthContextSuccess( setThirdPartyAuthContextSuccess(
data.fieldDescriptions, data.fieldDescriptions,
@@ -116,7 +116,7 @@ const LoginPage = ({
setThirdPartyAuthContextFailure(); setThirdPartyAuthContextFailure();
} }
}, [tpaHint, queryParams, isSuccess, data, error, }, [tpaHint, queryParams, isSuccess, data, error,
setThirdPartyAuthContextBegin, setThirdPartyAuthContextSuccess, setThirdPartyAuthContextFailure]); setThirdPartyAuthContextSuccess, setThirdPartyAuthContextFailure]);
useEffect(() => { useEffect(() => {
if (thirdPartyErrorMessage) { if (thirdPartyErrorMessage) {
@@ -210,7 +210,7 @@ const LoginPage = ({
} }
if (skipHintedLogin) { if (skipHintedLogin) {
window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl; window.location.href = getSiteConfig().lmsBaseUrl + provider.loginUrl;
return null; return null;
} }
@@ -227,11 +227,10 @@ const LoginPage = ({
/> />
); );
} }
return ( return (
<> <>
<Helmet> <Helmet>
<title>{formatMessage(messages['login.page.title'], { siteName: getConfig().SITE_NAME })}</title> <title>{formatMessage(messages['login.page.title'], { siteName: getSiteConfig().siteName })}</title>
</Helmet> </Helmet>
<RedirectLogistration <RedirectLogistration
success={loginResult.success} success={loginResult.success}

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { LoginProvider, useLoginContext } from './LoginContext'; import { LoginProvider, useLoginContext } from './LoginContext';
const TestComponent = () => { const TestComponent = () => {
@@ -29,7 +28,7 @@ describe('LoginContext', () => {
</LoginProvider>, </LoginProvider>,
); );
expect(screen.getByText('Test Child')).toBeInTheDocument(); expect(screen.getByText('Test Child')).toBeTruthy();
}); });
it('should provide all context values to children', () => { it('should provide all context values to children', () => {
@@ -39,12 +38,12 @@ describe('LoginContext', () => {
</LoginProvider>, </LoginProvider>,
); );
expect(screen.getByText('FormFields Available')).toBeInTheDocument(); expect(screen.getByText('FormFields Available')).toBeTruthy();
expect(screen.getByText('EmailOrUsername Field Available')).toBeInTheDocument(); expect(screen.getByText('EmailOrUsername Field Available')).toBeTruthy();
expect(screen.getByText('Password Field Available')).toBeInTheDocument(); expect(screen.getByText('Password Field Available')).toBeTruthy();
expect(screen.getByText('Errors Available')).toBeInTheDocument(); expect(screen.getByText('Errors Available')).toBeTruthy();
expect(screen.getByText('EmailOrUsername Error Available')).toBeInTheDocument(); expect(screen.getByText('EmailOrUsername Error Available')).toBeTruthy();
expect(screen.getByText('Password Error Available')).toBeInTheDocument(); expect(screen.getByText('Password Error Available')).toBeTruthy();
}); });
it('should render multiple children', () => { it('should render multiple children', () => {
@@ -56,8 +55,8 @@ describe('LoginContext', () => {
</LoginProvider>, </LoginProvider>,
); );
expect(screen.getByText('First Child')).toBeInTheDocument(); expect(screen.getByText('First Child')).toBeTruthy();
expect(screen.getByText('Second Child')).toBeInTheDocument(); expect(screen.getByText('Second Child')).toBeTruthy();
expect(screen.getByText('Third Child')).toBeInTheDocument(); expect(screen.getByText('Third Child')).toBeTruthy();
}); });
}); });

View File

@@ -1,28 +1,28 @@
import { import {
createContext, FC, ReactNode, useContext, useMemo, useState, createContext, Dispatch, FC, ReactNode, SetStateAction, useContext, useMemo, useState,
} from 'react'; } from 'react';
export interface FormFields { export interface FormFields {
emailOrUsername: string; emailOrUsername: string,
password: string; password: string,
} }
export interface FormErrors { export interface FormErrors {
emailOrUsername: string; emailOrUsername: string,
password: string; password: string,
} }
interface LoginContextType { interface LoginContextType {
formFields: FormFields; formFields: FormFields,
setFormFields: (fields: FormFields) => void; setFormFields: Dispatch<SetStateAction<FormFields>>,
errors: FormErrors; errors: FormErrors,
setErrors: (errors: FormErrors) => void; setErrors: Dispatch<SetStateAction<FormErrors>>,
} }
const LoginContext = createContext<LoginContextType | undefined>(undefined); const LoginContext = createContext<LoginContextType | undefined>(undefined);
interface LoginProviderProps { interface LoginProviderProps {
children: ReactNode; children: ReactNode,
} }
export const LoginProvider: FC<LoginProviderProps> = ({ children }) => { export const LoginProvider: FC<LoginProviderProps> = ({ children }) => {

View File

@@ -1,27 +1,25 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { camelCaseObject, getAuthenticatedHttpClient, getSiteConfig, getUrlByRouteRole } from '@openedx/frontend-base';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as QueryString from 'query-string'; import * as QueryString from 'query-string';
import { login } from './api'; import { login } from './api';
// Mock the platform dependencies // Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({ jest.mock('@openedx/frontend-base', () => ({
getConfig: jest.fn(), getSiteConfig: jest.fn(),
camelCaseObject: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(), getAuthenticatedHttpClient: jest.fn(),
getUrlByRouteRole: jest.fn(),
camelCaseObject: jest.fn(),
})); }));
jest.mock('query-string', () => ({ jest.mock('query-string', () => ({
stringify: jest.fn(), stringify: jest.fn(),
})); }));
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>; const mockGetSiteConfig = getSiteConfig as jest.MockedFunction<typeof getSiteConfig>;
const mockCamelCaseObject = camelCaseObject as jest.MockedFunction<typeof camelCaseObject>; const mockCamelCaseObject = camelCaseObject as jest.MockedFunction<typeof camelCaseObject>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>; jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockGetUrlByRouteRole = getUrlByRouteRole as jest.MockedFunction<typeof getUrlByRouteRole>;
const mockQueryStringify = QueryString.stringify as jest.MockedFunction<typeof QueryString.stringify>; const mockQueryStringify = QueryString.stringify as jest.MockedFunction<typeof QueryString.stringify>;
describe('login api', () => { describe('login api', () => {
@@ -30,13 +28,14 @@ describe('login api', () => {
}; };
const mockConfig = { const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000', lmsBaseUrl: 'http://localhost:18000',
}; } as ReturnType<typeof getSiteConfig>;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig); mockGetSiteConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any); mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockGetUrlByRouteRole.mockReturnValue('/dashboard');
mockCamelCaseObject.mockImplementation((obj) => obj); mockCamelCaseObject.mockImplementation((obj) => obj);
mockQueryStringify.mockImplementation((obj) => `stringified=${JSON.stringify(obj)}`); mockQueryStringify.mockImplementation((obj) => `stringified=${JSON.stringify(obj)}`);
}); });
@@ -46,7 +45,7 @@ describe('login api', () => {
email_or_username: 'testuser@example.com', email_or_username: 'testuser@example.com',
password: 'password123', password: 'password123',
}; };
const expectedUrl = `${mockConfig.LMS_BASE_URL}/api/user/v2/account/login_session/`; const expectedUrl = `${mockConfig.lmsBaseUrl}/api/user/v2/account/login_session/`;
const expectedConfig = { const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true, isPublic: true,
@@ -151,7 +150,7 @@ describe('login api', () => {
expect(mockHttpClient.post).toHaveBeenCalledWith( expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl, expectedUrl,
`stringified=${JSON.stringify(mockCredentials)}`, `stringified=${JSON.stringify(mockCredentials)}`,
expectedConfig expectedConfig,
); );
}); });

View File

@@ -1,5 +1,4 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { camelCaseObject, getAuthenticatedHttpClient, getSiteConfig, getUrlByRouteRole } from '@openedx/frontend-base';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as QueryString from 'query-string'; import * as QueryString from 'query-string';
const login = async (creds) => { const login = async (creds) => {
@@ -7,11 +6,12 @@ const login = async (creds) => {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true, isPublic: true,
}; };
const url = `${getConfig().LMS_BASE_URL}/api/user/v2/account/login_session/`; const url = `${getSiteConfig().lmsBaseUrl}/api/user/v2/account/login_session/`;
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.post(url, QueryString.stringify(creds), requestConfig); .post(url, QueryString.stringify(creds), requestConfig);
const defaultRedirectUrl = getUrlByRouteRole('org.openedx.frontend.role.dashboard');
return camelCaseObject({ return camelCaseObject({
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`, redirectUrl: data.redirect_url || defaultRedirectUrl,
success: data.success || false, success: data.success || false,
}); });
}; };

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging'; import { camelCaseObject, logError, logInfo } from '@openedx/frontend-base';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
@@ -12,12 +11,9 @@ import {
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants'; import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
// Mock the dependencies // Mock the dependencies
jest.mock('@edx/frontend-platform/logging', () => ({ jest.mock('@openedx/frontend-base', () => ({
logError: jest.fn(), logError: jest.fn(),
logInfo: jest.fn(), logInfo: jest.fn(),
}));
jest.mock('@edx/frontend-platform/utils', () => ({
camelCaseObject: jest.fn(), camelCaseObject: jest.fn(),
})); }));

View File

@@ -1,5 +1,4 @@
import { logError, logInfo } from '@edx/frontend-platform/logging'; import { camelCaseObject, logError, logInfo } from '@openedx/frontend-base';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { login } from './api'; import { login } from './api';
@@ -7,17 +6,17 @@ import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
// Type definitions // Type definitions
interface LoginData { interface LoginData {
email_or_username: string; email_or_username: string,
password: string; password: string,
} }
interface LoginResponse { interface LoginResponse {
redirectUrl?: string; redirectUrl?: string,
} }
interface UseLoginOptions { interface UseLoginOptions {
onSuccess?: (data: LoginResponse) => void; onSuccess?: (data: LoginResponse) => void,
onError?: (error: unknown) => void; onError?: (error: unknown) => void,
} }
const useLogin = (options: UseLoginOptions = {}) => useMutation<LoginResponse, unknown, LoginData>({ const useLogin = (options: UseLoginOptions = {}) => useMutation<LoginResponse, unknown, LoginData>({
@@ -36,10 +35,10 @@ const useLogin = (options: UseLoginOptions = {}) => useMutation<LoginResponse, u
count: 0, count: 0,
}; };
if (error && typeof error === 'object' && 'response' in error && error.response) { if (error && typeof error === 'object' && 'response' in error && error.response) {
const response = error.response as { status?: number; data?: unknown }; const response = error.response as { status?: number, data?: unknown };
const { status, data } = camelCaseObject(response); const { status, data } = camelCaseObject(response);
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
const errorData = data as { errorCode?: string; context?: { failureCount?: number } }; const errorData = data as { errorCode?: string, context?: { failureCount?: number } };
formattedError = { formattedError = {
type: errorData.errorCode || FORBIDDEN_REQUEST, type: errorData.errorCode || FORBIDDEN_REQUEST,
context: errorData.context || {}, context: errorData.context || {},

Some files were not shown because too many files have changed in this diff Show More