Compare commits

...

373 Commits

Author SHA1 Message Date
1f0eb5258a Merge pull request 'andal-lnd/front-end-updated' (#1) from andal-lnd/front-end-updated into master
Reviewed-on: #1
Reviewed-by: Damar <damar@noreply.git.andalsoftware.com>
Reviewed-by: tauficls <tauficls@noreply.git.andalsoftware.com>
2026-04-01 08:39:34 +07:00
Neo
31a10333e9 commit color 2026-03-31 14:30:37 +07:00
Neo
f1fb37b7d7 commit color 2026-03-31 14:29:42 +07:00
Neo
22499360ae feat: updates for andal-lnd login-ui-ux 2026-03-30 14:51:31 +07:00
Neo
80d11b5d3f Andal Learning branding: Apply orange color #ff4f00 and hide register tab
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 14:46:11 +07:00
renovate[bot]
604a785007 chore(deps): update dependency jest to v30.3.0 (#1642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-10 05:30:20 +00:00
renovate[bot]
0d2e41244a chore(deps): update dependency @tanstack/react-query to v5.90.21 (#1639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 20:19:49 +00:00
Adolfo R. Brandes
93bd0f24fe fix: prioritize registration errors over inline validations in backendValidations
Registration errors from form submission (e.g., "password too similar to
username") were being masked by stale inline validation results. The
backendValidations memo checked state.validations first, which was set
during on-blur field validation with no errors, causing it to never reach
the registrationError branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:14:50 -03:00
Jesus Balderrama
0d709d1565 feat: React Query migration (#1629)
Move from Redux to React Query across the board.
2026-03-05 13:46:05 -03:00
Max Sokolski
642853e001 fix: adding value length check for full name field (#1561)
Co-authored-by: Artur Filippovskii <118079918+filippovskii09@users.noreply.github.com>
2026-03-05 10:18:51 -03:00
bydawen
4cb79223b2 fix: add missing space symbol to the 'additional.help.text' field (#1521) 2026-02-27 10:35:43 -03:00
renovate[bot]
bbccd79785 chore(deps): update dependency algoliasearch to v4.27.0 (#1638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 10:15:58 +00:00
renovate[bot]
359c349d50 chore(deps): update dependency algoliasearch-helper to v3.28.0 (#1637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:54:18 +00:00
renovate[bot]
3942378177 chore(deps): update dependency algoliasearch to v4.26.0 (#1635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 17:25:22 +00:00
renovate[bot]
4be8a2a452 chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#1634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 01:15:59 +00:00
renovate[bot]
f3a353245f chore(deps): update dependency algoliasearch-helper to v3.27.1 (#1630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 12:52:09 +00:00
Anton Melser
063bf759d4 docs: include 'standard' dev instructions 2026-01-29 06:35:35 -03:00
renovate[bot]
b54ad18da2 chore(deps): update dependency @edx/browserslist-config to v1.5.1 (#1626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 17:45:16 +00:00
renovate[bot]
3ef7bb2dc7 chore(deps): update dependency @openedx/frontend-plugin-framework to v1.8.0 (#1625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 14:45:44 +00:00
renovate[bot]
dd3ba31529 chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#1623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 22:09:21 +00:00
renovate[bot]
9b7be2aade chore(deps): update dependency @openedx/paragon to v23.19.1 (#1618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 10:00:43 +00:00
renovate[bot]
c3a14286d0 chore(deps): update dependency @openedx/paragon to v23.19.0 (#1617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 02:01:22 +00:00
renovate[bot]
6f2e519b3c fix(deps): update react-router monorepo to v6.30.3 (#1616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-07 21:39:01 +00:00
renovate[bot]
f6a06d7f86 chore(deps): update dependency algoliasearch-helper to v3.27.0 (#1615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 13:51:30 +00:00
renovate[bot]
b7decc3c73 chore(deps): update dependency @openedx/paragon to v23.18.2 (#1614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 17:08:46 +00:00
renovate[bot]
0e2e802f9d chore(deps): update dependency ts-jest to v29.4.6 (#1610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 19:17:29 +00:00
renovate[bot]
882545be12 chore(deps): update dependency @openedx/paragon to v23.18.1 (#1608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-25 18:00:06 +00:00
renovate[bot]
34a334fabc chore(deps): update dependency @openedx/paragon to v23.18.0 (#1603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 12:06:07 +00:00
renovate[bot]
1fd6e94792 chore(deps): update dependency @openedx/paragon to v23.17.0 (#1602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 02:42:26 +00:00
renovate[bot]
e9b0902f49 fix(deps): update react-router monorepo to v6.30.2 (#1601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 23:55:54 +00:00
renovate[bot]
a6ab635ea6 chore(deps): update dependency @openedx/paragon to v23.16.1 (#1600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 21:48:30 +00:00
renovate[bot]
a22e0c80ca chore(deps): update dependency algoliasearch-helper to v3.26.1 (#1599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 18:48:58 +00:00
renovate[bot]
cff8da5a5e chore(deps): update dependency algoliasearch to v4.25.3 (#1598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 17:56:08 +00:00
renovate[bot]
70f11247d8 chore(deps): update dependency @openedx/paragon to v23.16.0 (#1597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 19:12:38 +00:00
renovate[bot]
362a2962af chore(deps): update dependency @openedx/paragon to v23.15.2 (#1596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 05:21:11 +00:00
renovate[bot]
80760103a2 chore(deps): update dependency @openedx/paragon to v23.15.1 (#1594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 03:04:18 +00:00
Feanil Patel
bee0afd611 Merge pull request #1354 from open-craft/kshitij/plugin-slot
feat: Add plugin slot for login page
2025-10-24 09:53:15 -04:00
kshitij.sobti
0418a04fff feat: Add plugin slot for login page
This change adds a plugin slot for the login page allowing it to be customised.

Since there was a dependency conflict between frontend-plugin-framework and the react-hooks testing package, the react-hooks testing package has been removed and a replaced with a simple mechanism for testing hooks.

Since this touched the Login Page those have also been refactored to move away from redux connect.
2025-10-24 15:57:09 +05:30
renovate[bot]
5061391122 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#1593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 04:10:39 +00:00
renovate[bot]
deb7cef005 chore(deps): update dependency @openedx/paragon to v23.15.0 (#1592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 18:27:36 +00:00
renovate[bot]
9bd9d31599 fix(deps): update dependency redux-saga to v1.4.2 (#1591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 22:53:05 +00:00
renovate[bot]
aec68e7c18 fix(deps): update dependency redux-saga to v1.4.1 (#1590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 08:43:12 +00:00
renovate[bot]
f8868b1e36 chore(deps): update dependency @openedx/paragon to v23.14.9 (#1586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 18:41:27 +00:00
Ihor Romaniuk
ffb8a2d434 fix: username suggestions alignment (#1584)
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2025-10-15 01:51:39 +05:00
renovate[bot]
a615cba2fa chore(deps): update dependency @openedx/paragon to v23.14.8 (#1583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:11:29 +00:00
renovate[bot]
c09d7f4eec chore(deps): update dependency ts-jest to v29.4.5 (#1582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 18:04:03 +00:00
renovate[bot]
a67a08a5fb chore(deps): update dependency @openedx/paragon to v23.14.6 (#1581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 13:10:59 +00:00
renovate[bot]
43ef53b703 chore(deps): update dependency babel-plugin-formatjs to v10.5.41 (#1580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 18:05:55 +00:00
renovate[bot]
1dc39fcce1 chore(deps): update dependency @openedx/paragon to v23.14.5 (#1579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 20:51:02 +00:00
renovate[bot]
0a28ef2fb4 chore(deps): update dependency babel-plugin-formatjs to v10.5.40 (#1578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 01:36:40 +00:00
renovate[bot]
c2fa1fa2df chore(deps): update dependency @openedx/paragon to v23.14.4 (#1577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 21:33:53 +00:00
renovate[bot]
44cf541b06 chore(deps): update dependency jest to v30.2.0 (#1576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 01:01:52 +00:00
Feanil Patel
b5b12d0e87 Merge pull request #1574 from openedx/feanil/remove-reactifex-packages
build: remove unused @edx/reactifex package
2025-09-26 16:14:56 -04:00
Feanil Patel
b2862eeb42 build: remove unused @edx/reactifex package
Remove @edx/reactifex package from devDependencies as it is no longer
needed. Translation extraction functionality has been verified to work
correctly without this dependency.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 16:11:41 -04:00
renovate[bot]
92a333cc66 chore(deps): update dependency @openedx/paragon to v23.14.3 (#1575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 16:10:20 -04:00
oleksandr.buhaienko
7a9d9bb300 test: Remove support for Node 20 2025-09-26 10:50:26 -03:00
oleksandr.buhaienko
fc4eb61ec9 build: Upgrade to Node 24 2025-09-26 09:17:49 -03:00
renovate[bot]
b2972929c9 chore(deps): update dependency ts-jest to v29.4.4 (#1573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 18:00:14 +00:00
bydawen
bc251a61b2 test: Add Node 24 to CI matrix (#1564) 2025-09-19 13:56:48 -04:00
renovate[bot]
632e962161 chore(deps): update dependency ts-jest to v29.4.3 (#1572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 10:35:42 +00:00
renovate[bot]
5d913b720e chore(deps): update dependency ts-jest to v29.4.2 (#1570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 10:17:33 +00:00
renovate[bot]
f8a5cb50ed fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#1568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 02:15:50 +00:00
Samuel Allan
b97f777b6f fix: update frontend-build to fix install issues (#1553)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

```
npm install --package-lock-only @openedx/frontend-build
```

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-08 13:39:50 -06:00
renovate[bot]
b2f7579054 fix(deps): update dependency form-urlencoded to v6.1.6 (#1560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 08:36:57 +00:00
renovate[bot]
24742c1cf5 chore(deps): update dependency jest to v30.1.3 (#1557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 16:30:15 +00:00
renovate[bot]
051383e68a chore(deps): update dependency jest to v30.1.2 (#1555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:23:58 +00:00
renovate[bot]
5443ebd01b chore(deps): update dependency @openedx/frontend-build to v14.6.2 (#1554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 07:55:50 +00:00
renovate[bot]
3aa2422735 chore(deps): update dependency jest to v30.1.1 (#1552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 20:30:04 +00:00
renovate[bot]
90a7dfeb15 chore(deps): update dependency jest to v30.1.0 (#1551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 05:27:12 +00:00
renovate[bot]
97c7bd744f fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#1549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 02:27:19 +00:00
renovate[bot]
55c5f705fb fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.5 (#1548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 03:30:08 +00:00
renovate[bot]
f4e2adc261 fix(deps): update dependency @openedx/paragon to v23.14.2 (#1546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:49:41 +00:00
Hassan Raza
58ec90aca6 chore: Handle forbidden username exceptions on registration (#1545) 2025-08-13 07:35:30 +00:00
Diana Villalvazo
76e400f0ad refactor: Replace of injectIntl with useIntl (#1540) 2025-08-12 11:00:33 -04:00
Diana Villalvazo
5bd6926f2f refactor: Replace of injectIntl with useIntl (#1539) 2025-08-12 10:46:01 -04:00
Diana Villalvazo
43a584ebd1 refactor: Replace of injectIntl with useIntl (#1538) 2025-08-12 09:42:57 -04:00
sundasnoreen12
4cf0a64d81 Merge pull request #1541 from WGU-Open-edX/1526/injectIntl-4of4
Replace of injectIntl with useIntl() 4/4
2025-08-11 12:31:50 +05:00
diana-villalvazo-wgu
db3d007c51 refactor: Replace of injectIntl with useIntl 2025-08-07 10:50:11 -05:00
renovate[bot]
55a930840f fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#1543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 21:25:05 +00:00
renovate[bot]
fad82b52ad fix(deps): update dependency @openedx/paragon to v23.14.1 (#1537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:50:25 +00:00
renovate[bot]
41450686aa chore(deps): update dependency ts-jest to v29.4.1 (#1536)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 02:11:37 +00:00
Kyle McCormick
2fcda640f5 chore: Delete CODEOWNERS (#1535)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:18:27 -04:00
renovate[bot]
82252f9a7c fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#1531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-24 15:03:04 +00:00
renovate[bot]
818d0278a5 chore(deps): update dependency jest to v30.0.5 (#1527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 08:44:21 +00:00
renovate[bot]
ff3fce99db fix(deps): update dependency @openedx/paragon to v23.14.0 (#1517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 19:01:47 +00:00
renovate[bot]
157c302384 chore(deps): update dependency jest to v30.0.4 (#1516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 00:16:29 +00:00
renovate[bot]
f2a905d373 fix(deps): update dependency @openedx/paragon to v23.13.0 (#1514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 23:59:51 +00:00
renovate[bot]
e984a0b07b chore(deps): update dependency jest to v30.0.3 (#1513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 03:34:38 +00:00
renovate[bot]
7150d4562a fix(deps): update dependency algoliasearch to v4.25.2 (#1510)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 17:06:34 +00:00
renovate[bot]
451056866f chore(deps): update dependency eslint-plugin-import to v2.32.0 (#1509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 23:30:16 +00:00
renovate[bot]
76f0cc54d9 fix(deps): update dependency algoliasearch to v4.25.0 (#1508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 19:51:42 +00:00
renovate[bot]
fb70f7a1c2 chore(deps): update dependency jest to v30.0.2 (#1507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 15:46:32 +00:00
renovate[bot]
b664150b4d chore(deps): update dependency jest to v30.0.1 (#1506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 22:50:33 +00:00
Brian Smith
da5a2e31b6 feat!: add design tokens support (#1504)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 14:29:11 -04:00
renovate[bot]
486d0bfd37 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 03:45:28 +00:00
renovate[bot]
9332fc113a fix(deps): update dependency algoliasearch-helper to v3.26.0 (#1501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 17:05:31 +00:00
renovate[bot]
181e837ca4 fix(deps): update dependency @openedx/paragon to v22.20.2 (#1500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 06:13:56 +00:00
renovate[bot]
735a9afc3c chore(deps): update dependency babel-plugin-formatjs to v10.5.39 (#1499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 00:51:57 +00:00
renovate[bot]
319c48f1c8 fix(deps): update dependency @openedx/paragon to v22.20.1 (#1496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 17:38:03 +00:00
renovate[bot]
fbd73bfbfe chore(deps): update dependency jest to v30 (#1495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 05:28:53 +00:00
renovate[bot]
27a63cf406 fix(deps): update dependency @edx/frontend-platform to v8.4.0 (#1494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 16:33:27 +00:00
renovate[bot]
7ea351f6a0 fix(deps): update dependency core-js to v3.43.0 (#1493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 07:34:11 +00:00
renovate[bot]
61e8c254d7 fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:27:33 +00:00
renovate[bot]
3a08e790c3 fix(deps): update dependency @openedx/paragon to v22.20.0 (#1491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 18:02:08 +00:00
renovate[bot]
b4c5171886 fix(deps): update dependency @openedx/paragon to v22.18.2 (#1490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 20:32:20 +00:00
renovate[bot]
7b83c416f8 fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#1489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 21:59:07 +00:00
renovate[bot]
a3c261bb13 fix(deps): update dependency @openedx/paragon to v22.18.1 (#1488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 18:12:22 +00:00
renovate[bot]
98d03aa29f fix(deps): update dependency @openedx/paragon to v22.18.0 (#1487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 22:11:58 +00:00
renovate[bot]
f5d5e2fd02 fix(deps): update react-router monorepo to v6.30.1 (#1486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 20:32:18 +00:00
renovate[bot]
490bf27ed1 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:30:11 +00:00
renovate[bot]
780acac2fd fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:17:03 +00:00
renovate[bot]
2ea763701d fix(deps): update dependency @edx/frontend-platform to v8.3.5 (#1481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 11:51:58 +00:00
renovate[bot]
e2d9ba5857 chore(deps): update dependency babel-plugin-formatjs to v10.5.38 (#1480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:15:18 +00:00
renovate[bot]
747d656f0a fix(deps): update dependency core-js to v3.42.0 (#1479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 19:33:32 +00:00
renovate[bot]
8638ed5cf4 fix(deps): update dependency algoliasearch-helper to v3.25.0 (#1478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 15:06:11 +00:00
renovate[bot]
ca2e7f554a chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 23:54:00 +00:00
Adolfo R. Brandes
0e94124d74 Merge pull request #1470 from regisb/regisb/no-husky
chore: remove husky 🪓🐶
2025-04-14 14:12:48 -03:00
Régis Behmo
af7edd8a3f chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-14 18:53:56 +02:00
renovate[bot]
9323f119c8 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#1474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 18:08:35 +00:00
Brian Smith
38a1924c6a feat: upgrade to react 18 (#1466) 2025-04-04 10:17:53 -04:00
renovate[bot]
2d7303009f fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 18:46:51 +00:00
Brian Smith
dfcb94a831 fix: properly set background color for floating labels (#1468) 2025-04-01 12:51:38 -04:00
renovate[bot]
520dd6ed6b chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 14:49:39 +00:00
renovate[bot]
1b32dbfa19 fix(deps): update dependency @openedx/paragon to v22.16.2 (#1462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 12:28:50 +00:00
renovate[bot]
f7d9bdb5b5 chore(deps): update dependency babel-plugin-formatjs to v10.5.37 (#1461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 06:50:31 +00:00
sarina
063ec80cde docs: Add instructions on using Tutor for development 2025-03-25 14:40:55 -03:00
sarina
3cbb134c3a docs: Update migrated edx.rtd links to docs.openedx.org 2025-03-25 14:40:55 -03:00
Brian Smith
059a60d1c8 chore(deps): update @openedx dependencies to versions that support React 18 (#1458) 2025-03-25 12:22:43 -04:00
renovate[bot]
c88d701271 fix(deps): update dependency @openedx/paragon to v22.16.1 (#1460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 22:21:52 +00:00
renovate[bot]
33b98b356b chore(deps): update dependency @openedx/frontend-build to v14.3.3 (#1459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 20:18:14 +00:00
renovate[bot]
1f81699af4 fix(deps): update dependency algoliasearch-helper to v3.24.3 (#1457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 13:55:06 +00:00
renovate[bot]
13aa77fc70 fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:13:09 +00:00
renovate[bot]
66531831b7 chore(deps): update dependency babel-plugin-formatjs to v10.5.36 (#1455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 03:42:16 +00:00
renovate[bot]
846d3f0662 fix(deps): update dependency @edx/frontend-platform to v8.3.2 (#1454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 19:54:09 +00:00
renovate[bot]
f78e84ee0a fix(deps): update dependency @edx/frontend-platform to v8.3.1 (#1449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 23:16:01 +00:00
renovate[bot]
2d27da2391 fix(deps): update dependency @openedx/paragon to v22.16.0 (#1446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 18:03:14 +00:00
renovate[bot]
78413be34a chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 01:30:17 +00:00
renovate[bot]
88866a39c1 fix(deps): update dependency algoliasearch-helper to v3.24.2 (#1442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 16:02:35 +00:00
renovate[bot]
dc9699c033 fix(deps): update dependency @edx/frontend-platform to v8.3.0 (#1440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 02:44:20 +00:00
renovate[bot]
00a0e27062 fix(deps): update dependency core-js to v3.41.0 (#1439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 03:32:41 +00:00
renovate[bot]
6839afcf3c fix(deps): update react-router monorepo to v6.30.0 (#1435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 18:41:40 +00:00
renovate[bot]
1cd9c58c1a fix(deps): update dependency @openedx/paragon to v22.15.3 (#1432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 22:14:39 +00:00
renovate[bot]
5d481a93c7 fix(deps): update dependency @edx/frontend-platform to v8.2.1 (#1429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 22:56:35 +00:00
renovate[bot]
438d1fcfa7 fix(deps): update dependency @edx/frontend-platform to v8.2.0 (#1428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 03:16:30 +00:00
renovate[bot]
bc912ce139 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 23:02:47 +00:00
renovate[bot]
ab1c2d5379 fix(deps): update dependency @openedx/paragon to v22.15.2 (#1426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 15:27:23 +00:00
renovate[bot]
c109f6e771 chore(deps): update dependency babel-plugin-formatjs to v10.5.35 (#1421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 01:03:12 +00:00
renovate[bot]
8976647190 fix(deps): update dependency @openedx/paragon to v22.15.1 (#1420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 21:51:26 +00:00
renovate[bot]
cb051a83ad fix(deps): update dependency @openedx/paragon to v22.15.0 (#1419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 17:40:38 +00:00
renovate[bot]
1b8aec5709 fix(deps): update react-router monorepo to v6.29.0 (#1417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 17:54:09 +00:00
renovate[bot]
9a68e95fcc fix(deps): update dependency algoliasearch-helper to v3.24.1 (#1414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 12:31:35 +00:00
renovate[bot]
c90980afb0 fix(deps): update dependency @openedx/paragon to v22.14.0 (#1413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:06:09 +00:00
renovate[bot]
abb8ae5085 fix(deps): update dependency algoliasearch-helper to v3.23.1 (#1412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 18:52:31 +00:00
renovate[bot]
8bb7462098 chore(deps): update dependency babel-plugin-formatjs to v10.5.34 (#1411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 17:48:45 +00:00
renovate[bot]
b4a5397ba1 chore(deps): update dependency babel-plugin-formatjs to v10.5.33 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:32:02 +00:00
Feanil Patel
a43c620dc4 Merge pull request #1406 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-01-17 10:42:49 -05:00
salman2013
93d11b8485 chore: Update catalog-info file for release data and remove openedx.yaml 2025-01-17 10:39:31 -05:00
renovate[bot]
68e13d4daf chore(deps): update dependency babel-plugin-formatjs to v10.5.31 (#1409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 09:05:56 +00:00
renovate[bot]
f6617935e3 fix(deps): update react-router monorepo to v6.28.2 (#1408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 18:07:00 +00:00
renovate[bot]
5f4591c046 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 16:22:56 +00:00
renovate[bot]
ae52a8cb65 fix(deps): update dependency algoliasearch-helper to v3.23.0 (#1405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 18:38:26 +00:00
renovate[bot]
b8df66ad23 fix(deps): update dependency core-js to v3.40.0 (#1404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 20:46:54 +00:00
renovate[bot]
923776ab96 fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 12:10:02 +00:00
renovate[bot]
f170f5e3f0 chore(deps): update dependency babel-plugin-formatjs to v10.5.30 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 18:14:00 +00:00
renovate[bot]
730875ceb2 fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 09:15:59 +00:00
renovate[bot]
812350d24a fix(deps): update react-router monorepo to v6.28.1 (#1399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 21:25:00 +00:00
renovate[bot]
6879bacb89 fix(deps): update dependency @openedx/paragon to v22.13.0 (#1398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 17:20:36 +00:00
renovate[bot]
9b2b0f2019 fix(deps): update font awesome to v6.7.2 (#1397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 22:17:55 +00:00
renovate[bot]
87884f2d91 fix(deps): update dependency @openedx/paragon to v22.12.0 (#1395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 01:22:07 +00:00
renovate[bot]
3e20fcae57 fix(deps): update dependency @openedx/paragon to v22.11.2 (#1391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 01:57:22 +00:00
renovate[bot]
173896811d chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 23:47:31 +00:00
renovate[bot]
7af4a08bd9 fix(deps): update dependency algoliasearch-helper to v3.22.6 (#1393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:57:08 +00:00
renovate[bot]
6c12b3b034 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 15:21:14 +00:00
renovate[bot]
5304085cd8 chore(deps): update dependency babel-plugin-formatjs to v10.5.29 (#1389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 09:02:19 +00:00
renovate[bot]
3cd9ae130c chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:41:46 +00:00
renovate[bot]
28ad2c2cf6 chore(deps): update dependency @openedx/frontend-build to v14.2.1 (#1388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:46:21 +00:00
renovate[bot]
3e889df109 chore(deps): update dependency @edx/browserslist-config to v1.3.0 (#1386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 02:36:06 +00:00
Eemaan Amir
52c6efc34d Merge pull request #1357 from openedx/renovate/universal-cookie-7.x
fix(deps): update dependency universal-cookie to v7
2024-11-27 17:14:02 +05:00
renovate[bot]
584a84a99c fix(deps): update dependency universal-cookie to v7 2024-11-27 09:07:41 +00:00
Eemaan Amir
7e4ab1c74c Merge pull request #1356 from openedx/renovate/reselect-5.x
fix(deps): update dependency reselect to v5
2024-11-27 14:07:16 +05:00
renovate[bot]
13d89cb3a0 fix(deps): update dependency reselect to v5 2024-11-22 06:55:30 +00:00
Eemaan Amir
5a1e2e6c97 Merge pull request #1147 from openedx/renovate/actions-checkout-4.x
chore(deps): update actions/checkout action to v4
2024-11-22 11:54:42 +05:00
renovate[bot]
6f1cf29a60 chore(deps): update actions/checkout action to v4 2024-11-21 10:15:17 +00:00
renovate[bot]
159f1ae30e fix(deps): update font awesome to v6.7.1 (#1374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 08:56:48 +00:00
Eemaan Amir
e2e626552f Merge pull request #1314 from openedx/repo-tools/salman/add-dependabot-file-e2a206c
chore: enable github action auto update in dependabot.yml
2024-11-21 13:53:25 +05:00
edX requirements bot
308d7c62e4 chore: enable github action auto update in dependabot.yml 2024-11-19 19:18:17 +05:00
Eemaan Amir
0bc78da55d Merge pull request #1367 from openedx/renovate/codecov-codecov-action-5.x
chore(deps): update codecov/codecov-action action to v5
2024-11-19 18:28:46 +05:00
renovate[bot]
6527caea54 chore(deps): update codecov/codecov-action action to v5 2024-11-19 10:57:48 +00:00
Eemaan Amir
a52912e35b Merge pull request #1149 from openedx/renovate/husky-9.x
chore(deps): update dependency husky to v9
2024-11-19 15:57:32 +05:00
renovate[bot]
6479382b90 chore(deps): update dependency husky to v9 2024-11-19 00:06:24 +00:00
renovate[bot]
4ce36bb12c fix(deps): update font awesome to v6.7.0 (#1371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:17:51 +00:00
renovate[bot]
4cc7723984 chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#1370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 20:24:00 +00:00
renovate[bot]
3c3d359d4e chore(deps): update dependency babel-plugin-formatjs to v10.5.26 (#1369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 09:44:26 +00:00
renovate[bot]
cccbf3a9d1 chore(deps): update dependency babel-plugin-formatjs to v10.5.25 (#1368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:28:06 +00:00
renovate[bot]
4a3fd2ee8e fix(deps): update dependency @openedx/paragon to v22.10.0 (#1365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 02:40:50 +00:00
Eemaan Amir
48d7cb386a Merge pull request #1362 from openedx/renovate/algolia-instantsearch-monorepo
fix(deps): update dependency algoliasearch-helper to v3.22.5
2024-11-11 18:52:42 +05:00
renovate[bot]
bdf9cab869 fix(deps): update dependency algoliasearch-helper to v3.22.5 2024-11-11 12:41:03 +00:00
Eemaan Amir
be02dabf40 Merge pull request #1361 from openedx/revert-1313-renovate/algolia-instantsearch-monorepo
Revert "fix(deps): update dependency algoliasearch-helper to v3.22.5"
2024-11-11 17:39:58 +05:00
Eemaan Amir
c535fb9d24 Revert "fix(deps): update dependency algoliasearch-helper to v3.22.5"
This reverts commit 8ab8d09b97.
2024-11-11 17:32:28 +05:00
renovate[bot]
8ab8d09b97 fix(deps): update dependency algoliasearch-helper to v3.22.5 2024-11-11 16:45:38 +05:00
renovate[bot]
286c70d50f fix(deps): update react-router monorepo to v6.28.0 (#1355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 12:00:01 +00:00
renovate[bot]
8939e5b91f chore(deps): update dependency babel-plugin-formatjs to v10.5.24 2024-11-06 10:42:36 +00:00
renovate[bot]
bc9f7b3bce fix(deps): update react-router monorepo to v6.27.0 2024-11-01 16:32:19 +00:00
renovate[bot]
fd0bcb9e5f fix(deps): update dependency core-js to v3.39.0 2024-11-01 13:58:08 +00:00
renovate[bot]
98e0167ef1 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-11-01 11:23:30 +00:00
renovate[bot]
8091085f45 chore(deps): update dependency eslint-plugin-import to v2.31.0 2024-11-01 06:51:18 +00:00
renovate[bot]
cd5abd1d9c fix(deps): update dependency redux-mock-store to v1.5.5 2024-11-01 03:09:49 +00:00
renovate[bot]
2a88f435b9 fix(deps): update dependency @edx/frontend-platform to v8.1.2 2024-11-01 00:54:11 +00:00
renovate[bot]
fe1e9c5629 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-10-31 21:45:56 +00:00
renovate[bot]
0e363ca724 chore(deps): update dependency babel-plugin-formatjs to v10.5.22 2024-10-31 19:54:28 +00:00
Bilal Qamar
c874638bd1 test: Remove support for Node 18 (#1312)
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2024-10-31 15:51:22 -04:00
Brian Smith
e5c3b1ed41 Revert "fix(deps): update dependency @testing-library/react to v16" (#1339)
This reverts commit 8a27b8cc37.
2024-10-31 15:25:11 -04:00
renovate[bot]
8a27b8cc37 fix(deps): update dependency @testing-library/react to v16 2024-10-31 15:09:42 -04:00
renovate[bot]
046fbeab01 fix(deps): update dependency react-loading-skeleton to v3.5.0 2024-09-23 01:48:16 +00:00
renovate[bot]
27ea509989 fix(deps): update dependency @openedx/paragon to v22.8.1 2024-09-20 21:22:27 +00:00
renovate[bot]
27f0508e6e chore(deps): update dependency eslint-plugin-import to v2.30.0 2024-09-20 19:10:37 +00:00
renovate[bot]
c53fedf7a1 chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-20 17:06:05 +00:00
renovate[bot]
0f1a5e9aef fix(deps): update react-router monorepo to v6.26.2 2024-09-20 14:37:28 +00:00
Feanil Patel
6cb4b799b7 Merge pull request #1316 from openedx/feanil/ubuntu_upgrade
build: Switch to ubuntu-latest for builds
2024-09-20 10:32:28 -04:00
Feanil Patel
439b9161b5 build: Switch to ubuntu-latest for builds
This code does not have any dependencies that are specific to any specific
version of ubuntu.  So instead of testing on a specific version and then needing
to do work to keep the versions up-to-date, we switch to the ubuntu-latest
target which should be sufficient for testing purposes.

This work is being done as a part of https://github.com/openedx/platform-roadmap/issues/377

closes https://github.com/openedx/frontend-app-authn/issues/1299
2024-09-20 10:25:36 -04:00
sundasnoreen12
6ffa45f0c1 Merge pull request #1317 from openedx/sundas/INF-1551
docs: updated catalog-info file for authn MFE
2024-09-10 16:50:23 +05:00
sundasnoreen12
a03ba3e3b3 docs: updated catalog-info file for authn MFE 2024-09-10 13:36:13 +03:00
renovate[bot]
e2a206caa5 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 2024-09-02 10:22:01 +00:00
Bilal Qamar
3a963da819 build: Upgrade to Node 20 (#1295)
* feat: updated node to v20

* refactor: updated package-lock

* refactor: updated package-lock & lockfile version check workflow

* refactor: updated package-lock along with ci & lockfile version check workflows

* refactor: updated lockfile version workflow
2024-09-02 15:18:31 +05:00
Blue
4a65f0a84c fix: change the totalRegisterationTime to snake case (#1301)
Description:
Convert the totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-08-28 15:08:30 +05:00
renovate[bot]
9688bd3699 fix(deps): update react-router monorepo to v6.26.1 2024-08-23 06:35:23 +00:00
renovate[bot]
c123815a55 fix(deps): update dependency core-js to v3.38.1 2024-08-23 05:27:21 +00:00
renovate[bot]
182e669593 chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-23 01:23:22 +00:00
renovate[bot]
65533b8d58 fix(deps): update dependency algoliasearch-helper to v3.22.4 2024-08-22 22:43:06 +00:00
renovate[bot]
45185dba70 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-08-22 18:38:57 +00:00
Bilal Qamar
444c4b4434 test: Add Node 20 to CI matrix (#1303) 2024-08-22 14:34:50 -04:00
renovate[bot]
d629d66bf2 fix(deps): update react-router monorepo to v6.25.1 2024-07-22 00:49:21 +00:00
renovate[bot]
9d46d68150 fix(deps): update font awesome to v6.6.0 2024-07-19 23:07:12 +00:00
renovate[bot]
a4ed6a362e fix(deps): update dependency @openedx/paragon to v22.7.0 2024-07-19 18:42:43 +00:00
renovate[bot]
a1a0d3cd96 fix(deps): update dependency algoliasearch-helper to v3.22.3 2024-07-19 15:43:53 +00:00
renovate[bot]
950c401e88 fix(deps): update dependency redux to v4.2.1 2024-07-19 12:37:44 -03:00
renovate[bot]
ce056c9ad2 fix(deps): update react-router monorepo to v6.24.1 2024-07-09 09:47:38 +00:00
renovate[bot]
3bd6e454d0 fix(deps): update dependency @edx/frontend-platform to v8.1.0 2024-07-09 06:59:28 +00:00
renovate[bot]
f52129a11e chore(deps): update dependency iframe-resizer to v4.4.4 2024-07-09 03:58:26 +00:00
renovate[bot]
ea01050163 fix(deps): update dependency algoliasearch-helper to v3.22.2 2024-07-09 00:27:00 +00:00
renovate[bot]
c1ec9b6e99 fix(deps): update dependency algoliasearch-helper to v3.22.1 2024-07-01 18:59:29 +00:00
renovate[bot]
2c509b00ac fix(deps): update dependency @openedx/paragon to v22.6.1 2024-07-01 17:39:58 +00:00
renovate[bot]
ef358fe741 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-07-01 12:56:55 +00:00
renovate[bot]
56e0520d9c chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-07-01 11:26:22 +00:00
Bilal Qamar
1f7b7f5c41 chore: major version upgrades for frontend-platform & frontend-build (#1251) 2024-07-01 13:22:45 +02:00
renovate[bot]
471fa75155 fix(deps): update react-router monorepo to v6.23.1 2024-06-18 22:48:40 +00:00
renovate[bot]
c89d16e529 fix(deps): update dependency core-js to v3.37.1 2024-06-18 20:06:43 +00:00
renovate[bot]
fc02ab820a fix(deps): update dependency algoliasearch-helper to v3.22.0 2024-06-18 17:06:47 +00:00
renovate[bot]
ac23cdcc7a fix(deps): update dependency algoliasearch-helper to v3.21.0 2024-06-18 12:06:40 +00:00
renovate[bot]
02c4c5be29 fix(deps): update dependency @openedx/paragon to v22.6.0 2024-06-18 09:11:03 +00:00
renovate[bot]
3bd7d61e3a fix(deps): update dependency form-urlencoded to v6.1.5 2024-06-18 07:26:49 +00:00
renovate[bot]
32ebc69c0e fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-06-18 03:19:51 +00:00
renovate[bot]
c98c3b16c5 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-18 02:42:13 +00:00
renovate[bot]
287fe3adfe fix(deps): update dependency @edx/frontend-platform to v7.1.4 2024-06-17 22:30:03 +00:00
renovate[bot]
d4e7b7b371 chore(deps): update dependency iframe-resizer to v4.3.11 2024-06-17 19:04:24 +00:00
renovate[bot]
ad78f068e0 chore(deps): update dependency babel-plugin-formatjs to v10.5.16 2024-06-17 15:05:32 +00:00
Adolfo R. Brandes
d156de2e66 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:02:25 -03:00
Attiya Ishaque
99bca1bd9b fix: frontend validation on email field (#1249) 2024-06-12 14:15:56 +05:00
Blue
8efb22595c fix: add new entry for another US label (#1244)
Add new entry for for another US label which is United States
2024-05-03 10:27:32 +05:00
Syed Sajjad Hussain Shah
73e8913f90 feat: remove username from the registration from (#1201) (#1241)
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
2024-05-02 08:55:56 +05:00
Stanislav Lunyachek
3ddaf795f2 feat: Hide preloaders for third party auth providers if they are disabled 2024-04-19 10:55:48 +05:00
renovate[bot]
a18df02d37 fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-11 09:20:07 +00:00
renovate[bot]
5a3cd93a09 fix(deps): update dependency algoliasearch to v4.23.3 2024-04-11 06:45:25 +00:00
renovate[bot]
d989eba0e1 fix(deps): update dependency @openedx/paragon to v22.2.1 2024-04-11 04:10:33 +00:00
renovate[bot]
00f8ee9c85 chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-11 01:50:57 +00:00
renovate[bot]
01b14d6d30 fix(deps): update font awesome to v6.5.2 2024-04-10 21:29:13 +00:00
renovate[bot]
35dbca7bd1 fix(deps): update dependency @edx/frontend-platform to v7.1.3 2024-04-10 19:15:33 +00:00
renovate[bot]
73579ec53d chore(deps): update dependency babel-plugin-formatjs to v10.5.14 2024-04-10 17:38:23 +00:00
Syed Sajjad Hussain Shah
90ae870a93 fix: codecov not running on PRs (#1213) 2024-04-03 11:28:39 +05:00
Zainab Amir
e9af062ff1 fix: resolve console warnings in tests (#1212) 2024-04-01 01:33:50 -07:00
Syed Sajjad Hussain Shah
60a6c97e22 fix: codecov not running on PRs (#1211) 2024-04-01 11:30:54 +05:00
mubbsharanwar
cd8474465b perf: use useSelector with granular approach in registration component to minimize rendrening 2024-04-01 10:42:55 +05:00
Samir Sabri
2d37b8b0bf feat!: remove Transifex calls for OEP-58 (#1085) 2024-03-20 09:31:09 -04:00
renovate[bot]
05c2caa4d9 fix(deps): update dependency core-js to v3.36.1 2024-03-20 03:45:13 +00:00
renovate[bot]
535a8c543f fix(deps): update dependency @edx/frontend-platform to v7.1.2 2024-03-20 02:22:44 +00:00
Dmytro
dc90cf9ce5 fix: Registration with password that doesn't meet the requirements (#1184)
* fix: Registration with password that doesn't meet the requirements
---------

Co-authored-by: Dima Alipov <dimaalipov@MacBook-Pro-Dima.local>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-03-15 11:53:42 +05:00
Attiya Ishaque
36354761cc feat: Remove simplify registration experiment code (#1194) 2024-03-15 10:58:54 +05:00
renovate[bot]
f53add81f3 fix(deps): update react-router monorepo to v6.22.3 2024-03-08 12:18:55 +00:00
renovate[bot]
bca59ebd40 fix(deps): update dependency algoliasearch-helper to v3.16.3 2024-03-08 10:45:07 +00:00
renovate[bot]
dcb5da42b0 fix(deps): update dependency @edx/frontend-platform to v7.1.1 2024-03-08 06:51:41 +00:00
renovate[bot]
b346c22b57 chore(deps): update codecov/codecov-action action to v4 (#1172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-07 19:28:01 -08:00
Zainab Amir
8be350e35f Fix TPA skeleton loader (#1189)
* feat: update TPA skeleton
* fix: Prevent wrong appearance of skeleton after second tab click

---------

Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2024-03-06 06:11:54 -08:00
Syed Sajjad Hussain Shah
6695fb6f61 fix: move country field to simplify registration second step (#1195) 2024-03-06 12:21:56 +05:00
Syed Sajjad Hussain Shah
be5b0bb461 fix: invalid form submit btn event error (#1191) 2024-03-05 14:57:10 +05:00
Syed Sajjad Hussain Shah
5f93278326 feat: add invalid form submit click event: (#1190) 2024-03-05 13:13:30 +05:00
Blue
488644f50d fix: remove rebrand experiment from authn (#1187)
Description:
Remove rebrand experiment code from Authn
VAN-1858
2024-03-04 16:14:41 +05:00
Hina Khadim
56bab26018 fix: browser header showing null and replace authn with Authentication (#1185) 2024-03-04 00:53:26 -08:00
Attiya Ishaque
ca42f3851d fix: Fix post registration recommendations card issue (#1183) 2024-03-01 15:25:56 +05:00
Syed Sajjad Hussain Shah
e4bddc2db0 fix: remove password field duplicate validation (#1181) 2024-02-29 17:47:13 +05:00
Syed Sajjad Hussain Shah
8aeacaa001 fix: fix error banner alert (#1180) 2024-02-29 12:14:52 +05:00
Syed Sajjad Hussain Shah
80435d3e5b fix: change submit cta text for experiment (#1179) 2024-02-29 11:25:43 +05:00
Syed Sajjad Hussain Shah
d6c5415c9a feat: add submit btn click event (#1178) 2024-02-28 11:38:07 +05:00
Zainab Amir
0306763eeb feat: update code owner information (#1177) 2024-02-27 06:40:48 -08:00
Zainab Amir
e4ac1288a9 feat: add submit btn click event for default register page (#1176)
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-02-27 01:24:52 -08:00
Attiya Ishaque
e1f489838c fix: browser header showing null (#1168) 2024-02-27 12:52:25 +05:00
Syed Sajjad Hussain Shah
9b046146a0 fix: fix simplify registration experiment bugs (#1175) 2024-02-27 10:12:16 +05:00
Syed Sajjad Hussain Shah
e0d605582e fix: fix simplify registration experiment bugs (#1174) 2024-02-26 18:54:16 +05:00
Blue
21b5a01cab fix: Update custom SSO buttons to use brand colors (#1165)
Description: Update custom SSO buttons to use brand colors
VAN-1838
2024-02-26 18:51:12 +05:00
Syed Sajjad Hussain Shah
955ea6485f fix: update comment (#1173) 2024-02-26 17:50:19 +05:00
Syed Sajjad Hussain Shah
9f8a1af7e3 feat: simplify registration experiment (#1164) 2024-02-26 17:22:24 +05:00
renovate[bot]
e617a3ba40 fix(deps): update react-router monorepo to v6.22.1 2024-02-26 12:16:52 +00:00
renovate[bot]
fb3f962039 fix(deps): update dependency react-loading-skeleton to v3.4.0 2024-02-26 11:25:17 +00:00
renovate[bot]
64da54f17c fix(deps): update dependency core-js to v3.36.0 2024-02-26 11:15:39 +00:00
renovate[bot]
74741a1be6 fix(deps): update dependency @edx/frontend-platform to v7.1.0 2024-02-26 10:46:33 +00:00
Stanislav
e9aaf7024a fix: Enabling the ability to log in with a username consisting of 2 characters (#1073)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2024-02-15 15:07:24 +05:00
Blue
e3d96385ee fix: remove username and bring in name field for embedded case (#1163)
Description:
Remove username and bring in name field in case embedded registration
VAN-1824
2024-02-14 16:39:31 +05:00
Blue
dc266a613e fix: replace username with name in base component and on welcome page (#1161)
* fix: replace username with fullName in base component and on welcome page
Description: Replace username with name in welcome page component
VAN-1824
2024-02-14 12:12:43 +05:00
Syed Sajjad Hussain Shah
1b5755664c chore: upgrade paragon to v22.1.1 (#1162) 2024-02-14 11:15:26 +05:00
Zainab Amir
02bd8abcd1 feat: update default authn settings (#1160)
At present, Authn MFE doesn't work out of the box with
devstack settings. These changes allow authn to work with
default devstack settings.
2024-02-13 00:54:26 -08:00
Blue
a6e96f5ed1 fix: update paragone and country field (#1157)
Description: Update Paragon version and changed the country field implementation as per documentation
VAN-1814
2024-02-09 16:40:41 +05:00
Attiya Ishaque
872aa48675 feat: modifying the base container in authn (#1153) 2024-02-09 16:15:22 +05:00
Blue
60efe3cbb7 feat: removal of country code selection event (#1129)
Revert country code selection event

VAN-1793
Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>
2024-02-07 06:09:48 -08:00
Muhammad Abdullah Waheed
167f86c283 fix: updated frontend-build to latest version to fix sharp issue (#1154) 2024-02-06 14:50:41 +05:00
Blue
02d14a6359 fix: country field is not populated on page load (#1151)
Description: Country field is not populated when page load
VAN-1795
2024-02-01 11:20:34 +05:00
Omar Al-Ithawi
a6a473ee5c feat: tutor-mfe compatiblilty for atlas pull (#1152)
- install atlas
 - remove `--filter` to pull all languages by default
 - use ATLAS_OPTIONS to allow custom `--filter`
 - include frontend-platform in `atlas pull`

Refs: FC-0012 OEP-58
2024-01-30 13:05:02 -05:00
renovate[bot]
8be469680d fix(deps): update react-router monorepo to v6.21.3 2024-01-26 11:41:55 +00:00
renovate[bot]
45e84d3f9c fix(deps): update font awesome to v6.5.1 2024-01-26 09:39:53 +00:00
renovate[bot]
d6d71587c7 chore(deps): update dependency babel-plugin-formatjs to v10.5.13 2024-01-26 08:46:38 +00:00
renovate[bot]
6b70692dd4 fix(deps): update dependency redux-saga to v1.3.0 2024-01-26 04:43:47 +00:00
renovate[bot]
a056f241b5 fix(deps): update dependency core-js to v3.35.1 2024-01-26 00:44:08 +00:00
renovate[bot]
115ce8d7c6 fix(deps): update dependency classnames to v2.5.1 2024-01-25 22:09:32 +00:00
renovate[bot]
6e58c13ef5 fix(deps): update dependency @redux-devtools/extension to v3.3.0 2024-01-25 18:00:44 +00:00
renovate[bot]
65e29a021b chore(deps): update dependency jest to v29.7.0 2024-01-25 14:48:39 +00:00
renovate[bot]
6c91f01226 chore(deps): update dependency eslint-plugin-import to v2.29.1 2024-01-25 10:34:20 +00:00
renovate[bot]
36a9ebef8c fix(deps): update dependency regenerator-runtime to v0.14.1 2024-01-25 06:02:01 +00:00
renovate[bot]
5c921fb983 fix(deps): update dependency form-urlencoded to v6.1.4 2024-01-25 04:14:27 +00:00
renovate[bot]
98699b08ad chore(deps): update dependency babel-plugin-formatjs to v10.5.12 2024-01-25 00:39:42 +00:00
renovate[bot]
1f3d1d1aee fix(deps): update dependency algoliasearch-helper to v3.16.2 2024-01-24 22:48:51 +00:00
Brian Smith
fc60d9f7d1 chore(deps): update paragon and frontend-build to openedx scope (#1132) 2024-01-24 10:10:13 -05:00
Zainab Amir
ad7099ad38 Refactor login page to use functional component (#899)
* refactor: zamir/van 1390/add basic login form (#894)
Description: login refactor

* feat: add basic login form
* feat: remove cookie logic from login page

* refactor: refactor social auth, tpahint and institution login (#895)

* feat: add basic login form

* feat: add error handling

* refactor: refactor social auth, tpahint and institution login

Description:

Refactor the following flows:
1 - Institution login
2 - SSO login
3 - Tpahint

VAN-1391

---------

Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>

* fix: tests and lint issues (#905)

* feat: add tests

* fix: rebase this branch with master
Description:
Rebase with master branch
VAN-1413

* chore: update variable name

* fix: fix When using tpa-hint don't show the login form
Description: When using tpa-hint don't show the login form
VAN-1801

---------

Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: ahtesham-quraish <ahtesham.quraish@gmail.com>
2024-01-24 10:44:31 +05:00
mubbsharanwar
2ea9301c5e refactor: depreciate enzyme
move unit test from enzyme to RTL

VAN-1792
2024-01-17 12:37:01 +05:00
Blue
b9b4492de9 feat: check how many users update country on registration form after we auto populate it with IP (#1127)
Description: Check how many users update country on registration form after we auto populate it with IP.
VAN-1793
2024-01-16 12:35:54 +05:00
Attiya Ishaque
d74b5c49d9 feat: add work experience property to the welcome page event (#1125) 2024-01-11 16:00:43 +05:00
Syed Sajjad Hussain Shah
27545ea4b6 refactor: move login unit tests to RTL (#1123)
fix: migrate login tests to RTL
Description:
Migrate login unit tests to RTL
VAN-1770

Co-authored-by: ahtesham-quraish <ahtesham.quraish@gmail.com>
2024-01-05 17:16:31 +05:00
Attiya Ishaque
db3655c843 fix: Migrate Registration tests to RTL (#1122) 2024-01-04 14:33:48 +05:00
Attiya Ishaque
10a10c8ed9 fix: Migrate forgot password and reset password tests to RTL (#1115) 2023-12-26 17:03:27 +05:00
Attiya Ishaque
800a5fc6be fix: Migrate common components tests to RTL (#1117) 2023-12-20 15:25:33 +05:00
Attiya Ishaque
924488c29b fix: Migrate welcome page and recommendations tests to RTL (#1116) 2023-12-19 15:57:51 +05:00
Attiya Ishaque
dae050ecb3 fix: country field error is updated (#1108) 2023-12-08 13:35:03 +05:00
Stanislav
3a31cf33e2 fix: Missed favicon in Safari (#1077)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2023-12-08 10:06:39 +05:00
renovate[bot]
66a0d5d840 chore(deps): update dependency iframe-resizer to v4.3.9 2023-12-04 15:34:52 +00:00
renovate[bot]
26caf857bf chore(deps): update dependency babel-plugin-formatjs to v10.5.10 2023-12-04 13:50:50 +00:00
renovate[bot]
fa34eae800 chore(deps): update dependency @edx/frontend-build to v13.0.8 2023-12-04 11:48:51 +00:00
vladislavkeblysh
644b16580d fix: fixed spacing in tablet version (#1062) 2023-11-28 10:47:53 +05:00
Ihor Romaniuk
479dac8397 fix: content centering and z-index position on adaptation (#1094) 2023-11-27 11:44:03 +05:00
Attiya Ishaque
c097b5b831 fix: moved registration tests to specfic files. (#1104) 2023-11-22 13:54:56 +05:00
Moncef Abboud
78722f3e73 feat: implement SHOW_REGISTRATION_LINKS setting 2023-11-21 18:54:39 +05:00
edx-transifex-bot
7f8a270770 chore(i18n): update translations (#1106)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-11-20 12:09:20 +05:00
Syed Sajjad Hussain Shah
4ff14c8731 fix: fix duplicate mfe context calls and cookie set exc (#1103) 2023-11-16 10:44:29 +05:00
edx-transifex-bot
a957973105 chore(i18n): update translations (#1101)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
2023-11-15 12:25:29 +05:00
Blue
f0b855d87e fix: add spinner while loading the optional fields in embadded exp (#1102)
Description:
Add spinner while loading the optional fields in embadded exp
VAN-1658
2023-11-14 18:32:39 +05:00
Ghassan Maslamani
446649735d fix: form submission when country is not requried (#1099)
When country is not required to submit, the displayValue check
will always return true. This fixes it by only check it if it's
needed through the config `SHOW_CONFIGURABLE_EDX_FIELDS`

This issue might has been discoverd at edx.org becuase edx.org
requires the Country to be filled when creating an account,
however this is not the case for Open edX by default, hence the
issue reported below

Ref: openedx/wg-build-test-release/issues/318
2023-11-13 12:14:34 -05:00
Mashal Malik
397c237e30 refactor: updated README file to reflect template changes (#1089)
* refactor: updated README file to reflect template changes

* refactor: updated README file to reflect template changes

* refactor: updated README file to reflect template changes

* refactor: update readMe file
2023-11-01 10:58:30 +05:00
Feanil Patel
e10d6b6384 docs: Update the security e-mail address.
This repository is now managed by the Axim Collaborative and security issues
with it should be reported to security@openedx.org instead of security@edx.org

This work is being done as a part of https://github.com/openedx/wg-security/issues/16
2023-10-31 12:42:02 -04:00
Feanil Patel
38c186d5a7 chore: Update to the new version of brand-openedx in the new scope. (#1083)
Part of https://github.com/openedx/axim-engineering/issues/23

This updates the brand alias to point to the package at the `openedx`
scope.  This does not impact imports because this package is used via an
alias.
2023-10-20 17:28:31 -04:00
Syed Ali Abbas Zaidi
55c320a88a chore: bump frontend-platform (#1076) 2023-10-17 12:02:54 +05:00
Muhammad Abdullah Waheed
6ec0a22194 feat: babel-plugin-react-intl to babel-plugin-formatjs migration (#1067)
* feat: babel-plugin-react-intl to babel-plugin-formatjs migration

* fix: upgraded frontend-build to fix security issue
2023-10-13 09:28:29 +05:00
Syed Sajjad Hussain Shah
7bae030713 fix: name field validations (#1069) 2023-10-11 09:44:03 +05:00
Zainab Amir
7b4714a22a feat: fix recommendations card subtitle (#1070) 2023-10-06 15:58:29 +05:00
Zainab Amir
332d6abee7 feat: updated default value for USER_RETENTION_COOKIE_NAME (#1059)
Moved the value to internal configuration

VAN-1624
2023-09-12 14:10:43 +05:00
Zainab Amir
4df13cf0b7 feat: remove optimizely event (#1051)
VAN-1624
2023-09-12 13:32:29 +05:00
Syed Sajjad Hussain Shah
512deae883 fix: recs skip btn event gets cancelled on redirection (#1058) 2023-09-08 15:37:33 +05:00
Syed Sajjad Hussain Shah
2d11477037 fix: post registration recommendations eventing (#1057) 2023-09-08 15:04:38 +05:00
Zainab Amir
5e15969f4a Recommendations v2 (#1040)
* feat: add personalized recommendations (#1024)

* use Algolia for personalized recommendations
* show personalized recommendations to use that have consented
to functional cookies
* update tests

VAN-1599

* Revert "fix: special characters in redirect url getting decoded to space (#1029)" (#1030)

This reverts commit fc62241332.

* feat: update recommendations page design (#1036)

VAN-1598

* feat: add events for recommendations (#1039)

* feat: remove static recommendations

---------

Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
2023-09-08 12:08:41 +05:00
Syed Sajjad Hussain Shah
37e811d7e5 fix: updated optional fields check based on updated response (#1055) 2023-09-07 09:31:19 +05:00
Mashal Malik
a35a1d1ba6 refactor: updated lock file version check to use new workflow (#1048)
Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com>
2023-09-06 14:58:18 +05:00
Syed Sajjad Hussain Shah
41a9c89d71 fix: password validation with correct password value on icon blur (#1054) 2023-09-05 17:52:35 +05:00
Syed Sajjad Hussain Shah
d469102cee refactor: registration component refactoring (#1050)
* refactor: registration component refactoring

* fix: refactored constants
2023-09-05 13:52:25 +05:00
edx-transifex-bot
c685bdd373 chore(i18n): update translations (#1052)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-09-04 16:23:08 +05:00
Emad Rad
daa7ae4d73 feat: Add persian language (#1020)
Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>
2023-08-30 13:09:40 +05:00
Zainab Amir
c5caaeba60 feat: add registration conversion event (#1049) 2023-08-29 15:22:07 +05:00
Syed Sajjad Hussain Shah
a473d79554 fix: forms data persistence issue (#1046) 2023-08-25 17:48:49 +05:00
Zainab Amir
1b5aa106ab feat: remove hydrate call (#1043) 2023-08-24 12:29:48 +05:00
271 changed files with 31734 additions and 21930 deletions

8
.env
View File

@@ -15,7 +15,7 @@ SEGMENT_KEY=''
SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME=null
USER_RETENTION_COOKIE_NAME=null
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
@@ -23,11 +23,13 @@ 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_POPULAR_AND_TRENDING_RECOMMENDATIONS=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
MARKETING_EMAILS_OPT_IN=''
SHOW_CONFIGURABLE_EDX_FIELDS=''
ENABLE_IMAGE_LAYOUT=''
# ***** Zendesk related keys *****
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
@@ -39,3 +41,5 @@ BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -19,8 +19,10 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
INFO_EMAIL='info@example.com'
# ***** Features *****
ENABLE_DYNAMIC_REGISTRATION_FIELDS='true'
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Links *****
@@ -39,3 +41,5 @@ APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -16,6 +16,6 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

View File

@@ -1,12 +1,12 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
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
// @edx/frontend-build from v3 to v5:
// @openedx/frontend-build from v3 to v5:
// - TypeError: Cannot read property 'range' of null
indent: [
'error',

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @openedx/2U-infinity

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -10,7 +10,7 @@ on:
jobs:
autoupdate:
name: autoupdate
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1
env:

View File

@@ -10,17 +10,15 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v4
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- name: Install Dependencies
run: npm ci
@@ -41,4 +39,7 @@ jobs:
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ temp/babel-plugin-react-intl
*~
/temp
/.vscode
src/i18n/messages

2
.nvmrc
View File

@@ -1 +1 @@
18
24

View File

@@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-authn]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

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

View File

@@ -1,20 +1,17 @@
export TRANSIFEX_RESOURCE = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA,it_IT,pt_PT,de_DE"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs
precommit:
npm run lint
npm audit
requirements:
npm install
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -32,33 +29,16 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
&& atlas pull $(ATLAS_OPTIONS) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
$(intl_imports) paragon frontend-app-authn
endif
$(intl_imports) paragon frontend-platform frontend-app-authn
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,12 +1,12 @@
##################
frontend-app-authn
##################
|Build Status| |ci-badge| |Codecov| |semantic-release|
frontend-app-authn
====================
Please tag **@openedx/vanguards** on any PRs or issues. Thanks!
Introduction
------------
********
Purpose
********
This is a micro-frontend application responsible for the login, registration and password reset functionality.
@@ -22,33 +22,57 @@ This is a micro-frontend application responsible for the login, registration and
- Progressive profiling page
***************
Getting Started
***************
Installation
------------
Prerequisites
=============
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
`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.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
2. Start up LMS, if it's not already started.
Cloning and Startup
===================
4. Within this project (frontend-app-authn), install requirements and start the development server:
1. Clone your new repo:
.. code-block::
.. code-block:: bash
npm install
npm start # The server will run on port 1999
git clone https://github.com/edx/frontend-app-authn.git
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
2. Use the version of Node specified in the ``.nvmrc`` file.
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
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
---------------------------------
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
The authentication micro-frontend also requires the following additional variable:
@@ -106,15 +130,19 @@ The authentication micro-frontend also requires the following additional variabl
* - ``MFE_CONFIG_API_URL``
- 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``
- 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.
- ``authn`` | ``''``
* - ``ENABLE_IMAGE_LAYOUT``
- Enables the image layout feature within the authn. When set to True, this feature allows the inclusion of images in the base container layout. For more details on configuring this feature, please refer to the `Modifying base container <docs/how_tos/modifying_base_container.rst>`_.
- ``true`` | ``''`` (empty strings are falsy)
edX-specific Environment Variables
**********************************
==================================
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
@@ -132,15 +160,16 @@ Furthermore, there are several edX-specific environment variables that enable in
* - ``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)
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
How To Contribute
------------
=================
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
@@ -149,34 +178,58 @@ can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend
This project is currently accepting all types of contributions, bug fixes and security fixes.
Open edX Code of Conduct
------------------------
Getting Help
============
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-authn/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
============================
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
People
------
======
The assigned maintainers for this component and other project details may be
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues
-------------------------
=========================
Please do not report security issues in public. Please email security@edx.org.
Please do not report security issues in public. Please email security@openedx.org.
Known Issues
------------
============
None
License
-------
=======
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
otherwise noted.
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
==============================
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master

View File

@@ -12,7 +12,8 @@ metadata:
icon: 'Article'
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:vanguards
owner: group:2u-infinity
type: 'service'
lifecycle: 'production'

View File

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

View File

@@ -0,0 +1,39 @@
========================================
Modifying the Base Container in Authn
========================================
The base container in Authn serves as the fundamental layout structure for rendering different components based on configurations. This document outlines the process for modifying the base container to accommodate changes or customize layouts as needed.
Understanding Base Container Versions
--------------------------------------
The base container supports two main versions:
- **Default Layout:** The default layout is the standard layout used when specific configurations do not dictate otherwise.
.. image:: ../images/default_layout.png
- **Image Layout:** The image layout is an alternative layout option that can be enabled based on configurations.
.. image:: ../images/image_layout.png
Enabling the Image Layout
---------------------------
To activate the image layout feature, navigate to your .env file and update the configurations:
**Update Configuration**
Locate the ``ENABLE_IMAGE_LAYOUT`` parameter and set its value to ``true``. Additionally, ensure that the Image configuration settings are provided. Your overall configurations should resemble the following:
.. code-block::
# ***** Image Layout Configuration *****
ENABLE_IMAGE_LAYOUT = True # Set to True to enable image layout feature
# ***** Base Container Images *****
BANNER_IMAGE_LARGE='' # Path to the large banner image
BANNER_IMAGE_MEDIUM='' # Path to the medium-sized banner image
BANNER_IMAGE_SMALL='' # Path to the small banner image
BANNER_IMAGE_EXTRA_SMALL='' # Path to the extra-small banner image
This allows for the customization and adaptation of the base container layout according to specific requirements.

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('jest', {
setupFiles: [

View File

@@ -1,8 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: Authn MFE
oeps: {}
owner: openedx/vanguards
openedx-release:
ref: master

28891
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,14 @@
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -32,52 +29,48 @@
"url": "https://github.com/openedx/frontend-app-authn/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-platform": "^5.0.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.2.5",
"@tanstack/react-query": "^5.90.19",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"classnames": "2.3.2",
"core-js": "3.32.0",
"algoliasearch-helper": "^3.26.0",
"classnames": "2.5.1",
"core-js": "3.43.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.0",
"form-urlencoded": "6.1.6",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.3.1",
"react-redux": "7.2.9",
"react-loading-skeleton": "3.5.0",
"react-responsive": "8.2.0",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.4",
"redux-saga": "1.2.3",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.0",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
"regenerator-runtime": "0.14.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.9.8",
"@edx/reactifex": "1.1.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"babel-plugin-formatjs": "10.5.3",
"enzyme": "3.11.0",
"eslint-plugin-import": "2.28.0",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.9.1",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "7.0.4",
"jest": "29.6.2",
"react-test-renderer": "^17.0.2"
"jest": "30.3.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
}
}

View File

@@ -1,11 +1,12 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Authn | <%= process.env.SITE_NAME %></title>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
<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>

View File

@@ -1,14 +1,12 @@
import React from 'react';
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 configureStore from './data/configureStore';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
@@ -31,33 +29,48 @@ import './index.scss';
registerIcons();
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
});
const MainApp = () => (
<AppProvider store={configureStore()}>
<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 /></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 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;

View File

@@ -1,50 +1,48 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { render, screen } from '@testing-library/react';
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
describe('Default Layout tests', () => {
it('should display the form passed as a child in SmallScreenLayout', () => {
const smallScreen = mount(
render(
<IntlProvider locale="en">
<div>
<DefaultSmallLayout />
<form>
<form aria-label="form">
<input type="text" />
</form>
</div>
</IntlProvider>,
);
expect(smallScreen.find('form').exists()).toEqual(true);
expect(screen.getByRole('form')).toBeDefined();
});
it('should display the form passed as a child in MediumScreenLayout', () => {
const mediumScreen = mount(
render(
<IntlProvider locale="en">
<div>
<DefaultMediumLayout />
<form>
<form aria-label="form">
<input type="text" />
</form>
</div>
</IntlProvider>,
);
expect(mediumScreen.find('form').exists()).toEqual(true);
expect(screen.getByRole('form')).toBeDefined();
});
it('should display the form passed as a child in LargeScreenLayout', () => {
const largeScreen = mount(
render(
<IntlProvider locale="en">
<div>
<DefaultLargeLayout />
<form>
<form aria-label="form">
<input type="text" />
</form>
</div>
</IntlProvider>,
);
expect(largeScreen.find('form').exists()).toEqual(true);
expect(screen.getByRole('form')).toBeDefined();
});
});

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './messages';

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './messages';
@@ -23,13 +21,15 @@ const MediumLayout = () => {
<div>
<h1
className={classNames(
'display-1 text-white mt-5 mb-5 mr-2',
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</span>
</h1>
</div>

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './messages';
@@ -17,17 +15,18 @@ const SmallLayout = () => {
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
<div className="d-flex align-items-center m-3.5">
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
<h1
className={classNames(
'text-white mt-3.5 mb-3.5',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span className="mr-1">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</span>
</h1>
</div>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import messages from './messages';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import './index.scss';
import messages from './messages';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import './index.scss';
import messages from './messages';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import messages from './messages';

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const LargeLayout = ({ username }) => {
const LargeLayout = ({ fullName }) => {
const { formatMessage } = useIntl();
return (
@@ -20,7 +18,7 @@ const LargeLayout = ({ username }) => {
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
<div>
<h1 className="welcome-to-platform data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
</h1>
<h2 className="complete-your-profile">
{formatMessage(messages['complete.your.profile.1'])}
@@ -43,7 +41,7 @@ const LargeLayout = ({ username }) => {
};
LargeLayout.propTypes = {
username: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
};
export default LargeLayout;

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const MediumLayout = ({ username }) => {
const MediumLayout = ({ fullName }) => {
const { formatMessage } = useIntl();
return (
@@ -22,7 +20,7 @@ const MediumLayout = ({ username }) => {
<div className="medium-yellow-line mt-5 mr-n2" />
<div>
<h1 className="h3 data-hj-suppress mw-320">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
</h1>
<h2 className="display-1">
{formatMessage(messages['complete.your.profile.1'])}
@@ -46,7 +44,7 @@ const MediumLayout = ({ username }) => {
};
MediumLayout.propTypes = {
username: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
};
export default MediumLayout;

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import { Hyperlink, Image } from '@openedx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const SmallLayout = ({ username }) => {
const SmallLayout = ({ fullName }) => {
const { formatMessage } = useIntl();
return (
@@ -16,11 +14,11 @@ const SmallLayout = ({ username }) => {
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
</Hyperlink>
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
<div className="d-flex align-items-center m-3.5">
<div className="small-yellow-line mt-4.5" />
<div>
<h1 className="h5 data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
</h1>
<h2 className="h1">
{formatMessage(messages['complete.your.profile.1'])}
@@ -35,7 +33,7 @@ const SmallLayout = ({ username }) => {
};
SmallLayout.propTypes = {
username: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
};
export default SmallLayout;

View File

@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'welcome.to.platform': {
id: 'welcome.to.platform',
defaultMessage: 'Welcome to {siteName}, {username}!',
defaultMessage: 'Welcome to {siteName}, {fullName}!',
description: 'Welcome message that appears on progressive profile page',
},
'complete.your.profile.1': {

View File

@@ -1,4 +0,0 @@
const IMAGE_LAYOUT = 'image-layout';
const DEFAULT_LAYOUT = 'default-layout';
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };

View File

@@ -1,7 +1,5 @@
import React, { useEffect, useState } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { breakpoints } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints } from '@openedx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
@@ -11,45 +9,26 @@ import {
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
} from './components/image-layout';
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
const BaseContainer = ({ children, showWelcomeBanner }) => {
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
const username = authenticatedUser ? authenticatedUser.username : null;
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
useEffect(() => {
const initRebrandExperiment = () => {
if (window.experiments?.rebrandExperiment) {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
} else {
window.experiments = window.experiments || {};
window.experiments.rebrandExperiment = {};
window.experiments.rebrandExperiment.handleLoaded = () => {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
};
}
};
initRebrandExperiment();
}, []);
if (baseContainerVersion === IMAGE_LAYOUT) {
if (enableImageLayout) {
return (
<div className="layout">
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
{authenticatedUser ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageExtraSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
{authenticatedUser ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{authenticatedUser ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <ImageMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{authenticatedUser ? <AuthLargeLayout username={username} /> : <ImageLargeLayout />}
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <ImageLargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
{children}
</div>
</div>
@@ -61,15 +40,15 @@ const BaseContainer = ({ children, showWelcomeBanner }) => {
<div className="col-md-12 extra-large-screen-top-stripe" />
<div className="layout">
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
{authenticatedUser ? <AuthSmallLayout username={username} /> : <DefaultSmallLayout />}
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <DefaultSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{authenticatedUser ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <DefaultMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{authenticatedUser ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <DefaultLargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
{children}
</div>
</div>
@@ -79,11 +58,13 @@ const BaseContainer = ({ children, showWelcomeBanner }) => {
BaseContainer.defaultProps = {
showWelcomeBanner: false,
fullName: null,
};
BaseContainer.propTypes = {
children: PropTypes.node.isRequired,
showWelcomeBanner: PropTypes.bool,
fullName: PropTypes.string,
};
export default BaseContainer;

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import { Context as ResponsiveContext } from 'react-responsive';
import BaseContainer from '../index';
@@ -12,32 +11,34 @@ const LargeScreen = {
};
describe('Base component tests', () => {
it('should should default layout', () => {
const baseContainer = mount(
it('should show default layout', () => {
const { container } = render(
<IntlProvider locale="en">
<BaseContainer />
<BaseContainer>
<div>Test Content</div>
</BaseContainer>
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
expect(container.querySelector('.banner__image')).toBeNull();
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
});
it('[experiment] should show image layout for treatment group', () => {
window.experiments = {
rebrandExperiment: {
variation: 'image-layout',
},
};
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
mergeConfig({
ENABLE_IMAGE_LAYOUT: true,
});
const baseContainer = mount(
const { container } = render(
<IntlProvider locale="en">
<BaseContainer />
<BaseContainer showWelcomeBanner={false}>
<div>Test Content</div>
</BaseContainer>
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
expect(container.querySelector('.banner__image')).toBeDefined();
});
});

View File

@@ -1,5 +1,3 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';

View File

@@ -1,13 +1,11 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
Button, Form,
Icon,
} from '@edx/paragon';
import { Login } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
} from '@openedx/paragon';
import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
@@ -19,7 +17,8 @@ import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
const EnterpriseSSO = (props) => {
const { formatMessage } = useIntl();
const tpaProvider = props.provider;
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|| getConfig().SHOW_REGISTRATION_LINKS === false;
const handleSubmit = (e, url) => {
e.preventDefault();
@@ -74,7 +73,7 @@ const EnterpriseSSO = (props) => {
className="w-100"
onClick={(e) => handleClick(e)}
>
{disablePublicAccountCreation
{hideRegistrationLink
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
: formatMessage(messages['enterprisetpa.login.button.text'])}
</Button>

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { useState } from 'react';
import {
Form, TransitionReplace,
} from '@edx/paragon';
} from '@openedx/paragon';
import PropTypes from 'prop-types';
const FormGroup = (props) => {

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink, Icon } from '@edx/paragon';
import { Institution } from '@edx/paragon/icons';
import { Button, Hyperlink, Icon } from '@openedx/paragon';
import { Institution } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (

View File

@@ -1,41 +1,116 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
} from '@edx/paragon';
} from '@openedx/paragon';
import {
Check, Remove, Visibility, VisibilityOff,
} from '@edx/paragon/icons';
} from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
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';
const PasswordField = (props) => {
const { formatMessage } = useIntl();
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const {
setValidationsSuccess,
setValidationsFailure,
validationApiRateLimited,
clearRegistrationBackendError,
} = useRegisterContext();
const fieldValidationsMutation = useFieldValidations({
onSuccess: (data) => {
setValidationsSuccess(data);
},
onError: () => {
setValidationsFailure();
},
});
const handleBlur = (e) => {
if (props.handleBlur) { props.handleBlur(e); }
const { name, value } = e.target;
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
return; // Do not run validations on password icon click
}
let passwordValue = value;
if (name === 'passwordIcon') {
// To validate actual password value when onBlur is triggered by focusing out the password icon
passwordValue = props.value;
}
if (props.handleBlur) {
props.handleBlur({
target: {
name: props.name,
value: passwordValue,
},
});
}
setShowTooltip(props.showRequirements && false);
if (props.handleErrorChange) { // If rendering from register page
const fieldError = validatePasswordField(passwordValue, formatMessage);
if (fieldError) {
props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) {
fieldValidationsMutation.mutate({ password: passwordValue });
}
}
};
const handleFocus = (e) => {
if (e.target?.name === 'passwordIcon') {
return; // Do not clear error on password icon focus
}
if (props.handleFocus) {
props.handleFocus(e);
}
if (props.handleErrorChange) {
props.handleErrorChange('password', '');
clearRegistrationBackendError('password');
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};
const HideButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
<IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={VisibilityOff}
iconAs={Icon}
onClick={setHiddenTrue}
size="sm"
variant="secondary"
alt={formatMessage(messages['hide.password'])}
/>
);
const ShowButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
<IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={Visibility}
iconAs={Icon}
onClick={setHiddenFalse}
size="sm"
variant="secondary"
alt={formatMessage(messages['show.password'])}
/>
);
const placement = window.innerWidth < 768 ? 'top' : 'left';
const tooltip = (
<Tooltip id={`password-requirement-${placement}`}>
@@ -76,7 +151,7 @@ const PasswordField = (props) => {
{props.errorMessage !== '' && (
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
{props.errorMessage}
<span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>
{props.showScreenReaderText && <span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>}
</Form.Control.Feedback>
)}
</Form.Group>
@@ -89,7 +164,9 @@ PasswordField.defaultProps = {
handleBlur: null,
handleFocus: null,
handleChange: () => {},
handleErrorChange: null,
showRequirements: true,
showScreenReaderText: true,
autoComplete: null,
};
@@ -100,10 +177,12 @@ PasswordField.propTypes = {
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
handleChange: PropTypes.func,
handleErrorChange: PropTypes.func,
name: PropTypes.string.isRequired,
showRequirements: PropTypes.bool,
value: PropTypes.string.isRequired,
autoComplete: PropTypes.string,
showScreenReaderText: PropTypes.bool,
};
export default PasswordField;

View File

@@ -9,6 +9,7 @@ import { setCookie } from '../data/utils';
const RedirectLogistration = (props) => {
const {
authenticatedUser,
finishAuthUrl,
redirectUrl,
redirectToProgressiveProfilingPage,
@@ -21,7 +22,6 @@ const RedirectLogistration = (props) => {
host,
} = props;
let finalRedirectUrl = '';
if (success) {
// 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.
@@ -52,6 +52,7 @@ const RedirectLogistration = (props) => {
state={{
registrationResult,
optionalFields,
authenticatedUser,
}}
replace
/>
@@ -81,6 +82,7 @@ const RedirectLogistration = (props) => {
};
RedirectLogistration.defaultProps = {
authenticatedUser: {},
educationLevel: null,
finishAuthUrl: null,
success: false,
@@ -94,6 +96,7 @@ RedirectLogistration.defaultProps = {
};
RedirectLogistration.propTypes = {
authenticatedUser: PropTypes.shape({}),
educationLevel: PropTypes.string,
finishAuthUrl: PropTypes.string,
success: PropTypes.bool,

View File

@@ -1,10 +1,8 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { Login } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Icon } from '@openedx/paragon';
import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';

View File

@@ -1,18 +1,22 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Hyperlink, Icon,
} from '@openedx/paragon';
import { Institution } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Skeleton from 'react-loading-skeleton';
import messages from './messages';
import {
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
RenderInstitutionButton,
SocialAuthProviders,
} from '../common-components';
import {
PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
} from './index';
/**
* This component renders the Single sign-on (SSO) buttons for the providers passed.
@@ -20,33 +24,60 @@ import {
const ThirdPartyAuth = (props) => {
const { formatMessage } = useIntl();
const {
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus,
providers,
secondaryProviders,
currentProvider,
handleInstitutionLogin,
thirdPartyAuthApiStatus,
isLoginPage,
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return (
<>
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
<div className="mt-4 mb-3 h4">
{formatMessage(messages['registration.other.options.heading'])}
{isLoginPage
? formatMessage(messages['login.other.options.heading'])
: formatMessage(messages['registration.other.options.heading'])}
</div>
)}
{(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
<Hyperlink
className={classNames(
'btn btn-link btn-sm text-body p-0',
{ 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
{ 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
)}
destination={enterpriseLoginURL}
>
<Icon src={Institution} className="institute-icon" />
{formatMessage(messages['enterprise.login.btn.text'])}
</Hyperlink>
)}
{thirdPartyAuthApiStatus === PENDING_STATE ? (
<Skeleton className="tpa-skeleton" height={36} count={2} />
{thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
<div className="mt-4">
<Skeleton className="tpa-skeleton" height={36} count={2} />
</div>
) : (
<>
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
<RenderInstitutionButton
onSubmitHandler={handleInstitutionLogin}
buttonTitle={formatMessage(messages['register.institution.login.button'])}
buttonTitle={formatMessage(messages['institution.login.button'])}
/>
)}
{isSocialAuthActive && (
<div className="row m-0">
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
<SocialAuthProviders
socialAuthProviders={providers}
referrer={isLoginPage ? LOGIN_PAGE : REGISTER_PAGE}
/>
</div>
)}
</>
@@ -59,7 +90,8 @@ ThirdPartyAuth.defaultProps = {
currentProvider: null,
providers: [],
secondaryProviders: [],
thirdPartyAuthApiStatus: 'pending',
thirdPartyAuthApiStatus: PENDING_STATE,
isLoginPage: false,
};
ThirdPartyAuth.propTypes = {
@@ -86,6 +118,7 @@ ThirdPartyAuth.propTypes = {
}),
),
thirdPartyAuthApiStatus: PropTypes.string,
isLoginPage: PropTypes.bool,
};
export default ThirdPartyAuth;

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { Alert } from '@openedx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';

View File

@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext';
const TestComponent = () => {
const {
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
} = useThirdPartyAuthContext();
return (
<div>
<div>{fieldDescriptions ? 'FieldDescriptions Available' : 'FieldDescriptions Not Available'}</div>
<div>{optionalFields ? 'OptionalFields Available' : 'OptionalFields Not Available'}</div>
<div>{thirdPartyAuthApiStatus !== null ? 'AuthApiStatus Available' : 'AuthApiStatus Not Available'}</div>
<div>{thirdPartyAuthContext ? 'AuthContext Available' : 'AuthContext Not Available'}</div>
</div>
);
};
describe('ThirdPartyAuthContext', () => {
it('should render children', () => {
render(
<ThirdPartyAuthProvider>
<div>Test Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('should provide all context values to children', () => {
render(
<ThirdPartyAuthProvider>
<TestComponent />
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('FieldDescriptions Available')).toBeInTheDocument();
expect(screen.getByText('OptionalFields Available')).toBeInTheDocument();
expect(screen.getByText('AuthApiStatus Not Available')).toBeInTheDocument(); // Initially null
expect(screen.getByText('AuthContext Available')).toBeInTheDocument();
});
it('should render multiple children', () => {
render(
<ThirdPartyAuthProvider>
<div>First Child</div>
<div>Second Child</div>
<div>Third Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('First Child')).toBeInTheDocument();
expect(screen.getByText('Second Child')).toBeInTheDocument();
expect(screen.getByText('Third Child')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,133 @@
import {
createContext, FC, ReactNode, useCallback, useContext, useMemo, useState,
} from 'react';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
interface ThirdPartyAuthContextType {
fieldDescriptions: any;
optionalFields: {
fields: any;
extended_profile: any[];
};
thirdPartyAuthApiStatus: string | null;
thirdPartyAuthContext: {
platformName: string | null;
autoSubmitRegForm: boolean;
currentProvider: string | null;
finishAuthUrl: string | null;
countryCode: string | null;
providers: any[];
secondaryProviders: any[];
pipelineUserDetails: any | null;
errorMessage: string | null;
welcomePageRedirectUrl: string | null;
};
setThirdPartyAuthContextBegin: () => void;
setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void;
setThirdPartyAuthContextFailure: () => void;
clearThirdPartyAuthErrorMessage: () => void;
}
const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined);
interface ThirdPartyAuthProviderProps {
children: ReactNode;
}
export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => {
const [fieldDescriptions, setFieldDescriptions] = useState({});
const [optionalFields, setOptionalFields] = useState({
fields: {},
extended_profile: [],
});
const [thirdPartyAuthApiStatus, setThirdPartyAuthApiStatus] = useState<string | null>(null);
const [thirdPartyAuthContext, setThirdPartyAuthContext] = useState({
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
// Function to handle begin state - mirrors THIRD_PARTY_AUTH_CONTEXT.BEGIN
const setThirdPartyAuthContextBegin = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
}, []);
// Function to handle success - mirrors THIRD_PARTY_AUTH_CONTEXT.SUCCESS
const setThirdPartyAuthContextSuccess = useCallback((fieldDescData, optionalFieldsData, contextData) => {
setFieldDescriptions(fieldDescData?.fields || {});
setOptionalFields(optionalFieldsData || { fields: {}, extended_profile: [] });
setThirdPartyAuthContext(contextData || {
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
setThirdPartyAuthApiStatus(COMPLETE_STATE);
}, []);
// Function to handle failure - mirrors THIRD_PARTY_AUTH_CONTEXT.FAILURE
const setThirdPartyAuthContextFailure = useCallback(() => {
setThirdPartyAuthApiStatus(FAILURE_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
// Function to clear error message - mirrors THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG
const clearThirdPartyAuthErrorMessage = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
const value = useMemo(() => ({
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
}), [
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
]);
return (
<ThirdPartyAuthContext.Provider value={value}>
{children}
</ThirdPartyAuthContext.Provider>
);
};
export const useThirdPartyAuthContext = (): ThirdPartyAuthContextType => {
const context = useContext(ThirdPartyAuthContext);
if (context === undefined) {
throw new Error('useThirdPartyAuthContext must be used within a ThirdPartyAuthProvider');
}
return context;
};

View File

@@ -1,27 +0,0 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
export const clearThirdPartyAuthContextErrorMessage = () => ({
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
});

View File

@@ -1,8 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
const getThirdPartyAuthContext = async (urlParams : string) => {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
@@ -13,13 +12,14 @@ export async function getThirdPartyAuthContext(urlParams) {
.get(
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
requestConfig,
)
.catch((e) => {
throw (e);
});
);
return {
fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || {},
};
}
};
export {
getThirdPartyAuthContext,
};

View File

@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { getThirdPartyAuthContext } from './api';
import { ThirdPartyAuthQueryKeys } from './queryKeys';
// Error constants
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';
const useThirdPartyAuthHook = (pageId, payload) => useQuery({
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId),
queryFn: () => getThirdPartyAuthContext(payload),
retry: false,
});
export {
useThirdPartyAuthHook,
};

View File

@@ -0,0 +1,6 @@
import { appId } from '../../constants';
export const ThirdPartyAuthQueryKeys = {
all: [appId, 'ThirdPartyAuth'] as const,
byPage: (pageId: string) => [appId, 'ThirdPartyAuth', pageId] as const,
};

View File

@@ -1,62 +0,0 @@
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
},
};
const reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
return {
...state,
fieldDescriptions: action.payload.fieldDescriptions.fields,
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: FAILURE_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
default:
return state;
}
};
export default reducer;

View File

@@ -1,32 +0,0 @@
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure,
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import {
getThirdPartyAuthContext,
} from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

@@ -1,28 +0,0 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const optionalFieldsSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.optionalFields,
);
export const tpaProvidersSelector = createSelector(
commonComponentsSelector,
commonComponents => ({
providers: commonComponents.thirdPartyAuthContext.providers,
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
}),
);

View File

@@ -1,82 +0,0 @@
import { PENDING_STATE } from '../../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
import reducer from '../reducers';
describe('common components reducer', () => {
it('test mfe context response', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
},
};
const fieldDescriptions = {
fields: [],
};
const optionalFields = {
fields: [],
extended_profile: {},
};
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
const action = {
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
};
expect(
reducer(state, action),
).toEqual(
{
...state,
fieldDescriptions: [],
optionalFields: {
fields: [],
extended_profile: {},
},
thirdPartyAuthApiStatus: 'complete',
},
);
});
it('should clear tpa context error message', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: 'An error occured',
},
};
const action = {
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
};
expect(
reducer(state, action),
).toEqual(
{
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
},
);
});
});

View File

@@ -1,71 +0,0 @@
import { runSaga } from 'redux-saga';
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
}));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});

View File

@@ -7,9 +7,6 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
export { default as InstitutionLogistration } from './InstitutionLogistration';
export { RenderInstitutionButton } from './InstitutionLogistration';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField';
export { default as Zendesk } from './Zendesk';

View File

@@ -112,6 +112,26 @@ const messages = defineMessages({
description: 'Select ticket form',
defaultMessage: 'Please choose your request type:',
},
'registration.other.options.heading': {
id: 'registration.other.options.heading',
defaultMessage: 'Or register with:',
description: 'A message that appears above third party auth providers i.e saml, google, facebook etc',
},
'institution.login.button': {
id: 'institution.login.button',
defaultMessage: 'Institution/campus credentials',
description: 'shows institutions list',
},
'login.other.options.heading': {
id: 'login.other.options.heading',
defaultMessage: 'Or sign in with:',
description: 'Text that appears above other sign in options like social auth buttons',
},
'enterprise.login.btn.text': {
id: 'enterprise.login.btn.text',
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
});
export default messages;

View File

@@ -3,16 +3,15 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
const RRD = require('react-router-dom');
// Just render plain div with its children
// eslint-disable-next-line react/prop-types
@@ -27,6 +26,10 @@ const TestApp = () => (
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
/>
<Route
path={PAGE_NOT_FOUND}
element={<span>Page not found</span>}
/>
</Routes>
</div>
</Router>
@@ -45,12 +48,13 @@ describe('EmbeddedRegistrationRoute', () => {
it('should not render embedded register page if host query param is not available in the url', async () => {
let embeddedRegistrationPage = null;
await act(async () => {
embeddedRegistrationPage = await mount(routerWrapper());
const { container } = await render(routerWrapper());
embeddedRegistrationPage = container;
});
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
const renderedPage = embeddedRegistrationPage.querySelector('span');
expect(renderedPage.textContent).toBe('Page not found');
});
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
@@ -61,12 +65,13 @@ describe('EmbeddedRegistrationRoute', () => {
};
let embeddedRegistrationPage = null;
await act(async () => {
embeddedRegistrationPage = await mount(routerWrapper());
const { container } = await render(routerWrapper());
embeddedRegistrationPage = container;
});
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
const renderedPage = embeddedRegistrationPage.querySelector('span');
expect(renderedPage).toBeTruthy();
expect(renderedPage.textContent).toBe('Embedded Register Page');
});
});

View File

@@ -1,12 +1,21 @@
import React from 'react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render } from '@testing-library/react';
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 PasswordField from '../PasswordField';
// Mock the useFieldValidations hook
jest.mock('../../register/data/apiHook', () => ({
useFieldValidations: jest.fn(),
}));
describe('FormGroup', () => {
const props = {
floatingLabel: 'Email',
@@ -17,47 +26,89 @@ describe('FormGroup', () => {
};
it('should show help text on field focus', () => {
const formGroup = mount(<FormGroup {...props} />);
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').exists()).toBeFalsy();
const { queryByText, getByLabelText } = render(<FormGroup {...props} />);
const emailInput = getByLabelText('Email');
formGroup.find('input#email').simulate('focus');
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').text()).toEqual('Email field help text');
expect(queryByText('Email field help text')).toBeNull();
fireEvent.focus(emailInput);
const helpText = queryByText('Email field help text');
expect(helpText).toBeTruthy();
expect(helpText.textContent).toEqual('Email field help text');
});
});
describe('PasswordField', () => {
const IntlPasswordField = injectIntl(PasswordField);
let props = {};
let queryClient;
let mockMutate;
const renderWrapper = (children) => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<RegisterProvider>
{children}
</RegisterProvider>
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
);
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
mockMutate = jest.fn();
useFieldValidations.mockReturnValue({
mutate: mockMutate,
isPending: false,
});
props = {
floatingLabel: 'Password',
name: 'password',
value: 'password123',
handleFocus: jest.fn(),
};
jest.clearAllMocks();
});
it('should show/hide password on icon click', () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
passwordField.find('button[aria-label="Show password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('text');
const showPasswordButton = getByLabelText('Show password');
fireEvent.click(showPasswordButton);
expect(passwordInput.type).toBe('text');
passwordField.find('button[aria-label="Hide password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('password');
const hidePasswordButton = getByLabelText('Hide password');
fireEvent.click(hidePasswordButton);
expect(passwordInput.type).toBe('password');
});
it('should show password requirement tooltip on focus', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
fireEvent.focus(passwordInput);
jest.runAllTimers();
});
passwordField.update();
const passwordRequirementTooltip = document.querySelector('#password-requirement-left');
expect(passwordField.find('#password-requirement-left').exists()).toBeTruthy();
expect(passwordRequirementTooltip).toBeTruthy();
});
it('should show all password requirement checks as failed', async () => {
@@ -65,31 +116,193 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
await act(async () => {
passwordField.find('input').simulate('focus');
fireEvent.focus(passwordInput);
jest.runAllTimers();
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
const letterCheckIcon = document.querySelector('#letter-check span');
const numberCheckIcon = document.querySelector('#number-check span');
const charactersCheckIcon = document.querySelector('#characters-check span');
expect(letterCheckIcon).toBeTruthy();
expect(letterCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
expect(numberCheckIcon).toBeTruthy();
expect(numberCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
expect(charactersCheckIcon).toBeTruthy();
expect(charactersCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
});
it('should update password requirement checks', async () => {
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
fireEvent.focus(passwordInput);
jest.runAllTimers();
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
const letterCheckIcon = document.querySelector('#letter-check span');
const numberCheckIcon = document.querySelector('#number-check span');
const charactersCheckIcon = document.querySelector('#characters-check span');
expect(letterCheckIcon).toBeTruthy();
expect(letterCheckIcon.className).toContain('pgn__icon text-success mr-1');
expect(numberCheckIcon).toBeTruthy();
expect(numberCheckIcon.className).toContain('pgn__icon text-success mr-1');
expect(charactersCheckIcon).toBeTruthy();
expect(charactersCheckIcon.className).toContain('pgn__icon text-success mr-1');
});
it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password');
fireEvent.blur(passwordInput, {
target: {
name: 'password',
value: 'invalid',
},
relatedTarget: passwordIcon,
});
expect(container.querySelector('div[feedback-for="password"]')).toBeNull();
});
it('should call props handle blur if available', () => {
props = {
...props,
handleBlur: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
target: {
name: 'password',
value: '',
},
});
expect(props.handleBlur).toHaveBeenCalledTimes(1);
});
it('should run validations on blur event when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
target: {
name: 'password',
value: '',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'password',
'Password criteria has not been met',
);
});
it('should not clear error when focus is fired on password icon click when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
fireEvent.focus(passwordIcon, {
target: {
name: 'passwordIcon',
value: '',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
});
it('should clear error when focus is fired on password icon click when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
fireEvent.focus(passwordIcon, {
target: {
name: 'password',
value: 'invalid',
},
});
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'password',
'',
);
});
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
props = {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
name: 'password',
value: 'password123',
},
});
expect(mockMutate).toHaveBeenCalledWith({ password: 'password123' });
});
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
props = {
...props,
value: 'testPassword',
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
fireEvent.blur(passwordIcon, {
target: {
name: 'passwordIcon',
value: undefined,
},
});
expect(props.handleBlur).toHaveBeenCalledTimes(1);
expect(props.handleBlur).toHaveBeenCalledWith({
target: {
name: 'password',
value: 'testPassword',
},
});
});
});

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { REGISTER_PAGE } from '../../data/constants';
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
describe('ThirdPartyAuthAlert', () => {
@@ -38,4 +36,19 @@ describe('ThirdPartyAuthAlert', () => {
).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders skeleton for pending third-party auth', () => {
props = {
...props,
thirdPartyAuthApiStatus: PENDING_STATE,
isThirdPartyAuthActive: true,
};
const tree = renderer.create(
<IntlProvider locale="en">
<ThirdPartyAuthAlert {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,18 +1,15 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(),
@@ -55,7 +52,7 @@ describe('UnAuthOnlyRoute', () => {
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
await act(async () => {
await mount(routerWrapper());
await render(routerWrapper());
});
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
@@ -66,7 +63,7 @@ describe('UnAuthOnlyRoute', () => {
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
await act(async () => {
await mount(routerWrapper());
await render(routerWrapper());
});
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });

View File

@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
style={{}}
viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor"
style={Object {}}
style={{}}
/>
</svg>
</div>
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
`;
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
Array [
[
<button
className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"

View File

@@ -1,5 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThirdPartyAuthAlert renders skeleton for pending third-party auth 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
id="tpa-alert"
role="alert"
>
<div
className="pgn__alert-message-wrapper"
>
<div
className="alert-message-content"
>
<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.
</p>
</div>
</div>
</div>
`;
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
@@ -21,7 +41,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
`;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
Array [
[
<div
className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert"

View File

@@ -5,23 +5,23 @@ exports[`Zendesk Help should match login page third party auth alert message sna
cookies={true}
defer={true}
webWidget={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"title": {
"*": "edX Support",
},
},
"chat": Object {
"departments": Object {
"enabled": Array [
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
"suppress": false,
},
"contactForm": Object {
"contactForm": {
"attachments": true,
"selectTicketForm": Object {
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": Object {
"prefill": {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`Zendesk Help should match login page third party auth alert message sna
},
],
},
"contactOptions": Object {
"contactOptions": {
"enabled": false,
},
"helpCenter": Object {
"helpCenter": {
"originalArticleButton": true,
},
}

View File

@@ -1,14 +1,17 @@
const configuration = {
// Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
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_POPULAR_AND_TRENDING_RECOMMENDATIONS: process.env.ENABLE_POPULAR_AND_TRENDING_RECOMMENDATIONS || 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,
@@ -26,12 +29,12 @@ const configuration = {
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
// Recommendation constants
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
POPULAR_PRODUCTS: process.env.POPULAR_PRODUCTS || '[]',
TRENDING_PRODUCTS: process.env.TRENDING_PRODUCTS || '[]',
// 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;

1
src/constants.ts Normal file
View File

@@ -0,0 +1 @@
export const appId = 'org.openedx.frontend.app.authn';

20
src/data/algolia.js Normal file
View File

@@ -0,0 +1,20 @@
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

@@ -1,33 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { composeWithDevTools } from '@redux-devtools/extension';
import { applyMiddleware, compose, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import createRootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
function composeMiddleware() {
if (getConfig().ENVIRONMENT === 'development') {
const loggerMiddleware = createLogger({
collapsed: true,
});
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
}
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
}
export default function configureStore(initialState = {}) {
const store = createStore(
createRootReducer(),
initialState,
composeMiddleware(),
);
sagaMiddleware.run(rootSaga);
return store;
}

View File

@@ -26,14 +26,12 @@ export const FAILURE_STATE = 'failure';
export const FORBIDDEN_STATE = 'forbidden';
export const EMBEDDED = 'embedded';
// Regex
export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/;
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
+ '|^"([\\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})'
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/;
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
// Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration.

3
src/data/oneTrust.js Normal file
View File

@@ -0,0 +1,3 @@
const isOneTrustFunctionalCookieEnabled = () => !!window?.OnetrustActiveGroups?.includes('C0003');
export default isOneTrustFunctionalCookieEnabled;

View File

@@ -1,36 +0,0 @@
import { combineReducers } from 'redux';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
} from '../common-components';
import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
} from '../forgot-password';
import {
reducer as loginReducer,
storeName as loginStoreName,
} from '../login';
import {
reducer as authnProgressiveProfilingReducers,
storeName as authnProgressiveProfilingStoreName,
} from '../progressive-profiling';
import {
reducer as registerReducer,
storeName as registerStoreName,
} from '../register';
import {
reducer as resetPasswordReducer,
storeName as resetPasswordStoreName,
} from '../reset-password';
const createRootReducer = () => combineReducers({
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

View File

@@ -1,19 +0,0 @@
import { all } from 'redux-saga/effects';
import { saga as commonComponentsSaga } from '../common-components';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as loginSaga } from '../login';
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
import { saga as registrationSaga } from '../register';
import { saga as resetPasswordSaga } from '../reset-password';
export default function* rootSaga() {
yield all([
loginSaga(),
registrationSaga(),
commonComponentsSaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
authnProgressiveProfilingSaga(),
]);
}

View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,52 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie';
import { setCookie } from '../utils';
// Mock getConfig function
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
// Mock Cookies class
jest.mock('universal-cookie');
describe('setCookie function', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should set a cookie with default options', () => {
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
setCookie('testCookie', 'testValue');
expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith();
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
domain: 'example.com',
path: '/',
});
});
it('should set a cookie with specified expiry', () => {
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
const expiry = new Date('2023-12-31');
setCookie('testCookie', 'testValue', expiry);
expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith();
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
domain: 'example.com',
path: '/',
expires: expiry,
});
});
it('should not set a cookie if cookieName is undefined', () => {
setCookie(undefined, 'testValue');
expect(Cookies).not.toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,5 @@
import { updatePathWithQueryParams } from './dataUtils';
import { LOGIN_PAGE } from '../constants';
import { updatePathWithQueryParams } from '../utils/dataUtils';
describe('updatePathWithQueryParams', () => {
it('should append query params into the path', () => {

View File

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

View File

@@ -7,5 +7,4 @@ export {
updatePathWithQueryParams,
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies';

View File

@@ -1,34 +0,0 @@
/**
* Helper class to save time when writing out action types for asynchronous methods. Also helps
* ensure that actions are namespaced.
*/
export default class AsyncActionType {
constructor(topic, name) {
this.topic = topic;
this.name = name;
}
get BASE() {
return `${this.topic}__${this.name}`;
}
get BEGIN() {
return `${this.topic}__${this.name}__BEGIN`;
}
get SUCCESS() {
return `${this.topic}__${this.name}__SUCCESS`;
}
get FAILURE() {
return `${this.topic}__${this.name}__FAILURE`;
}
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
get FORBIDDEN() {
return `${this.topic}__${this.name}__FORBIDDEN`;
}
}

View File

@@ -1,14 +0,0 @@
import AsyncActionType from './reduxUtils';
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
});
});

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { breakpoints } from '@edx/paragon';
import { breakpoints } from '@openedx/paragon';
/**
* A react hook used to determine if the current window is mobile or not.

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
import { Form, Icon } from '@openedx/paragon';
import { ExpandMore } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
const FormFieldRenderer = (props) => {

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { mount } from 'enzyme';
import { fireEvent, render } from '@testing-library/react';
import FieldRenderer from '../FieldRenderer';
@@ -28,13 +26,14 @@ describe('FieldRendererTests', () => {
options: [['1997', '1997'], ['1998', '1998']],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('select#yob-field');
field.simulate('change', { target: { value: 1997 } });
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const input = container.querySelector('select#yob-field');
const label = container.querySelector('label');
fireEvent.change(input, { target: { value: 1997 } });
expect(field.type()).toEqual('select');
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
expect(value).toEqual(1997);
expect(input.type).toEqual('select-one');
expect(label.textContent).toContain(fieldData.label);
expect(value).toEqual('1997');
});
it('should return null if no options are provided for select field', () => {
@@ -44,8 +43,8 @@ describe('FieldRendererTests', () => {
name: 'yob-field',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(container.innerHTML).toEqual('');
});
it('should render textarea field', () => {
@@ -55,12 +54,13 @@ describe('FieldRendererTests', () => {
name: 'goals-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#goals-field').last();
field.simulate('change', { target: { value: 'These are my goals.' } });
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const input = container.querySelector('#goals-field');
const label = container.querySelector('label');
fireEvent.change(input, { target: { value: 'These are my goals.' } });
expect(field.type()).toEqual('textarea');
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
expect(input.type).toEqual(fieldData.type);
expect(label.textContent).toContain('Why do you want to join this platform?');
expect(value).toEqual('These are my goals.');
});
@@ -71,12 +71,13 @@ describe('FieldRendererTests', () => {
name: 'company-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#company-field').last();
field.simulate('change', { target: { value: 'ABC' } });
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const input = container.querySelector('input#company-field');
const label = container.querySelector('label');
fireEvent.change(input, { target: { value: 'ABC' } });
expect(field.type()).toEqual('input');
expect(fieldRenderer.find('label').text()).toEqual('Company');
expect(input.type).toEqual(fieldData.type);
expect(label.textContent).toContain(fieldData.label);
expect(value).toEqual('ABC');
});
@@ -87,12 +88,13 @@ describe('FieldRendererTests', () => {
name: 'marketing-emails-opt-in-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const input = container.querySelector('input#marketing-emails-opt-in-field');
const label = container.querySelector('label');
fireEvent.click(input);
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
expect(input.type).toEqual(fieldData.type);
expect(label.textContent).toContain(fieldData.label);
expect(value).toEqual(true);
});
@@ -101,8 +103,8 @@ describe('FieldRendererTests', () => {
type: 'unknown',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(container.innerHTML).toContain('');
});
it('should run onBlur and onFocus functions for a field if given', () => {
@@ -117,7 +119,7 @@ describe('FieldRendererTests', () => {
functionValue = `${e.target.name} focussed`;
};
const fieldRenderer = mount(
const { container } = render(
<FieldRenderer
handleFocus={onFocus}
handleBlur={onBlur}
@@ -126,19 +128,19 @@ describe('FieldRendererTests', () => {
onChangeHandler={changeHandler}
/>,
);
const field = fieldRenderer.find('#test-field').last();
const input = container.querySelector('#test-field');
field.simulate('focus');
fireEvent.focus(input);
expect(functionValue).toEqual('test-field focussed');
field.simulate('blur');
fireEvent.blur(input);
expect(functionValue).toEqual('test-field blurred');
});
it('should render error message for required text fields', () => {
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
const fieldRenderer = mount(
const { container } = render(
<FieldRenderer
isRequired
fieldData={fieldData}
@@ -147,7 +149,7 @@ describe('FieldRendererTests', () => {
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Enter your first name');
});
it('should render error message for required select fields', () => {
@@ -155,7 +157,7 @@ describe('FieldRendererTests', () => {
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
};
const fieldRenderer = mount(
const { container } = render(
<FieldRenderer
isRequired
fieldData={fieldData}
@@ -164,13 +166,13 @@ describe('FieldRendererTests', () => {
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Select your preference');
});
it('should render error message for required textarea fields', () => {
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
const fieldRenderer = mount(
const { container } = render(
<FieldRenderer
isRequired
fieldData={fieldData}
@@ -179,13 +181,13 @@ describe('FieldRendererTests', () => {
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Tell us your goals');
});
it('should render error message for required checkbox fields', () => {
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
const fieldRenderer = mount(
const { container } = render(
<FieldRenderer
isRequired
fieldData={fieldData}
@@ -194,6 +196,6 @@ describe('FieldRendererTests', () => {
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('You must agree to our Honor Code');
});
});

View File

@@ -1,9 +1,7 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { CheckCircle, Error } from '@edx/paragon/icons';
import { Alert } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
@@ -43,7 +41,7 @@ const ForgotPasswordAlert = (props) => {
}}
/>
);
break;
break;
case INTERNAL_SERVER_ERROR:
message = formatMessage(messages['internal.server.error']);
break;

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -11,44 +10,41 @@ import {
StatefulButton,
Tab,
Tabs,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
} from '@openedx/paragon';
import { ChevronLeft } from '@openedx/paragon/icons';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import { useForgotPassword } from './data/apiHook';
import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages';
import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
const ForgotPasswordPage = (props) => {
const ForgotPasswordPage = () => {
const platformName = getConfig().SITE_NAME;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const {
status, submitState, emailValidationError,
} = props;
const { formatMessage } = useIntl();
const [email, setEmail] = useState(props.email);
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState('');
const [validationError, setValidationError] = useState(emailValidationError);
const navigate = useNavigate();
const [validationError, setValidationError] = useState('');
const [status, setStatus] = useState(location.state?.status || null);
// React Query hook for forgot password
const { mutate: sendForgotPassword, isPending: isSending } = useForgotPassword();
const submitState = isSending ? 'pending' : 'default';
useEffect(() => {
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {
setValidationError(emailValidationError);
}, [emailValidationError]);
useEffect(() => {
if (status === 'complete') {
setEmail('');
@@ -68,22 +64,38 @@ const ForgotPasswordPage = (props) => {
};
const handleBlur = () => {
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
setValidationError(getValidationMessage(email));
};
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
const handleFocus = () => {
setValidationError('');
};
const handleSubmit = (e) => {
e.preventDefault();
setBannerEmail(email);
const error = getValidationMessage(email);
if (error) {
setFormErrors(error);
props.setForgotPasswordFormData({ email, emailValidationError: error });
const validateError = getValidationMessage(email);
if (validateError) {
setFormErrors(validateError);
setValidationError(validateError);
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
} else {
props.forgotPassword(email);
setFormErrors('');
sendForgotPassword(email, {
onSuccess: (data, emailUsed) => {
setStatus('complete');
setBannerEmail(emailUsed);
setFormErrors('');
},
onError: (error) => {
if (error.response && error.response.status === 403) {
setStatus('forbidden');
} else {
setStatus('server-error');
}
},
});
}
};
@@ -153,7 +165,7 @@ const ForgotPasswordPage = (props) => {
)}
<p className="mt-5.5 small text-gray-700">
{formatMessage(messages['additional.help.text'], { platformName })}
<span>
<span className="mx-1">
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
</span>
</p>
@@ -164,26 +176,4 @@ const ForgotPasswordPage = (props) => {
);
};
ForgotPasswordPage.propTypes = {
email: PropTypes.string,
emailValidationError: PropTypes.string,
forgotPassword: PropTypes.func.isRequired,
setForgotPasswordFormData: PropTypes.func.isRequired,
status: PropTypes.string,
submitState: PropTypes.string,
};
ForgotPasswordPage.defaultProps = {
email: '',
emailValidationError: '',
status: null,
submitState: DEFAULT_STATE,
};
export default connect(
forgotPasswordResultSelector,
{
forgotPassword,
setForgotPasswordFormData,
},
)(ForgotPasswordPage);
export default ForgotPasswordPage;

View File

@@ -1,32 +0,0 @@
import { AsyncActionType } from '../../data/utils';
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
// Forgot Password
export const forgotPassword = email => ({
type: FORGOT_PASSWORD.BASE,
payload: { email },
});
export const forgotPasswordBegin = () => ({
type: FORGOT_PASSWORD.BEGIN,
});
export const forgotPasswordSuccess = email => ({
type: FORGOT_PASSWORD.SUCCESS,
payload: { email },
});
export const forgotPasswordForbidden = () => ({
type: FORGOT_PASSWORD.FORBIDDEN,
});
export const forgotPasswordServerError = () => ({
type: FORGOT_PASSWORD.FAILURE,
});
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
});

View File

@@ -0,0 +1,144 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
import { forgotPassword } from './api';
// Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('form-urlencoded', () => jest.fn());
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>;
describe('forgot-password api', () => {
const mockHttpClient = {
post: jest.fn(),
};
const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000',
};
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`);
});
describe('forgotPassword', () => {
const testEmail = 'test@example.com';
const expectedUrl = `${mockConfig.LMS_BASE_URL}/account/password`;
const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
it('should send forgot password request successfully', async () => {
const mockResponse = {
data: {
message: 'Password reset email sent successfully',
success: true,
},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: testEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: testEmail })}`,
expectedConfig
);
expect(result).toEqual(mockResponse.data);
});
it('should handle empty email address', async () => {
const emptyEmail = '';
const mockResponse = {
data: {
message: 'Email is required',
success: false,
}
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(emptyEmail);
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: emptyEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: emptyEmail })}`,
expectedConfig,
);
expect(result).toEqual(mockResponse.data);
});
it('should handle network errors without response', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockHttpClient.post.mockRejectedValueOnce(networkError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Network Error');
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
expect.any(String),
expectedConfig
);
});
it('should handle timeout errors', async () => {
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
mockHttpClient.post.mockRejectedValueOnce(timeoutError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Request timeout');
});
it('should handle response with no data field', async () => {
const mockResponse = {
// No data field
status: 200,
statusText: 'OK',
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toBeUndefined();
});
it('should return exactly the data field from response', async () => {
const expectedData = {
message: 'Password reset email sent successfully',
success: true,
timestamp: '2026-02-05T10:00:00Z',
};
const mockResponse = {
data: expectedData,
status: 200,
headers: {},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toEqual(expectedData);
expect(result).not.toHaveProperty('status');
expect(result).not.toHaveProperty('headers');
});
});
});

View File

@@ -2,8 +2,7 @@ import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
// eslint-disable-next-line import/prefer-default-export
export async function forgotPassword(email) {
const forgotPassword = async (email: string) => {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
@@ -20,4 +19,8 @@ export async function forgotPassword(email) {
});
return data;
}
};
export {
forgotPassword,
};

View File

@@ -0,0 +1,175 @@
import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import * as api from './api';
import { useForgotPassword } from './apiHook';
// Mock the logging functions
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
// Mock the API function
jest.mock('./api', () => ({
forgotPassword: jest.fn(),
}));
const mockForgotPassword = api.forgotPassword as jest.MockedFunction<typeof api.forgotPassword>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockLogInfo = logInfo as jest.MockedFunction<typeof logInfo>;
// Test wrapper component
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function TestWrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useForgotPassword', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isSuccess).toBe(false);
expect(result.current.error).toBe(null);
});
it('should send forgot password email successfully and log success', async () => {
const testEmail = 'test@example.com';
const mockResponse = {
message: 'Password reset email sent successfully',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
it('should handle 403 forbidden error and log as info', async () => {
const testEmail = 'blocked@example.com';
const mockError = {
response: {
status: 403,
data: {
detail: 'Too many password reset attempts',
},
},
message: 'Forbidden',
};
mockForgotPassword.mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogInfo).toHaveBeenCalledWith(mockError);
expect(mockLogError).not.toHaveBeenCalled();
expect(result.current.error).toEqual(mockError);
});
it('should handle network errors without response and log as error', async () => {
const testEmail = 'test@example.com';
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockForgotPassword.mockRejectedValueOnce(networkError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogError).toHaveBeenCalledWith(networkError);
expect(mockLogInfo).not.toHaveBeenCalled();
expect(result.current.error).toEqual(networkError);
});
it('should handle empty email address', async () => {
const testEmail = '';
const mockResponse = {
message: 'Email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith('');
});
it('should handle email with special characters', async () => {
const testEmail = 'user+test@example-domain.co.uk';
const mockResponse = {
message: 'Password reset email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
});

View File

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

View File

@@ -1,58 +0,0 @@
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
export const defaultState = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case FORGOT_PASSWORD.BEGIN:
return {
email: state.email,
status: 'pending',
submitState: PENDING_STATE,
};
case FORGOT_PASSWORD.SUCCESS:
return {
...defaultState,
status: 'complete',
};
case FORGOT_PASSWORD.FORBIDDEN:
return {
email: state.email,
status: 'forbidden',
};
case FORGOT_PASSWORD.FAILURE:
return {
email: state.email,
status: INTERNAL_SERVER_ERROR,
};
case PASSWORD_RESET_FAILURE:
return {
status: action.payload.errorCode,
};
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
const { forgotPasswordFormData } = action.payload;
return {
...state,
...forgotPasswordFormData,
};
}
default:
return {
...defaultState,
email: state.email,
emailValidationError: state.emailValidationError,
};
}
}
return state;
};
export default reducer;

View File

@@ -1,35 +0,0 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
// Actions
import {
FORGOT_PASSWORD,
forgotPasswordBegin,
forgotPasswordForbidden,
forgotPasswordServerError,
forgotPasswordSuccess,
} from './actions';
import { forgotPassword } from './service';
// Services
export function* handleForgotPassword(action) {
try {
yield put(forgotPasswordBegin());
yield call(forgotPassword, action.payload.email);
yield put(forgotPasswordSuccess(action.payload.email));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(forgotPasswordForbidden());
logInfo(e);
} else {
yield put(forgotPasswordServerError());
logError(e);
}
}
}
export default function* saga() {
yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword);
}

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