Compare commits

..

146 Commits

Author SHA1 Message Date
Ahtisham Shahid
fd02f8b3f4 Merge pull request #1571 from openedx/aansari/INF-2127
feat: added captcha v3 on registration
2025-09-17 20:00:47 +05:00
Awais Ansari
5ba6adcd7d refactor: fixed lint issue 2025-09-17 16:52:46 +05:00
Awais Ansari
527bc5aa37 test: added captcha hook test cases 2025-09-17 16:49:01 +05:00
Awais Ansari
fd5122bc10 refactor: improve code and test coverage 2025-09-17 16:03:15 +05:00
Awais Ansari
97c9a09efa feat: added captcha v3 on registration 2025-09-16 16:59:03 +05:00
Awais Ansari
cf3e8b5c7f Merge pull request #1569 from openedx/aansari/sync-with-2u-main-branch-demo
chore: sync 2u-main with master
2025-09-15 17:34:59 +05:00
Awais Ansari
764d5f51e1 test: resolved test conflicts 2025-09-15 17:22:19 +05:00
Awais Ansari
c4ef3dbbd9 Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/sync-with-2u-main-branch-demo 2025-09-15 17:14:56 +05: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
Hassan Raza
a552d025b6 chore: Handle forbidden username exceptions on registration (#1545) (#1550) 2025-08-22 18:43:06 +05: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
Awais Ansari
2cb62ca6d4 chore: sync 2u-main with master (#1520)
* fix(deps): update dependency @edx/frontend-platform to v8.3.2 (#1454)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency babel-plugin-formatjs to v10.5.36 (#1455)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1456)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency algoliasearch-helper to v3.24.3 (#1457)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @openedx/frontend-build to v14.3.3 (#1459)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.16.1 (#1460)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update `@openedx` dependencies to versions that support React 18 (#1458)

* docs: Update migrated edx.rtd links to docs.openedx.org

* docs: Add instructions on using Tutor for development

* chore(deps): update dependency babel-plugin-formatjs to v10.5.37 (#1461)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.16.2 (#1462)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1464)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix: properly set background color for floating labels (#1468)

* fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1471)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: upgrade to react 18 (#1466)

* chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#1474)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* 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

* chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1477)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency algoliasearch-helper to v3.25.0 (#1478)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency core-js to v3.42.0 (#1479)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency babel-plugin-formatjs to v10.5.38 (#1480)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.5 (#1481)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1482)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1484)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update react-router monorepo to v6.30.1 (#1486)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.18.0 (#1487)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.18.1 (#1488)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#1489)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.18.2 (#1490)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.20.0 (#1491)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1492)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency core-js to v3.43.0 (#1493)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @edx/frontend-platform to v8.4.0 (#1494)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency jest to v30 (#1495)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.20.1 (#1496)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency babel-plugin-formatjs to v10.5.39 (#1499)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v22.20.2 (#1500)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency algoliasearch-helper to v3.26.0 (#1501)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1503)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* 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>

* chore(deps): update dependency jest to v30.0.1 (#1506)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency jest to v30.0.2 (#1507)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency algoliasearch to v4.25.0 (#1508)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency eslint-plugin-import to v2.32.0 (#1509)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency algoliasearch to v4.25.2 (#1510)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency jest to v30.0.3 (#1513)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v23.13.0 (#1514)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency jest to v30.0.4 (#1516)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/paragon to v23.14.0 (#1517)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix: component destory issue after react 18 upgrade

* fix: fixed lint issue

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
Co-authored-by: sarina <sarina@axim.org>
Co-authored-by: Régis Behmo <regis@behmo.com>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
Co-authored-by: eemaanamir <eemaan.amir@gmail.com>
2025-07-09 12:05:30 +05: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
ayesha waris
f3f14fb3e7 fix: fixed cohesion page url setup (#1476)
Co-authored-by: Ayesha Waris <ayesha.waris@A006-01000.local>
2025-04-23 20:25:22 +05:00
ayesha waris
6c8b3835b6 fix: fixed cohesion pageview (#1475)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.17>
2025-04-23 13:17:14 +05:00
sundasnoreen12
8cbe6ce02e Merge pull request #1472 from openedx/sundas/upgrade-frontend-build
fix: properly set background color for floating labels
2025-04-03 17:05:24 +05:00
sundasnoreen12
a53334d3bf fix: properly set background color for floating labels 2025-04-03 16:48:53 +05:00
sundasnoreen12
0172c79dd9 Merge pull request #1465 from openedx/sundas/paragon-upgrade
feat: upgraded package for paragon
2025-03-28 14:17:25 +05:00
sundasnoreen12
5d4abcbab3 feat: upgraded package for paragon 2025-03-28 13:16:34 +05:00
sundasnoreen12
c9987eb2f4 Merge pull request #1425 from openedx/sundas-new/INF-1779
feat: implemented restricted country implementation
2025-03-20 12:59:23 +05:00
sundasnoreen12
a2ad9d5248 refactor: optimize code 2025-03-20 12:52:58 +05:00
sundasnoreen12
cca87bd16a fix: fixed issue by removing empty array 2025-03-20 12:52:50 +05:00
sundasnoreen12
206c4c887b test: refactor test file 2025-03-20 12:52:50 +05:00
sundasnoreen12
09dc21eb0e feat: implemented restricted country implementation 2025-03-20 12:52:50 +05:00
ayesha waris
945af2bdfd fix: fixed tagular console error if cohesion script is not loaded (#1452)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.3>
2025-03-19 23:31:59 +05:00
Awais Ansari
0dca6f3fdc Merge pull request #1451 from openedx/aansari/sync-with-master
chore: sync with master
2025-03-18 13:11:41 +05:00
Awais Ansari
189a67c9dc Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/sync-with-master 2025-03-18 12:58:16 +05:00
sundasnoreen12
3dceb63b9c Merge pull request #1445 from openedx/sundas/INF-1819
fix: blocked web crawlers for auth MFE
2025-03-10 12:25:26 +05:00
ayesha waris
9385174b93 fix: adding defer to cohesion script (#1447)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-06 13:43:09 +05:00
sundasnoreen12
86ed8e2361 fix: blocked web crawlers for auth MFE 2025-03-05 15:27:09 +05:00
ayesha waris
f5a6ece6b1 refactor: moved updated script to index.html (#1443)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-05 13:44:49 +05:00
ayesha waris
0d71e31ffb feat: update cohesion script (#1441)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-04 14:47:29 +05:00
Awais Ansari
38dd2944b8 Revert "feat: updated cohesion snippet (#1433)" (#1437)
This reverts commit 244b9e68e6.
2025-02-28 16:01:29 +05:00
Awais Ansari
f4708ed274 Revert "fix: removed style tag for updated cohesion script (#1434)" (#1438)
This reverts commit cb7300441c.
2025-02-28 16:01:16 +05:00
ayesha waris
cb7300441c fix: removed style tag for updated cohesion script (#1434)
Co-authored-by: Ayesha Waris <ayesha.waris@A006-01000.local>
2025-02-27 18:39:55 +05:00
ayesha waris
244b9e68e6 feat: updated cohesion snippet (#1433)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.37>
2025-02-27 16:34:56 +05:00
Awais Ansari
c04ed9aa43 feat: updated cohesion script (#1400) 2024-12-23 21:18:01 +05:00
ayesha waris
354c73bb2a fix: fixed account activation error message (#1384) 2024-11-29 18:28:23 +05:00
ayesha waris
76a5a5dffa fix: fixed event data (#1383) 2024-11-29 16:30:23 +05:00
Eemaan Amir
5efe9d8344 Merge pull request #1382 from openedx/inf-universal-cookie-update
chore: rebase 2u-main
2024-11-28 12:57:37 +05:00
ayesha waris
cd2003921b fix: fixed duplicate event firing on post SSO login registration (#1381) 2024-11-27 18:56:03 +05:00
Eemaan Amir
7339aec7c2 Merge pull request #1380 from openedx/inf-reselect-update
chore: rebase 2u-main
2024-11-27 14:20:46 +05:00
Eemaan Amir
025870a3b9 Merge pull request #1378 from openedx/inf-checkout-action-update
chore: rebase 2u-main
2024-11-22 13:12:32 +05:00
Eemaan Amir
8dc77d5db6 Merge pull request #1377 from openedx/inf-github-action-update
chore: rebase 2u-main
2024-11-21 15:14:51 +05:00
Eemaan Amir
40a1f4ce6b Merge pull request #1373 from openedx/inf-codecov-action-update
chore: rebase 2u-main
2024-11-19 19:18:40 +05:00
Eemaan Amir
bb9fcd91c0 Merge pull request #1372 from openedx/inf-husky-update
chore: rebase 2u-main
2024-11-19 16:40:26 +05:00
ayesha waris
0fa00290da fix: fixed cohesion snippet loading (#1366) 2024-11-13 17:45:20 +05:00
Eemaan Amir
b18caa2da0 Merge pull request #1363 from openedx/inf-algoliasearch-helper-update
Rebase with 2u-main
2024-11-11 19:02:34 +05:00
ayesha waris
5ca86f9183 perf: updated cohesion id stitching script (#1358) 2024-11-11 17:17:58 +05:00
ayesha waris
2a9dbe9d30 fix: delaying nudge password redirection (#1337)
* fix: delaying  nudge password redirection

* fix: fixed test cases
2024-10-24 14:41:35 +05:00
ayesha waris
62508e3bc7 fix: fixed event aborting in firefox due to page redirect (#1336)
* fix: fixed event aborting in firefox due to page redirect

* fix: fixed test cases

* refactor: created a util for redirect
2024-10-23 20:00:17 +05:00
ayesha waris
ceb489753b feat: added update cohesion script (#1335) 2024-10-22 15:11:11 +05:00
ayesha waris
5035a07e0a fix: fixed nudge password edge case (#1334) 2024-10-19 00:30:40 +05:00
Awais Ansari
f086a165e2 fix: removed duplicate registration events (#1333) 2024-10-18 20:01:26 +05:00
Awais Ansari
9239df3620 fix: cohesion script and SSO issue (#1332)
* fix: userId error on script load

* fix: SSO cohesion event
2024-10-18 19:26:04 +05:00
Awais Ansari
009125c3ef fix: triggered login event on success (#1331)
* fix: triggered login event on success

* fix: fixed failing test cases

---------

Co-authored-by: ayeshoali <ayeshoali@gmail.com>
2024-10-18 18:28:40 +05:00
ayesha waris
b69ed6e422 fix: fixed opt out event text (#1330) 2024-10-18 14:42:28 +05:00
ayesha waris
07ee2392e9 feat: added cohesion events tracking (#1329)
* feat: added cohesion events tracking

* test: fixed failing test cases

* refactor: moved cohesion code into a folder

* refactor: fire event on successful form submission

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2024-10-17 20:28:25 +05:00
Awais Ansari
2bfce01772 Merge pull request #1328 from openedx/aansari/rebase-with-master
chore: rebase 2u main with master
2024-10-11 16:06:10 +05:00
Awais Ansari
1477ed33d7 Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/rebase-with-master 2024-10-04 18:13:47 +05:00
Awais Ansari
c4f1a97316 build: updated webpack.prod.config.js (#1327)
* build: updated webpack.prod.config.js

* fix: lint error
2024-09-25 17:16:32 +05:00
Awais Ansari
47b0501e1c feat: add MainAppSlot for chatbot plugin (#1320)
* feat: add MainAppSlot for chatbot plugin

* test: added test for MainAppSlot

* chore: add read me for plugin-slot
2024-09-25 13:58:01 +05:00
Muhammad Abdullah Waheed
e496bb62c5 Merge pull request #1318 from openedx/manwar/update-2u-main
refactor: sync 2u-main with master
2024-09-19 16:20:26 +05:00
Awais Ansari
b41fca3605 feat: removed Russian Federation from country list (#1315) 2024-09-12 10:01:48 +05:00
Mubbshar Anwar
ac2548913f fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-09-12 10:01:48 +05:00
Blue
cd9b3bd084 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-09-12 10:01:40 +05:00
Syed Sajjad Hussain Shah
efc07aac67 fix: fix datadog js errors (#1296) 2024-09-12 09:59:52 +05:00
Syed Sajjad Hussain Shah
2d50ed224f fix: retain query params in authenticated user redirection (#1288) 2024-09-12 09:59:52 +05:00
Mubbshar Anwar
d10f9b932b fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component

VAN-2013
2024-09-12 09:59:52 +05:00
Mubbshar Anwar
05aa85a5fb fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-09-12 09:59:52 +05:00
Syed Sajjad Hussain Shah
56bd6d835e fix: set marketing opt in in cookie for sso (#1285) 2024-09-12 09:59:52 +05:00
Muhammad Abdullah Waheed
afd4d24360 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-09-12 09:59:47 +05:00
Mubbshar Anwar
4898864416 feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

* fix: fix secondary provider null name issue
2024-09-12 09:56:53 +05:00
Mubbshar Anwar
739f94d624 Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled

* feat: remove username from the registration from (#1201) (#1241)

Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>

* fix: add new entry for another US label (#1244)

Add new entry for for another US label which is United States

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: add multi step registration eventing (#1226)

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

* feat: capture marketing lead in experiment events (#1243)

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

* feat: implement auto generated username experiment (#1248)

* feat: implement auto generated username registration exp

* feat: add page event for reset password (#1253)

Description: Add page event for reset password page
VAN-1929

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-09-12 09:56:53 +05:00
Blue
1819edc9b7 feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
2024-09-12 09:56:53 +05:00
Blue
ad0d75ab0d feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
2024-09-12 09:56:53 +05:00
mubbsharanwar
a90ebb7d4d revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930
2024-09-12 09:52:48 +05:00
Syed Sajjad Hussain Shah
f8290adab5 feat: capture marketing lead in experiment events (#1243) 2024-09-12 09:50:51 +05:00
Syed Sajjad Hussain Shah
788a42b341 fix: fix register button loader for control 2024-09-12 09:49:24 +05:00
Syed Sajjad Hussain Shah
4f48e82959 fix: fix register button width 2024-09-12 09:49:19 +05:00
Syed Sajjad Hussain Shah
99850574fb feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

* feat: add multi step registration eventing
2024-09-12 09:47:16 +05:00
Syed Sajjad Hussain Shah
d66afe98f0 feat: implement multi step registration experiment 2024-09-12 09:44:24 +05:00
Syed Sajjad Hussain Shah
e2cdfce832 Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 18:23:56 +05:00
Awais Ansari
c1e63da778 feat: removed Russian Federation from country list (#1315) 2024-09-10 21:11:44 +05:00
Mubbshar Anwar
ecf4c3ae53 fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-08-29 09:48:21 +05:00
Blue
2428b4c389 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-08-28 14:58:40 +05:00
Syed Sajjad Hussain Shah
099fe8d717 fix: fix datadog js errors (#1296) 2024-08-01 16:06:20 +05:00
Syed Sajjad Hussain Shah
4755540be8 fix: retain query params in authenticated user redirection (#1288) 2024-07-29 11:23:48 +05:00
Mubbshar Anwar
9a30f053c7 fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component

VAN-2013
2024-07-26 15:57:18 +05:00
Mubbshar Anwar
6b983e18d3 fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-07-12 17:05:50 +05:00
Syed Sajjad Hussain Shah
327210192c fix: set marketing opt in in cookie for sso (#1285) 2024-07-12 13:18:42 +05:00
Muhammad Abdullah Waheed
0d603b5fa1 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-07-03 17:08:44 +05:00
Mubbshar Anwar
efaa83a1bc feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

* fix: fix secondary provider null name issue
2024-06-11 12:01:30 +05:00
Mubbshar Anwar
bd63bb1f15 Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled

* feat: remove username from the registration from (#1201) (#1241)

Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>

* fix: add new entry for another US label (#1244)

Add new entry for for another US label which is United States

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* feat: add multi step registration eventing (#1226)

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

* feat: capture marketing lead in experiment events (#1243)

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

* feat: implement auto generated username experiment (#1248)

* feat: implement auto generated username registration exp

* feat: add page event for reset password (#1253)

Description: Add page event for reset password page
VAN-1929

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-06-07 07:57:42 +05:00
Blue
5754c2961a feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
2024-06-04 16:27:14 +05:00
Blue
dcbd644a25 feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
2024-05-13 14:11:03 +05:00
Blue
52e438652c 2u-main rebase with master (#1246)
Rebase 2u-main with master
2024-05-07 16:44:47 +05:00
mubbsharanwar
d8947a4c0a revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930
2024-05-07 11:47:28 +05:00
Syed Sajjad Hussain Shah
03d1666c2c feat: capture marketing lead in experiment events (#1243) 2024-04-25 15:28:05 +05:00
Syed Sajjad Hussain Shah
3782503983 fix: fix register button loader for control 2024-04-22 16:53:23 +05:00
Syed Sajjad Hussain Shah
b219fe3683 fix: fix register button width 2024-04-22 14:37:36 +05:00
Syed Sajjad Hussain Shah
90f650ce3e feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

* feat: add multi step registration eventing
2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
6f325c20c3 feat: implement multi step registration experiment 2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
de12dfbf9e Merge pull request #1236 from openedx/master
adding master commits to 2u-main
2024-04-18 10:19:28 +05:00
Syed Sajjad Hussain Shah
c663f6fa30 Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 17:00:28 +05:00
renovate[bot]
dba93333fd fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-17 16:50:35 +05:00
renovate[bot]
611af07326 fix(deps): update dependency algoliasearch to v4.23.3 2024-04-17 16:50:35 +05:00
renovate[bot]
564ec70d9e fix(deps): update dependency @openedx/paragon to v22.2.1 2024-04-17 16:50:35 +05:00
renovate[bot]
65e95a4d1b chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-17 16:50:35 +05:00
renovate[bot]
cf2b50005b fix(deps): update font awesome to v6.5.2 2024-04-17 16:50:35 +05:00
renovate[bot]
faf4ff8488 fix(deps): update dependency @edx/frontend-platform to v7.1.3 2024-04-17 16:50:35 +05:00
renovate[bot]
7d64220852 chore(deps): update dependency babel-plugin-formatjs to v10.5.14 2024-04-17 16:50:35 +05:00
278 changed files with 21861 additions and 15697 deletions

49
.env Normal file
View File

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

View File

@@ -25,6 +25,9 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
# ***** Cookies *****
SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
@@ -41,3 +44,6 @@ APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}
RECAPTCHA_SITE_KEY_WEB=''

4
.env.private.example Normal file
View File

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

24
.env.test Normal file
View File

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

6
.eslintignore Executable file
View File

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

63
.eslintrc.js Normal file
View File

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

16
.gitignore vendored
View File

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

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
24
20

View File

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

View File

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

View File

@@ -34,6 +34,26 @@ Installation
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
2. Start up LMS, if it's not already started.
4. Within this project (frontend-app-authn), install requirements and start the development server:
.. code-block::
npm install
npm start # The server will run on port 1999
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
**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
=================================
@@ -123,6 +143,10 @@ Furthermore, there are several edX-specific environment variables that enable in
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
- ``true`` | ``''`` (empty strings are falsy)
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
@@ -201,4 +225,4 @@ Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/L
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
:alt: Continuous Integration
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release
:target: https://github.com/semantic-release/semantic-release

5
app.d.ts vendored
View File

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

View File

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

View File

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

60
example.env.config.js Normal file
View File

@@ -0,0 +1,60 @@
/*
Authn MFE is now able to handle JS-based configuration!
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
uncommented.
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
JS-based config will overwrite the .env environment variables.
frontend-platform's getConfig loads configuration in the following sequence:
- .env file config
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
- env.config.js file config
- runtime config
*/
module.exports = {
NODE_ENV: 'development',
NODE_PATH: './src',
PORT: 1999,
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'http://localhost:1999',
CREDENTIALS_BASE_URL: 'http://localhost:18150',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:1999/login',
LOGOUT_URL: 'http://localhost:18000/logout',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
ORDER_HISTORY_URL: 'http://localhost:1996/orders',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
SEGMENT_KEY: '',
SITE_NAME: 'Your Platform Name Here',
INFO_EMAIL: 'info@example.com',
ENABLE_DYNAMIC_REGISTRATION_FIELDS: 'true',
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: 'true',
SESSION_COOKIE_DOMAIN: 'localhost',
USER_INFO_COOKIE_NAME: 'edx-user-info',
LOGIN_ISSUE_SUPPORT_LINK: 'http://localhost:18000/login-issue-support-url',
TOS_AND_HONOR_CODE: 'http://localhost:18000/honor',
TOS_LINK: 'http://localhost:18000/tos',
PRIVACY_POLICY: 'http://localhost:18000/privacy',
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
BANNER_IMAGE_LARGE: '',
BANNER_IMAGE_MEDIUM: '',
BANNER_IMAGE_SMALL: '',
BANNER_IMAGE_EXTRA_SMALL: '',
APP_ID: '',
MFE_CONFIG_API_URL: '',
ZENDESK_KEY: '',
ZENDESK_LOGO_URL: '',
};

View File

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

18221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,25 @@
{
"name": "@openedx/frontend-app-authn",
"version": "1.0.0-alpha.6",
"description": "Frontend authentication",
"engines": {
"node": "^24.12"
},
"name": "@edx/frontend-app-authn",
"version": "0.1.0",
"description": "Frontend application template",
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-app-authn.git"
},
"exports": {
".": "./dist/index.js",
"./app.scss": "./dist/app.scss"
},
"files": [
"/dist"
],
"browserslist": [
"extends @edx/browserslist-config"
],
"sideEffects": [
"*.css",
"*.scss"
],
"scripts": {
"build": "make build",
"clean": "make clean",
"dev": "PORT=1999 PUBLIC_PATH=/authn openedx dev",
"i18n_extract": "openedx formatjs extract",
"lint": "openedx lint .",
"lint:fix": "openedx lint --fix .",
"prepack": "npm run build",
"snapshot": "openedx test --updateSnapshot",
"test": "openedx test --coverage --passWithNoTests"
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"author": "Open edX",
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
"publishConfig": {
@@ -44,41 +29,57 @@
"url": "https://github.com/openedx/frontend-app-authn/issues"
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/openedx-atlas": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"classnames": "^2.5.1",
"fastest-levenshtein": "^1.0.16",
"form-urlencoded": "^6.1.5",
"i18n-iso-countries": "^7.13.0",
"prop-types": "^15.8.1",
"query-string": "^7.1.3",
"react-helmet": "^6.1.0",
"react-loading-skeleton": "^3.5.0",
"react-responsive": "^8.2.0",
"universal-cookie": "^8.0.1"
"@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",
"@openedx/frontend-plugin-framework": "^1.3.0",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.26.0",
"classnames": "2.5.1",
"core-js": "3.43.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.6",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react-error-boundary": "^4.0.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-google-recaptcha-v3": "^1.11.0",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.5.0",
"@testing-library/react": "^16.3.0",
"@types/jest": "^29.5.14",
"babel-plugin-formatjs": "10.5.38",
"eslint-plugin-import": "2.31.0",
"jest": "^29.7.0",
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"copy-webpack-plugin": "^11.0.0",
"@openedx/frontend-build": "^14.6.2",
"babel-plugin-formatjs": "10.5.39",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.1.3",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0",
"tsc-alias": "^1.8.16"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.14",
"@openedx/paragon": "^23",
"@tanstack/react-query": "^5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6",
"react-router-dom": "^6"
"ts-jest": "^29.4.0"
}
}

View File

@@ -1,10 +1,32 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en-us">
<head>
<title>Authentication Development Site></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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" />
<meta name="robots" content="noindex, nofollow" />
<link
rel="shortcut icon"
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
type="image/x-icon"
/>
<script defer src="https://www.edx.org/beam-wrapper.js" ></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
</head>
<body>
<div id="root"></div>
</body>

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

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

View File

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

View File

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

75
src/MainApp.jsx Executable file
View File

@@ -0,0 +1,75 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, RouteTracker, UnAuthOnlyRoute,
} from './common-components';
import configureStore from './data/configureStore';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
PAGE_NOT_FOUND,
PASSWORD_RESET_CONFIRM,
RECOMMENDATIONS,
REGISTER_EMBEDDED_PAGE,
REGISTER_PAGE,
RESET_PAGE,
} from './data/constants';
import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import MainAppSlot from './plugin-slots/MainAppSlot';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password';
import './index.scss';
registerIcons();
const MainApp = () => {
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
return (
<GoogleReCaptchaProvider
reCaptchaKey={recaptchaKey}
useEnterprise
>
<AppProvider store={configureStore()}>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<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>
<RouteTracker />
<MainAppSlot />
</AppProvider>
</GoogleReCaptchaProvider>
);
};
export default MainApp;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
src/cohesion/constants.js Normal file
View File

@@ -0,0 +1,22 @@
export const PAGE_TYPES = {
ACCOUNT_CREATION: 'account-creation',
SIGN_IN: 'sign-in',
};
export const ELEMENT_TYPES = {
BUTTON: 'BUTTON',
};
export const EVENT_TYPES = { ElementClicked: 'redventures.usertracking.v3.ElementClicked' };
export const ELEMENT_TEXT = {
CREATE_ACCOUNT: 'create-account',
OPT_IN_TEXT: 'I agree that edx may send me marketing messages',
SIGN_IN: 'Sign In',
};
export const ELEMENT_NAME = {
SIGN_IN: PAGE_TYPES.SIGN_IN,
OPT_OUT: 'opt-out',
CREATE_ACCOUNT: 'Create an account for free',
};

View File

@@ -0,0 +1,6 @@
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
export const setCohesionEventStates = (eventData) => ({
type: SET_COHESION_EVENT_ELEMENT_STATES,
payload: eventData,
});

View File

@@ -0,0 +1,17 @@
import { SET_COHESION_EVENT_ELEMENT_STATES } from './actions';
export const storeName = 'cohesion';
export const defaultState = {
eventData: {},
};
export const reducer = (state = defaultState, action = {}) => {
if (action.type === SET_COHESION_EVENT_ELEMENT_STATES) {
return {
...state,
eventData: action.payload,
};
}
return state;
};

24
src/cohesion/trackers.js Normal file
View File

@@ -0,0 +1,24 @@
import { EVENT_TYPES } from './constants';
/**
* Tracks cohesion events by setting the page type and tracking a click event.
*
* @param {string} pageType - The type of page where the event occurred.
* @param {string} elementType - The type of the web element (e.g., 'BUTTON', 'LINK').
* @param {string} webElementText - The text content of the web element.
* @param {string} webElementName - The name of the web element.
*/
const trackCohesionEvent = (eventData) => {
window.chsn_pageType = eventData.pageType;
const webElement = {
elementType: eventData.elementType,
text: eventData.webElementText,
name: eventData.webElementName,
};
window.tagular?.('beam', {
'@type': EVENT_TYPES.ElementClicked,
webElement,
});
};
export default trackCohesionEvent;

6
src/cohesion/utils.js Normal file
View File

@@ -0,0 +1,6 @@
const mockTagular = () => {
const getTagular = jest.fn();
window.tagular = getTagular;
};
export default mockTagular;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { useState } from 'react';
import {
Form, TransitionReplace,

View File

@@ -1,4 +1,7 @@
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink, Icon } from '@openedx/paragon';
import { Institution } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
@@ -28,7 +31,7 @@ export const RenderInstitutionButton = props => {
* This component renders the page list of available institutions for login
* */
const InstitutionLogistration = props => {
const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const { formatMessage } = useIntl();
const {
secondaryProviders,
@@ -57,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
{provider.name}
{provider?.name}
</Hyperlink>
</td>
</tr>

View File

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

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@openedx/frontend-base';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
} from '@openedx/paragon';
@@ -9,23 +10,19 @@ import {
} from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { validatePasswordField } from '../register/data/utils';
import messages from './messages';
const noopFn = () => {};
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
import { validatePasswordField } from '../register/data/utils';
const PasswordField = (props) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const {
validationApiRateLimited = false,
clearRegistrationBackendError = noopFn,
validateField = noopFn,
} = props;
const handleBlur = (e) => {
const { name, value } = e.target;
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
@@ -53,7 +50,7 @@ const PasswordField = (props) => {
if (fieldError) {
props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) {
validateField({ password: passwordValue });
dispatch(fetchRealtimeValidations({ password: passwordValue }));
}
}
};
@@ -68,7 +65,7 @@ const PasswordField = (props) => {
}
if (props.handleErrorChange) {
props.handleErrorChange('password', '');
clearRegistrationBackendError('password');
dispatch(clearRegistrationBackendError('password'));
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};
@@ -158,9 +155,6 @@ PasswordField.defaultProps = {
showRequirements: true,
showScreenReaderText: true,
autoComplete: null,
clearRegistrationBackendError: noopFn,
validateField: noopFn,
validationApiRateLimited: false,
};
PasswordField.propTypes = {
@@ -176,9 +170,6 @@ PasswordField.propTypes = {
value: PropTypes.string.isRequired,
autoComplete: PropTypes.string,
showScreenReaderText: PropTypes.bool,
clearRegistrationBackendError: PropTypes.func,
validateField: PropTypes.func,
validationApiRateLimited: PropTypes.bool,
};
export default PasswordField;

View File

@@ -1,11 +1,15 @@
import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';
import trackCohesionEvent from '../cohesion/trackers';
import {
AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
import { setCookie } from '../data/utils';
import setCookie from '../data/utils/cookies';
import { redirectWithDelay } from '../data/utils/dataUtils';
const RedirectLogistration = (props) => {
const {
@@ -15,20 +19,27 @@ const RedirectLogistration = (props) => {
redirectToProgressiveProfilingPage,
success,
optionalFields,
redirectToRecommendationsPage,
educationLevel,
userId,
registrationEmbedded,
host,
currectProvider,
} = props;
const cohesionEventData = useSelector(state => state.cohesion.eventData);
let finalRedirectUrl = '';
if (success) {
// This event is used by cohesion upon successful login and registration
if (!currectProvider) {
trackCohesionEvent(cohesionEventData);
}
// 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.
// Note: For multiple enterprise use case, we need to make sure that user first visits the
// enterprise selection page and then complete the auth workflow
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl;
} else {
finalRedirectUrl = redirectUrl;
}
@@ -36,12 +47,12 @@ const RedirectLogistration = (props) => {
// Redirect to Progressive Profiling after successful registration
if (redirectToProgressiveProfilingPage) {
// TODO: Do we still need this cookie?
setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
setCookie('van-504-returning-user', true);
if (registrationEmbedded) {
window.parent.postMessage({
action: REDIRECT,
redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
}, host);
return null;
}
@@ -59,7 +70,22 @@ const RedirectLogistration = (props) => {
);
}
window.location.href = finalRedirectUrl;
// Redirect to Recommendation page
if (redirectToRecommendationsPage) {
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Navigate
to={RECOMMENDATIONS}
state={{
registrationResult,
educationLevel,
userId,
}}
replace
/>
);
}
redirectWithDelay(finalRedirectUrl);
}
return null;
@@ -73,9 +99,11 @@ RedirectLogistration.defaultProps = {
redirectUrl: '',
redirectToProgressiveProfilingPage: false,
optionalFields: {},
redirectToRecommendationsPage: false,
userId: null,
registrationEmbedded: false,
host: '',
currectProvider: '',
};
RedirectLogistration.propTypes = {
@@ -86,9 +114,11 @@ RedirectLogistration.propTypes = {
redirectUrl: PropTypes.string,
redirectToProgressiveProfilingPage: PropTypes.bool,
optionalFields: PropTypes.shape({}),
redirectToRecommendationsPage: PropTypes.bool,
userId: PropTypes.number,
registrationEmbedded: PropTypes.bool,
host: PropTypes.string,
currectProvider: PropTypes.string,
};
export default RedirectLogistration;

View File

@@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const RouteTracker = () => {
const location = useLocation();
useEffect(() => {
window.tagular?.('pageView');
}, [location]);
return null;
};
export default RouteTracker;

View File

@@ -1,21 +1,43 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import { Icon } from '@openedx/paragon';
import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
import { ELEMENT_TYPES, PAGE_TYPES } from '../cohesion/constants';
import trackCohesionEvent from '../cohesion/trackers';
import {
LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES,
} from '../data/constants';
import { setCookie } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) {
function handleSubmit(e, providerName) {
e.preventDefault();
const eventData = {
pageType: referrer === LOGIN_PAGE ? PAGE_TYPES.SIGN_IN : PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: providerName,
webElementName: providerName.toLowerCase(),
};
// This event is used by cohesion upon successful login
trackCohesionEvent(eventData);
if (referrer === REGISTER_PAGE) {
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
}
const url = e.currentTarget.dataset.providerUrl;
window.location.href = getSiteConfig().lmsBaseUrl + url;
redirectWithDelay(getConfig().LMS_BASE_URL + url);
}
const socialAuth = socialAuthProviders.map((provider, index) => (
@@ -25,7 +47,7 @@ const SocialAuthProviders = (props) => {
type="button"
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
onClick={handleSubmit}
onClick={(event) => handleSubmit(event, provider?.name)}
>
{provider.iconImage ? (
<div aria-hidden="true">

View File

@@ -1,4 +1,7 @@
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Hyperlink, Icon,
} from '@openedx/paragon';
@@ -7,10 +10,10 @@ 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 messages from './messages';
import {
RenderInstitutionButton,
@@ -32,8 +35,8 @@ const ThirdPartyAuth = (props) => {
} = props;
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = useAppConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getSiteConfig().lmsBaseUrl + ENTERPRISE_LOGIN_URL;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return (

View File

@@ -1,14 +1,18 @@
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
import PropTypes from 'prop-types';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
const { currentProvider, referrer } = props;
const platformName = getSiteConfig().siteName;
const platformName = getConfig().SITE_NAME;
let message;
if (referrer === LOGIN_PAGE) {
@@ -17,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
if (!currentProvider) {
if (currentProvider) {
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
setCookie('ssoPipelineRedirectionDone', true);
} else {
return null;
}
@@ -27,7 +34,7 @@ const ThirdPartyAuthAlert = (props) => {
{referrer === REGISTER_PAGE ? (
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
) : null}
<p>{message}</p>
<p>{ message }</p>
</Alert>
{referrer === REGISTER_PAGE ? (
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
import {
DEFAULT_REDIRECT_URL,
} from '../data/constants';
import { RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
/**
* This wrapper redirects the requester to our default redirect url if they are
@@ -24,7 +24,12 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
const updatedPath = updatePathWithQueryParams(window.location.pathname);
if (updatedPath.startsWith(RESET_PAGE)) {
global.location.href = getConfig().LMS_BASE_URL;
return null;
}
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null;
}

View File

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

View File

@@ -1,60 +0,0 @@
import { render, screen } from '@testing-library/react';
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')).toBeTruthy();
});
it('should provide all context values to children', () => {
render(
<ThirdPartyAuthProvider>
<TestComponent />
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('FieldDescriptions Available')).toBeTruthy();
expect(screen.getByText('OptionalFields Available')).toBeTruthy();
expect(screen.getByText('AuthApiStatus Not Available')).toBeTruthy(); // Initially null
expect(screen.getByText('AuthContext Available')).toBeTruthy();
});
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')).toBeTruthy();
expect(screen.getByText('Second Child')).toBeTruthy();
expect(screen.getByText('Third Child')).toBeTruthy();
});
});

View File

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

@@ -0,0 +1,33 @@
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,
countriesCodesList) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: {
fieldDescriptions, optionalFields, thirdPartyAuthContext, countriesCodesList,
},
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
export const clearThirdPartyAuthContextErrorMessage = () => ({
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
});

View File

@@ -1,24 +0,0 @@
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
const getThirdPartyAuthContext = async (urlParams: string) => {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getSiteConfig().lmsBaseUrl}/api/mfe_context`,
requestConfig,
);
return {
fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || {},
};
};
export {
getThirdPartyAuthContext,
};

View File

@@ -1,19 +0,0 @@
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, { enabled = true } = {}) => useQuery({
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId, payload),
queryFn: () => getThirdPartyAuthContext(payload),
retry: false,
staleTime: 5 * 60 * 1000, // 5 minutes — TPA context is effectively static per session
enabled,
});
export {
useThirdPartyAuthHook,
};

View File

@@ -0,0 +1,83 @@
export const registerFields = {
fields: {
country: {
name: 'country',
error_message: 'Select your country or region of residence',
},
honor_code: {
name: 'honor_code',
type: 'tos_and_honor_code',
error_message: '',
},
},
};
export const progressiveProfilingFields = {
extended_profile: [],
fields: {
level_of_education: {
name: 'level_of_education',
type: 'select',
label: 'Highest level of education completed',
error_message: '',
options: [
[
'p',
'Doctorate',
],
[
'm',
"Master's or professional degree",
],
[
'b',
"Bachelor's degree",
],
[
'a',
'Associate degree',
],
[
'hs',
'Secondary/high school',
],
[
'jhs',
'Junior secondary/junior high/middle school',
],
[
'none',
'No formal education',
],
[
'other',
'Other education',
],
],
},
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
error_message: '',
options: [
[
'm',
'Male',
],
[
'f',
'Female',
],
[
'o',
'Other/Prefer Not to Say',
],
],
},
},
};
export const FIELD_LABELS = {
COUNTRY: 'country',
};

View File

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

View File

@@ -0,0 +1,64 @@
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: {
autoSubmitRegForm: false,
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,
countriesCodesList: action.payload.countriesCodesList,
};
}
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

@@ -0,0 +1,51 @@
import { getConfig } from '@edx/frontend-platform';
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 { progressiveProfilingFields, registerFields } from './constants';
import {
getCountryList,
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);
const countriesCodesList = (yield call(getCountryList)) || [];
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
// hard code country field, level of education and gender fields
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
yield put(getThirdPartyAuthContextSuccess(
registerFields,
progressiveProfilingFields,
thirdPartyAuthContext,
countriesCodesList,
));
} else {
yield put(getThirdPartyAuthContextSuccess(
fieldDescriptions,
optionalFields,
thirdPartyAuthContext,
countriesCodesList,
));
}
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

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

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

View File

@@ -0,0 +1,82 @@
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 occurred',
},
};
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

@@ -0,0 +1,77 @@
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();
jest.mock('../service', () => ({
getCountryList: jest.fn(),
getThirdPartyAuthContext: jest.fn(),
}));
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: {},
countriesCodesList: [],
}));
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

@@ -2,10 +2,15 @@ export { default as RedirectLogistration } from './RedirectLogistration';
export { default as registerIcons } from './RegisterFaIcons';
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
export { default as RouteTracker } from './RouteTracker';
export { default as NotFoundPage } from './NotFoundPage';
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

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

View File

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

View File

@@ -1,11 +1,15 @@
import { IntlProvider } from '@openedx/frontend-base';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { fetchRealtimeValidations } from '../../register/data/actions';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
describe('FormGroup', () => {
const props = {
floatingLabel: 'Email',
@@ -31,15 +35,26 @@ describe('FormGroup', () => {
});
describe('PasswordField', () => {
const mockStore = configureStore();
let props = {};
let store = {};
const wrapper = children => (
const reduxWrapper = children => (
<IntlProvider locale="en">
{children}
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
register: {
validationApiRateLimited: false,
},
};
beforeEach(() => {
store = mockStore(initialState);
props = {
floatingLabel: 'Password',
name: 'password',
@@ -49,7 +64,7 @@ describe('PasswordField', () => {
});
it('should show/hide password on icon click', () => {
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password');
@@ -62,7 +77,7 @@ describe('PasswordField', () => {
});
it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -79,7 +94,7 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -102,7 +117,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -125,7 +140,7 @@ describe('PasswordField', () => {
});
it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password');
@@ -146,7 +161,7 @@ describe('PasswordField', () => {
...props,
handleBlur: jest.fn(),
};
const { container } = render(wrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -164,7 +179,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(wrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -187,7 +202,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -207,7 +222,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -226,13 +241,12 @@ describe('PasswordField', () => {
});
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
const mockValidateField = jest.fn();
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
handleErrorChange: jest.fn(),
validateField: mockValidateField,
};
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
@@ -241,17 +255,18 @@ describe('PasswordField', () => {
},
});
expect(mockValidateField).toHaveBeenCalledWith({ password: 'password123' });
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
});
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
value: 'testPassword',
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');

View File

@@ -1,14 +1,35 @@
import { IntlProvider } from '@openedx/frontend-base';
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
registerIcons();
const mockStore = configureStore();
describe('SocialAuthProviders', () => {
let props = {};
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
},
},
};
const store = mockStore(initialState);
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const appleProvider = {
id: 'oa2-apple-id',
name: 'Apple',
@@ -28,11 +49,11 @@ describe('SocialAuthProviders', () => {
it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -46,11 +67,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -64,11 +85,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(
const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
).toJSON();
)).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -1,7 +1,9 @@
import { IntlProvider } from '@openedx/frontend-base';
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
import { REGISTER_PAGE } from '../../data/constants';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
describe('ThirdPartyAuthAlert', () => {
@@ -36,19 +38,4 @@ 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,7 +1,8 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@openedx/frontend-base';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import {
@@ -11,8 +12,7 @@ import {
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
jest.mock('@openedx/frontend-base', () => ({
...jest.requireActual('@openedx/frontend-base'),
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(),
}));

View File

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

View File

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

View File

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

43
src/config/index.js Normal file
View File

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

View File

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

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

@@ -5,6 +5,7 @@ export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
export const RESET_PAGE = '/reset';
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
export const DEFAULT_REDIRECT_URL = '/dashboard';
export const RECOMMENDATIONS = '/recommendations';
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
export const PAGE_NOT_FOUND = '/notfound';
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
@@ -28,11 +29,12 @@ export const EMBEDDED = 'embedded';
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}\\]$';
+ '|^"([\\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}\\]$';
// Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect';
export const APP_NAME = 'authn_mfe';

View File

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

17
src/data/optimizely.js Normal file
View File

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

38
src/data/reducers.js Executable file
View File

@@ -0,0 +1,38 @@
import { combineReducers } from 'redux';
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
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,
[cohesionStoreName]: cohesionReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

19
src/data/sagas.js Normal file
View File

@@ -0,0 +1,19 @@
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(),
]);
}

37
src/data/segment/utils.js Normal file
View File

@@ -0,0 +1,37 @@
/* eslint-disable import/prefer-default-export */
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { APP_NAME } from '../constants';
export const LINK_TIMEOUT = 300;
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
name,
{ ...options, app_name: APP_NAME },
);
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
name,
options,
{ app_name: APP_NAME },
);
export const createLinkTracker = (tracker, href) => (e) => {
e.preventDefault();
tracker();
return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
};

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

@@ -1,7 +1,13 @@
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');
@@ -11,7 +17,9 @@ describe('setCookie function', () => {
});
it('should set a cookie with default options', () => {
setCookie('testCookie', 'testValue', 'example.com');
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
setCookie('testCookie', 'testValue');
expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith();
@@ -22,8 +30,10 @@ describe('setCookie function', () => {
});
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', 'example.com', expiry);
setCookie('testCookie', 'testValue', expiry);
expect(Cookies).toHaveBeenCalled();
expect(Cookies).toHaveBeenCalledWith();
@@ -35,7 +45,7 @@ describe('setCookie function', () => {
});
it('should not set a cookie if cookieName is undefined', () => {
setCookie(undefined, 'testValue', 'example.com');
setCookie(undefined, 'testValue');
expect(Cookies).not.toHaveBeenCalled();
});

View File

@@ -0,0 +1,14 @@
import AsyncActionType from '../utils/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,12 +1,21 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie';
export default function setCookie(cookieName, cookieValue, cookieDomain, cookieExpiry) {
export default function setCookie(cookieName, cookieValue, cookieExpiry) {
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
const cookies = new Cookies();
const options = { domain: cookieDomain, path: '/' };
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
if (cookieExpiry) {
options.expires = cookieExpiry;
}
cookies.set(cookieName, cookieValue, options);
}
}
export function removeCookie(cookieName) {
if (cookieName) {
const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
cookies.remove(cookieName, options);
}
}

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