Compare commits
309 Commits
open-relea
...
embeddable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a378462480 | ||
|
|
512deae883 | ||
|
|
2d11477037 | ||
|
|
5e15969f4a | ||
|
|
0e7e748bc4 | ||
|
|
68be7c65d5 | ||
|
|
80e940f44e | ||
|
|
37e811d7e5 | ||
|
|
a35a1d1ba6 | ||
|
|
41a9c89d71 | ||
|
|
d469102cee | ||
|
|
c685bdd373 | ||
|
|
daa7ae4d73 | ||
|
|
c5caaeba60 | ||
|
|
a473d79554 | ||
|
|
1b5aa106ab | ||
|
|
d8b5653224 | ||
|
|
4cc4ff6c4b | ||
|
|
48a3c57e5f | ||
|
|
efdefc300e | ||
|
|
9730a4f55d | ||
|
|
fc62241332 | ||
|
|
0846001b6d | ||
|
|
90658722e1 | ||
|
|
240752c6cd | ||
|
|
429d4547e4 | ||
|
|
e278b5f74a | ||
|
|
a723058bc1 | ||
|
|
59fa7d5de3 | ||
|
|
60578189bd | ||
|
|
82cd11e01e | ||
|
|
4a10540d4a | ||
|
|
aeda262fb0 | ||
|
|
dff3903617 | ||
|
|
1399caf003 | ||
|
|
2dfb6bc528 | ||
|
|
a392395876 | ||
|
|
5542311c95 | ||
|
|
21e6bb6eec | ||
|
|
bfa7874108 | ||
|
|
423958c899 | ||
|
|
cb380a2031 | ||
|
|
f4e89efdb4 | ||
|
|
f5cb7a1dbd | ||
|
|
72e601948c | ||
|
|
29e30981ae | ||
|
|
06a61e6a22 | ||
|
|
1c83020b43 | ||
|
|
fa4a0ac2d5 | ||
|
|
2addf57cbd | ||
|
|
d521fd20ec | ||
|
|
38d44ac586 | ||
|
|
4768306f53 | ||
|
|
6c6b527dfc | ||
|
|
e14c9bd1b7 | ||
|
|
a2bdc4031b | ||
|
|
8f38eb9e3a | ||
|
|
c22aa58904 | ||
|
|
067bddf892 | ||
|
|
7e4bccbc29 | ||
|
|
f39bb35dc8 | ||
|
|
0d760c04b7 | ||
|
|
6f113542f5 | ||
|
|
1e4c342703 | ||
|
|
3ce0585d7e | ||
|
|
5bf6dd6361 | ||
|
|
929abdff69 | ||
|
|
f295d69e76 | ||
|
|
32a4c55e4a | ||
|
|
615ba91bdb | ||
|
|
dfb2f89a36 | ||
|
|
c9783234cc | ||
|
|
0513e6c2de | ||
|
|
3cdc0234ef | ||
|
|
e06d12be07 | ||
|
|
56394881fc | ||
|
|
61056240c4 | ||
|
|
a59e7c548c | ||
|
|
f63d7674e2 | ||
|
|
fa3a70e9a9 | ||
|
|
b817a8d122 | ||
|
|
2a6668cef3 | ||
|
|
a802821ae9 | ||
|
|
9e13141f6b | ||
|
|
4b64ce2534 | ||
|
|
c550069e11 | ||
|
|
1e10e9c89c | ||
|
|
cd6c1c0e42 | ||
|
|
5edcee9eb9 | ||
|
|
d41c06b1fd | ||
|
|
2a2c5abc81 | ||
|
|
ccdd648603 | ||
|
|
5c1ea04970 | ||
|
|
5ebd22f088 | ||
|
|
5aec091156 | ||
|
|
68993cc21f | ||
|
|
404fc2b36b | ||
|
|
7bb06d0bfa | ||
|
|
abbb64b9c7 | ||
|
|
99ca582c1a | ||
|
|
3e4cfc1573 | ||
|
|
5437e1b7e9 | ||
|
|
4d33cd7d69 | ||
|
|
cb82e94b53 | ||
|
|
dbe14ccedf | ||
|
|
841edf2d24 | ||
|
|
0c375cc50c | ||
|
|
50072887d0 | ||
|
|
976814d846 | ||
|
|
c22024cf66 | ||
|
|
829a219b9f | ||
|
|
13d572ab28 | ||
|
|
c0c2ffa122 | ||
|
|
84c563fda3 | ||
|
|
97720d6a46 | ||
|
|
386982d9c4 | ||
|
|
cb38a2a148 | ||
|
|
07f19209bf | ||
|
|
69c0ca13fc | ||
|
|
e49c50f55c | ||
|
|
f0105f0094 | ||
|
|
3c89afea4a | ||
|
|
848f574f3f | ||
|
|
ec9e34cea4 | ||
|
|
f9069df4e6 | ||
|
|
9035f3eb7e | ||
|
|
e197e788d1 | ||
|
|
49d33522a8 | ||
|
|
06f0ec3c0b | ||
|
|
54319c6949 | ||
|
|
86f875ec3e | ||
|
|
754a6ddb12 | ||
|
|
62e8f75b96 | ||
|
|
c0deb663a6 | ||
|
|
ce28add152 | ||
|
|
5f89315947 | ||
|
|
56dd194a1a | ||
|
|
adcdcc4c8b | ||
|
|
7fd45f089d | ||
|
|
65d82b2080 | ||
|
|
f771935e20 | ||
|
|
70c255fc4f | ||
|
|
bd1396fc54 | ||
|
|
cba5395d5c | ||
|
|
b42c09d919 | ||
|
|
b7af2356fa | ||
|
|
22d477e55f | ||
|
|
dfa69c27bb | ||
|
|
6b78158db2 | ||
|
|
c92cac0eed | ||
|
|
b2dce920fa | ||
|
|
44cec762fb | ||
|
|
a98188ead8 | ||
|
|
294b1a469f | ||
|
|
ebed588c1c | ||
|
|
46f8217e1a | ||
|
|
9ab23cf485 | ||
|
|
7790660fe8 | ||
|
|
1dd2726beb | ||
|
|
ba9ce89d1b | ||
|
|
c9082ac709 | ||
|
|
27c8fa8986 | ||
|
|
a04289d71b | ||
|
|
321859e0f5 | ||
|
|
4b4bf413c1 | ||
|
|
06c4f75b4a | ||
|
|
12dd97af61 | ||
|
|
8e77197459 | ||
|
|
3cc64cada6 | ||
|
|
3ac5874df1 | ||
|
|
107dd6f360 | ||
|
|
347e0cd336 | ||
|
|
2ba6058ec7 | ||
|
|
683aa258b8 | ||
|
|
d7ad7e314d | ||
|
|
ef66eb1c31 | ||
|
|
ec8b256852 | ||
|
|
5a715b2fb5 | ||
|
|
e80578e682 | ||
|
|
155a73dc39 | ||
|
|
f5d0b50d90 | ||
|
|
b0745de672 | ||
|
|
d54fdbf84f | ||
|
|
0a6432c393 | ||
|
|
9e91c382b3 | ||
|
|
2cf24761c0 | ||
|
|
c2bdc31a03 | ||
|
|
9d487d7b61 | ||
|
|
a2ab6c196a | ||
|
|
6a5b02e8ad | ||
|
|
e76f214024 | ||
|
|
cb47717b09 | ||
|
|
85dbc9a6ca | ||
|
|
4aebeaffa7 | ||
|
|
6a84e2d5b6 | ||
|
|
e26620e350 | ||
|
|
1cabd2a514 | ||
|
|
06dd70078e | ||
|
|
4b13866e1d | ||
|
|
11142fda25 | ||
|
|
b4057f9588 | ||
|
|
9524f030d1 | ||
|
|
3f10dce04f | ||
|
|
5f8802272d | ||
|
|
0d486c2774 | ||
|
|
e78a1583c0 | ||
|
|
ea966c48b9 | ||
|
|
810b8d46b9 | ||
|
|
8a00b74863 | ||
|
|
94fafe661d | ||
|
|
7d58a124ab | ||
|
|
378a8d95f9 | ||
|
|
3a2e39af97 | ||
|
|
6f6d725126 | ||
|
|
3d8eb34d80 | ||
|
|
2768fc02ea | ||
|
|
0902467fa6 | ||
|
|
1dc999070f | ||
|
|
7a169715ea | ||
|
|
361f6781ee | ||
|
|
42190a89dd | ||
|
|
2d4c6a1d3b | ||
|
|
1dd88795c3 | ||
|
|
7cff7311e1 | ||
|
|
bf93959350 | ||
|
|
94151c2668 | ||
|
|
bf650e6d4c | ||
|
|
575f195970 | ||
|
|
c6bf6c92c1 | ||
|
|
b86c31bff8 | ||
|
|
fc37bbec1d | ||
|
|
6525c66600 | ||
|
|
145234c5c3 | ||
|
|
a7f816f49a | ||
|
|
694b0a5381 | ||
|
|
8a0947faf3 | ||
|
|
d1c4b20160 | ||
|
|
d81d8419a0 | ||
|
|
c6acdab7c6 | ||
|
|
0374143148 | ||
|
|
2d3c5ed761 | ||
|
|
a611451233 | ||
|
|
93dcd8f16e | ||
|
|
294519c7a5 | ||
|
|
f11df1f513 | ||
|
|
563609e10a | ||
|
|
b4d4e36f72 | ||
|
|
f291efc428 | ||
|
|
1a61ba3cc7 | ||
|
|
3d10cea137 | ||
|
|
6c72e9dad4 | ||
|
|
c5d4f6b94d | ||
|
|
c52d7b6de5 | ||
|
|
6ade0a837f | ||
|
|
81d69c8e72 | ||
|
|
ca333e895f | ||
|
|
8e527efd07 | ||
|
|
c31c03f5a9 | ||
|
|
fe34acf314 | ||
|
|
1f21a874b8 | ||
|
|
ee6a6f0d2d | ||
|
|
8d0181ccca | ||
|
|
e8dba05920 | ||
|
|
6652e2f15c | ||
|
|
578eec8a2d | ||
|
|
8b116d2234 | ||
|
|
b9aa110440 | ||
|
|
9f50bbda79 | ||
|
|
6f2ce69b77 | ||
|
|
8ad2678ce2 | ||
|
|
49c91262fd | ||
|
|
15378682ab | ||
|
|
296861ce3a | ||
|
|
aa59acf0bc | ||
|
|
83e204f3f8 | ||
|
|
497d60e244 | ||
|
|
e8282d6d4a | ||
|
|
72510047d8 | ||
|
|
7dee21eb72 | ||
|
|
d06290642b | ||
|
|
525abe6b88 | ||
|
|
a7704edb9c | ||
|
|
78693f4fc6 | ||
|
|
186484defa | ||
|
|
f8fe704c42 | ||
|
|
82857db236 | ||
|
|
661914b0db | ||
|
|
057299de2b | ||
|
|
93db1a23e2 | ||
|
|
36ec03b24b | ||
|
|
1d6f47c49e | ||
|
|
10dfab7127 | ||
|
|
a8ebe2c096 | ||
|
|
b3dc1c1513 | ||
|
|
7cdae09a94 | ||
|
|
59c2c2fd5d | ||
|
|
70000aab75 | ||
|
|
059d79302d | ||
|
|
ee300466aa | ||
|
|
c4fb3f72e5 | ||
|
|
ed0da96076 | ||
|
|
85fbc54384 | ||
|
|
a6c282520a | ||
|
|
aa4faba4a3 | ||
|
|
5e864c4ff1 | ||
|
|
0c4e612e39 | ||
|
|
92faa846ac | ||
|
|
5bfdd8e1ef | ||
|
|
5af93a57c7 |
29
.env
29
.env
@@ -13,20 +13,29 @@ ORDER_HISTORY_URL=null
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME=null
|
SITE_NAME=null
|
||||||
USER_INFO_COOKIE_NAME=null
|
|
||||||
AUTHN_MINIMAL_HEADER=true
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
|
||||||
USER_SURVEY_COOKIE_NAME=null
|
|
||||||
COOKIE_DOMAIN=null
|
|
||||||
WELCOME_PAGE_SUPPORT_LINK=null
|
|
||||||
INFO_EMAIL=''
|
INFO_EMAIL=''
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
# ***** Cookies *****
|
||||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||||
ENABLE_PROGRESSIVE_PROFILING=''
|
# ***** Links *****
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||||
|
POST_REGISTRATION_REDIRECT_URL=''
|
||||||
|
SEARCH_CATALOG_URL=''
|
||||||
|
# ***** Features flags *****
|
||||||
|
DISABLE_ENTERPRISE_LOGIN=''
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||||
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||||
|
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
MARKETING_EMAILS_OPT_IN=''
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||||
|
# ***** Zendesk related keys *****
|
||||||
ZENDESK_KEY=''
|
ZENDESK_KEY=''
|
||||||
ZENDESK_LOGO_URL=''
|
ZENDESK_LOGO_URL=''
|
||||||
|
# ***** Base Container Images *****
|
||||||
|
BANNER_IMAGE_LARGE=''
|
||||||
|
BANNER_IMAGE_MEDIUM=''
|
||||||
|
BANNER_IMAGE_SMALL=''
|
||||||
|
BANNER_IMAGE_EXTRA_SMALL=''
|
||||||
|
# ***** Miscellaneous *****
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
ENABLE_COOKIE_POLICY_BANNER=''
|
|
||||||
|
|||||||
@@ -18,21 +18,24 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME='Your Platform Name Here'
|
SITE_NAME='Your Platform Name Here'
|
||||||
|
INFO_EMAIL='info@example.com'
|
||||||
|
# ***** Cookies *****
|
||||||
|
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||||
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
AUTHN_MINIMAL_HEADER=true
|
# ***** Links *****
|
||||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||||
TOS_LINK='http://localhost:18000/tos'
|
TOS_LINK='http://localhost:18000/tos'
|
||||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||||
COOKIE_DOMAIN='localhost'
|
# ***** Base Container Images *****
|
||||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
BANNER_IMAGE_LARGE=''
|
||||||
INFO_EMAIL='info@edx.org'
|
BANNER_IMAGE_MEDIUM=''
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
BANNER_IMAGE_SMALL=''
|
||||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
BANNER_IMAGE_EXTRA_SMALL=''
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
# ***** Miscellaneous *****
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
|
||||||
ZENDESK_KEY=''
|
|
||||||
ZENDESK_LOGO_URL=''
|
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
|
ZENDESK_KEY=''
|
||||||
|
ZENDESK_LOGO_URL=''
|
||||||
|
|||||||
4
.env.private.example
Normal file
4
.env.private.example
Normal 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'
|
||||||
@@ -16,15 +16,6 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME='Your Platform Name Here'
|
SITE_NAME='Your Platform Name Here'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
|
|
||||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
|
||||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
|
||||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
|
||||||
ZENDESK_KEY=''
|
|
||||||
ZENDESK_LOGO_URL=''
|
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
coverage/*
|
coverage/*
|
||||||
dist/
|
dist/
|
||||||
|
docs
|
||||||
node_modules/
|
node_modules/
|
||||||
__mocks__/
|
__mocks__/
|
||||||
__snapshots__/
|
__snapshots__/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { createConfig } = require('@edx/frontend-build');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
module.exports = createConfig('eslint', {
|
||||||
@@ -7,10 +8,10 @@ module.exports = createConfig('eslint', {
|
|||||||
// to fail for no apparent reason since upgrading
|
// to fail for no apparent reason since upgrading
|
||||||
// @edx/frontend-build from v3 to v5:
|
// @edx/frontend-build from v3 to v5:
|
||||||
// - TypeError: Cannot read property 'range' of null
|
// - TypeError: Cannot read property 'range' of null
|
||||||
'indent': [
|
indent: [
|
||||||
'error',
|
'error',
|
||||||
2,
|
2,
|
||||||
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
|
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
|
||||||
],
|
],
|
||||||
'template-curly-spacing': 'off',
|
'template-curly-spacing': 'off',
|
||||||
'jsx-a11y/label-has-associated-control': ['error', {
|
'jsx-a11y/label-has-associated-control': ['error', {
|
||||||
@@ -18,7 +19,7 @@ module.exports = createConfig('eslint', {
|
|||||||
labelAttributes: [],
|
labelAttributes: [],
|
||||||
controlComponents: [],
|
controlComponents: [],
|
||||||
assert: 'htmlFor',
|
assert: 'htmlFor',
|
||||||
depth: 25
|
depth: 25,
|
||||||
}],
|
}],
|
||||||
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
|
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
|
||||||
'import/order': [
|
'import/order': [
|
||||||
@@ -46,5 +47,6 @@ module.exports = createConfig('eslint', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'function-paren-newline': 'off',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or Github issues.
|
Include a description of your changes here, along with a link to any relevant Jira tickets and/or Github issues.
|
||||||
|
|
||||||
|
#### JIRA
|
||||||
|
|
||||||
|
[XXX-XXXX](https://2u-internal.atlassian.net/browse/XXX-XXXX)
|
||||||
|
|
||||||
#### How Has This Been Tested?
|
#### How Has This Been Tested?
|
||||||
|
|
||||||
Please describe in detail how you tested your changes.
|
Please describe in detail how you tested your changes.
|
||||||
|
|
||||||
#### Screenshots/sandbox (optional):
|
#### Screenshots/sandbox (optional):
|
||||||
|
|
||||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if its not applicable.**
|
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if its not applicable.**
|
||||||
|
|
||||||
|Before|After|
|
|Before|After|
|
||||||
|
|||||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "label: " it tries to apply
|
||||||
|
# the label indicated in rest of comment.
|
||||||
|
# If the comment starts with "remove label: ", it tries
|
||||||
|
# to remove the indicated label.
|
||||||
|
# Note: Labels are allowed to have spaces and this script does
|
||||||
|
# not parse spaces (as often a space is legitimate), so the command
|
||||||
|
# "label: really long lots of words label" will apply the
|
||||||
|
# label "really long lots of words label"
|
||||||
|
|
||||||
|
name: Allows for the adding and removing of labels via comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_remove_labels:
|
||||||
|
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||||
|
|
||||||
25
.github/workflows/autoupdate-pull-request.yml
vendored
Normal file
25
.github/workflows/autoupdate-pull-request.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: autoupdate
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
types: [ labeled ]
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
autoupdate:
|
||||||
|
name: autoupdate
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
DRY_RUN: "false"
|
||||||
|
PR_FILTER: "labelled"
|
||||||
|
PR_LABELS: "autoupdate"
|
||||||
|
EXCLUDED_LABELS: "dependencies,wontfix"
|
||||||
|
MERGE_MSG: "Branch was auto-updated."
|
||||||
|
RETRY_COUNT: "5"
|
||||||
|
RETRY_SLEEP: "300"
|
||||||
|
MERGE_CONFLICT_ACTION: "fail"
|
||||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -11,17 +11,16 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [16]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Nodejs Env
|
||||||
|
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ env.NODE_VER }}
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-check:
|
version-check:
|
||||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||||
|
|||||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "assign me" it assigns the author to the
|
||||||
|
# ticket (case insensitive)
|
||||||
|
|
||||||
|
name: Assign comment author to ticket if they say "assign me"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
self_assign_by_comment:
|
||||||
|
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
module.config.js
|
module.config.js
|
||||||
|
.env.private
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
src/i18n/transifex_input.json
|
src/i18n/transifex_input.json
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# The following users are the owners of all frontend-app-authn files
|
# The following users are the owners of all frontend-app-authn files
|
||||||
* @edx/vanguards
|
* @openedx/vanguards
|
||||||
|
|||||||
31
Makefile
31
Makefile
@@ -1,6 +1,7 @@
|
|||||||
export TRANSIFEX_RESOURCE = frontend-app-authn
|
export TRANSIFEX_RESOURCE = frontend-app-authn
|
||||||
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
|
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
|
||||||
|
|
||||||
|
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
transifex_input = $(i18n)/transifex_input.json
|
||||||
@@ -42,11 +43,37 @@ push_translations:
|
|||||||
# Pushing comments to Transifex...
|
# Pushing comments to Transifex...
|
||||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
|
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||||
# Pulls translations from Transifex.
|
# Pulls translations from Transifex.
|
||||||
pull_translations:
|
pull_translations:
|
||||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
else
|
||||||
|
# Experimental: OEP-58 Pulls translations using atlas
|
||||||
|
pull_translations:
|
||||||
|
rm -rf src/i18n/messages
|
||||||
|
mkdir src/i18n/messages
|
||||||
|
cd src/i18n/messages \
|
||||||
|
&& atlas pull --filter=$(transifex_langs) \
|
||||||
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
|
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||||
|
|
||||||
# This target is used by CI.
|
$(intl_imports) paragon frontend-app-authn
|
||||||
|
endif
|
||||||
|
|
||||||
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
# Checking for package-lock.json changes...
|
# Checking for package-lock.json changes...
|
||||||
git diff --exit-code package-lock.json
|
git diff --exit-code package-lock.json
|
||||||
|
|
||||||
|
.PHONY: validate
|
||||||
|
validate:
|
||||||
|
make validate-no-uncommitted-package-lock-changes
|
||||||
|
npm run i18n_extract
|
||||||
|
npm run lint -- --max-warnings 0
|
||||||
|
npm run test
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
.PHONY: validate.ci
|
||||||
|
validate.ci:
|
||||||
|
npm ci
|
||||||
|
make validate
|
||||||
|
|||||||
53
README.rst
53
README.rst
@@ -72,8 +72,8 @@ The authentication micro-frontend also requires the following additional variabl
|
|||||||
- The fully-qualified URL to the password reset support page in the target environment.
|
- The fully-qualified URL to the password reset support page in the target environment.
|
||||||
- ``https://support.example.com``
|
- ``https://support.example.com``
|
||||||
|
|
||||||
* - ``WELCOME_PAGE_SUPPORT_LINK``
|
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
|
||||||
- The fully-qualified URL to the welcome support page in the target environment.
|
- The fully-qualified URL to the progressive profiling support page in the target environment.
|
||||||
- ``https://support.example.com``
|
- ``https://support.example.com``
|
||||||
|
|
||||||
* - ``TOS_AND_HONOR_CODE``
|
* - ``TOS_AND_HONOR_CODE``
|
||||||
@@ -96,7 +96,7 @@ The authentication micro-frontend also requires the following additional variabl
|
|||||||
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
|
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
* - ``ENABLE_PROGRESSIVE_PROFILING``
|
* - ``ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN``
|
||||||
- Enables support for progressive profiling. If enabled, users are redirected to a second page where data for optional registration fields can be collected.
|
- Enables support for progressive profiling. If enabled, users are redirected to a second page where data for optional registration fields can be collected.
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
@@ -104,6 +104,15 @@ The authentication micro-frontend also requires the following additional variabl
|
|||||||
- Disables the enterprise login from Authn MFE.
|
- Disables the enterprise login from Authn MFE.
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
|
* - ``MFE_CONFIG_API_URL``
|
||||||
|
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
|
||||||
|
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
|
* - ``APP_ID``
|
||||||
|
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
|
||||||
|
- ``authn`` | ``''``
|
||||||
|
|
||||||
|
|
||||||
edX-specific Environment Variables
|
edX-specific Environment Variables
|
||||||
**********************************
|
**********************************
|
||||||
|
|
||||||
@@ -121,14 +130,52 @@ Furthermore, there are several edX-specific environment variables that enable in
|
|||||||
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
|
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
|
||||||
|
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
|
||||||
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
For more information see the document: `Micro-frontend applications in Open
|
For more information see the document: `Micro-frontend applications in Open
|
||||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||||
|
|
||||||
|
How To Contribute
|
||||||
|
------------
|
||||||
|
Contributions are very welcome, and strongly encouraged! We've
|
||||||
|
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||||
|
|
||||||
|
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||||
|
|
||||||
|
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||||
|
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-authn/blob/master/.github/pull_request_template.md>`_
|
||||||
|
|
||||||
|
This project is currently accepting all types of contributions, bug fixes and security fixes.
|
||||||
|
|
||||||
|
Open edX Code of Conduct
|
||||||
|
------------------------
|
||||||
|
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
|
||||||
|
|
||||||
|
People
|
||||||
|
------
|
||||||
|
The assigned maintainers for this component and other project details may be
|
||||||
|
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||||
|
file in this repo.
|
||||||
|
|
||||||
|
Reporting Security Issues
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Please do not report security issues in public. Please email security@edx.org.
|
||||||
|
|
||||||
Known Issues
|
Known Issues
|
||||||
------------
|
------------
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||||
|
otherwise noted.
|
||||||
|
|
||||||
|
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||||
|
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
|
|||||||
18
catalog-info.yaml
Normal file
18
catalog-info.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# This file records information about this repo. Its use is described in OEP-55:
|
||||||
|
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||||
|
|
||||||
|
apiVersion: backstage.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: 'frontend-app-authn'
|
||||||
|
description: "Micro-frontend for authentication service. It contains views for login, registration and password reset functionality."
|
||||||
|
links:
|
||||||
|
- url: 'https://github.com/openedx/frontend-app-authn/blob/master/README.rst'
|
||||||
|
title: 'Documentation'
|
||||||
|
icon: 'Article'
|
||||||
|
annotations:
|
||||||
|
openedx.org/arch-interest-groups: ""
|
||||||
|
spec:
|
||||||
|
owner: group:vanguards
|
||||||
|
type: 'service'
|
||||||
|
lifecycle: 'production'
|
||||||
@@ -91,7 +91,7 @@ In the data sub-directory, the file names describe what each piece of code does.
|
|||||||
/ProfilePhotoUploader.jsx // supporting view
|
/ProfilePhotoUploader.jsx // supporting view
|
||||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||||
/actions.js
|
/actions.js
|
||||||
/constants.js
|
/mockedData.js
|
||||||
/reducers.js
|
/reducers.js
|
||||||
/sagas.js
|
/sagas.js
|
||||||
/selectors.js
|
/selectors.js
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
nick: Authn MFE
|
nick: Authn MFE
|
||||||
oeps: {}
|
oeps: {}
|
||||||
owner: edx/vanguards
|
owner: openedx/vanguards
|
||||||
openedx-release:
|
openedx-release:
|
||||||
ref: master
|
ref: master
|
||||||
|
|||||||
41910
package-lock.json
generated
41910
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@@ -32,57 +32,55 @@
|
|||||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
|
"@edx/frontend-platform": "^5.0.0",
|
||||||
"@edx/frontend-platform": "4.2.0",
|
"@edx/paragon": "20.46.2",
|
||||||
"@edx/paragon": "20.20.0",
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.2.1",
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@redux-devtools/extension": "3.2.3",
|
"@optimizely/react-sdk": "^2.9.1",
|
||||||
|
"@redux-devtools/extension": "3.2.5",
|
||||||
|
"@testing-library/react": "^12.1.5",
|
||||||
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
|
"algoliasearch": "^4.14.3",
|
||||||
|
"algoliasearch-helper": "^3.14.0",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"core-js": "3.26.1",
|
"core-js": "3.32.0",
|
||||||
"extract-react-intl-messages": "4.1.1",
|
|
||||||
"fastest-levenshtein": "1.0.16",
|
"fastest-levenshtein": "1.0.16",
|
||||||
"form-urlencoded": "4.2.1",
|
"form-urlencoded": "6.1.0",
|
||||||
"lodash.camelcase": "4.3.0",
|
|
||||||
"lodash.snakecase": "4.1.1",
|
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "5.1.1",
|
"query-string": "7.1.3",
|
||||||
"react": "16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-loading-skeleton": "2.2.0",
|
"react-loading-skeleton": "3.3.1",
|
||||||
"react-onclickoutside": "6.12.2",
|
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-router": "5.3.4",
|
"react-router": "6.15.0",
|
||||||
"react-router-dom": "5.3.4",
|
"react-router-dom": "6.15.0",
|
||||||
|
"react-zendesk": "^0.1.13",
|
||||||
"redux": "4.2.0",
|
"redux": "4.2.0",
|
||||||
"redux-logger": "3.0.6",
|
"redux-logger": "3.0.6",
|
||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.4",
|
||||||
"redux-saga": "1.2.1",
|
"redux-saga": "1.2.3",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "0.13.11",
|
"regenerator-runtime": "0.14.0",
|
||||||
"reselect": "4.1.7",
|
"reselect": "4.1.8",
|
||||||
"sanitize-html": "2.7.3",
|
|
||||||
"semver-regex": "3.1.4",
|
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/frontend-build": "11.0.2",
|
"@edx/frontend-build": "12.9.8",
|
||||||
"@edx/reactifex": "1.1.0",
|
"@edx/reactifex": "1.1.0",
|
||||||
"babel-plugin-formatjs": "10.3.31",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||||
|
"babel-plugin-formatjs": "10.5.3",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.7",
|
"eslint-plugin-import": "2.28.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest": "27.5.1",
|
"jest": "29.6.2",
|
||||||
"react-test-renderer": "16.14.0"
|
"react-test-renderer": "^17.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
<title>Authn | <%= process.env.SITE_NAME %></title>
|
<title>Authn | <%= process.env.SITE_NAME %></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
|
||||||
|
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer">
|
||||||
|
</script>
|
||||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||||
<script
|
<script
|
||||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||||
@@ -13,58 +18,6 @@
|
|||||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||||
></script>
|
></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% if (process.env.ZENDESK_KEY) { %>
|
|
||||||
<script
|
|
||||||
id="ze-snippet"
|
|
||||||
src="https://static.zdassets.com/ekr/snippet.js?key=<%= process.env.ZENDESK_KEY %>"
|
|
||||||
>
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.zESettings = {
|
|
||||||
cookies: true,
|
|
||||||
webWidget: {
|
|
||||||
contactOptions: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
suppress: false,
|
|
||||||
},
|
|
||||||
contactForm: {
|
|
||||||
ticketForms: [
|
|
||||||
{
|
|
||||||
id: 360003368814,
|
|
||||||
subject: false,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
id: 'description',
|
|
||||||
prefill: {
|
|
||||||
'*': '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectTicketForm: {
|
|
||||||
'*': 'Please choose your request type:',
|
|
||||||
},
|
|
||||||
attachments: true,
|
|
||||||
},
|
|
||||||
helpCenter: {
|
|
||||||
originalArticleButton: true,
|
|
||||||
},
|
|
||||||
answerBot: {
|
|
||||||
suppress: false,
|
|
||||||
contactOnlyAfterQuery: true,
|
|
||||||
title: { '*': 'edX Support' },
|
|
||||||
avatar: {
|
|
||||||
url: '<%= process.env.ZENDESK_LOGO_URL %>',
|
|
||||||
name: { '*': 'edX Support' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<% } %>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,19 +3,30 @@ import React from 'react';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute,
|
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||||
} from './common-components';
|
} from './common-components';
|
||||||
import configureStore from './data/configureStore';
|
import configureStore from './data/configureStore';
|
||||||
import {
|
import {
|
||||||
LOGIN_PAGE, PAGE_NOT_FOUND, PASSWORD_RESET_CONFIRM, REGISTER_PAGE, RESET_PAGE, WELCOME_PAGE,
|
AUTHN_PROGRESSIVE_PROFILING,
|
||||||
|
LOGIN_PAGE,
|
||||||
|
PAGE_NOT_FOUND,
|
||||||
|
PASSWORD_RESET_CONFIRM,
|
||||||
|
RECOMMENDATIONS,
|
||||||
|
REGISTER_EMBEDDED_PAGE,
|
||||||
|
REGISTER_PAGE,
|
||||||
|
RESET_PAGE,
|
||||||
} from './data/constants';
|
} from './data/constants';
|
||||||
import { updatePathWithQueryParams } from './data/utils';
|
import { updatePathWithQueryParams } from './data/utils';
|
||||||
import ForgotPasswordPage from './forgot-password';
|
import { ForgotPasswordPage } from './forgot-password';
|
||||||
import ResetPasswordPage from './reset-password';
|
import Logistration from './logistration/Logistration';
|
||||||
import { ProgressiveProfiling } from './welcome';
|
import { ProgressiveProfiling } from './progressive-profiling';
|
||||||
|
import { RecommendationsPage } from './recommendations';
|
||||||
|
import { EmbeddableRegistrationPage } from './register';
|
||||||
|
import { ResetPasswordPage } from './reset-password';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
registerIcons();
|
registerIcons();
|
||||||
@@ -25,20 +36,27 @@ const MainApp = () => (
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Switch>
|
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||||
<Route exact path="/">
|
<Routes>
|
||||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||||
</Route>
|
<Route
|
||||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
|
path={REGISTER_EMBEDDED_PAGE}
|
||||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
|
||||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
/>
|
||||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
<Route
|
||||||
<Route exact path={WELCOME_PAGE} component={ProgressiveProfiling} />
|
path={LOGIN_PAGE}
|
||||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
element={
|
||||||
<Route path="*">
|
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||||
<Redirect to={PAGE_NOT_FOUND} />
|
}
|
||||||
</Route>
|
/>
|
||||||
</Switch>
|
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||||
|
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||||
|
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||||
|
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||||
|
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||||
|
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||||
|
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||||
|
</Routes>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const AuthLargeLayout = ({ intl, username }) => (
|
|
||||||
<div className="w-50 d-flex">
|
|
||||||
<div className="col-md-10 bg-light-200 p-0">
|
|
||||||
<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">
|
|
||||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
|
||||||
</h1>
|
|
||||||
<h2 className="complete-your-profile">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
|
||||||
<div className="text-accent-a">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-2 bg-white p-0">
|
|
||||||
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
|
||||||
<g transform="skewX(171.6)">
|
|
||||||
<rect x="0" y="0" height="100%" width="100%" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
AuthLargeLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AuthLargeLayout);
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const AuthMediumLayout = ({ intl, username }) => (
|
|
||||||
<>
|
|
||||||
<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={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">
|
|
||||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
|
||||||
</h1>
|
|
||||||
<h2 className="display-1">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
|
||||||
<div className="text-accent-a">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-2 bg-white p-0">
|
|
||||||
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
|
||||||
<g transform="skewX(168)">
|
|
||||||
<rect x="0" y="0" height="100%" width="100%" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
AuthMediumLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AuthMediumLayout);
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const AuthSmallLayout = ({ intl, username }) => (
|
|
||||||
<div className="min-vw-100 bg-light-200">
|
|
||||||
<div className="col-md-12 small-screen-top-stripe" />
|
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
|
||||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
|
||||||
</Hyperlink>
|
|
||||||
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
|
||||||
<div className="small-yellow-line mt-4.5" />
|
|
||||||
<div>
|
|
||||||
<h1 className="h5 data-hj-suppress">
|
|
||||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
|
||||||
</h1>
|
|
||||||
<h2 className="h1">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
|
||||||
<div className="text-accent-a">
|
|
||||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
|
||||||
</div>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
AuthSmallLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(AuthSmallLayout);
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
|
||||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
|
||||||
import { breakpoints } from '@edx/paragon';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import MediaQuery from 'react-responsive';
|
|
||||||
|
|
||||||
import AuthLargeLayout from './AuthLargeLayout';
|
|
||||||
import AuthMediumLayout from './AuthMediumLayout';
|
|
||||||
import AuthSmallLayout from './AuthSmallLayout';
|
|
||||||
import LargeLayout from './LargeLayout';
|
|
||||||
import MediumLayout from './MediumLayout';
|
|
||||||
import SmallLayout from './SmallLayout';
|
|
||||||
|
|
||||||
const BaseComponent = ({ children, showWelcomeBanner }) => {
|
|
||||||
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
|
||||||
const username = authenticatedUser ? authenticatedUser.username : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{getConfig().ENABLE_COOKIE_POLICY_BANNER ? <CookiePolicyBanner languageCode={getLocale()} /> : null}
|
|
||||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
|
||||||
<div className="layout">
|
|
||||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
|
||||||
{authenticatedUser ? <AuthSmallLayout username={username} /> : <SmallLayout />}
|
|
||||||
</MediaQuery>
|
|
||||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
|
||||||
{authenticatedUser ? <AuthMediumLayout username={username} /> : <MediumLayout />}
|
|
||||||
</MediaQuery>
|
|
||||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
|
||||||
{authenticatedUser ? <AuthLargeLayout username={username} /> : <LargeLayout />}
|
|
||||||
</MediaQuery>
|
|
||||||
|
|
||||||
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseComponent.defaultProps = {
|
|
||||||
showWelcomeBanner: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseComponent.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
showWelcomeBanner: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BaseComponent;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const LargeLayout = ({ intl }) => (
|
|
||||||
<div className="w-50 d-flex">
|
|
||||||
<div className="col-md-9 bg-primary-400">
|
|
||||||
<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': getConfig().SITE_NAME === 'edX' })} />
|
|
||||||
<h1
|
|
||||||
className={classNames(
|
|
||||||
'display-2 text-white mw-xs',
|
|
||||||
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages['start.learning'])}
|
|
||||||
<div className="text-accent-a">
|
|
||||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
|
||||||
</div>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-3 bg-white p-0">
|
|
||||||
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
|
||||||
<g transform="skewX(171.6)">
|
|
||||||
<rect x="0" y="0" height="100%" width="100%" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
LargeLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(LargeLayout);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const MediumLayout = ({ intl }) => (
|
|
||||||
<>
|
|
||||||
<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={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': getConfig().SITE_NAME === 'edX' })} />
|
|
||||||
<div>
|
|
||||||
<h1
|
|
||||||
className={classNames(
|
|
||||||
'display-1 text-white mt-5 mb-5 mr-2',
|
|
||||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="mr-2">{intl.formatMessage(messages['start.learning'])}</span>
|
|
||||||
<span className="text-accent-a d-inline-block">
|
|
||||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-2 bg-white p-0">
|
|
||||||
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
|
||||||
<g transform="skewX(168)">
|
|
||||||
<rect x="0" y="0" height="100%" width="100%" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
MediumLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(MediumLayout);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const SmallLayout = ({ intl }) => (
|
|
||||||
<span className="bg-primary-400 w-100">
|
|
||||||
<div className="col-md-12 small-screen-top-stripe" />
|
|
||||||
<div>
|
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
|
||||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
|
||||||
</Hyperlink>
|
|
||||||
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
|
||||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
|
||||||
<h1
|
|
||||||
className={classNames(
|
|
||||||
'text-white mt-3.5 mb-3.5',
|
|
||||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="mr-1">{intl.formatMessage(messages['start.learning'])}</span>
|
|
||||||
<span className="text-accent-a d-inline-block">
|
|
||||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
SmallLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(SmallLayout);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './BaseComponent';
|
|
||||||
@@ -3,16 +3,14 @@ import React from 'react';
|
|||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import LargeLayout from '../LargeLayout';
|
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||||
import MediumLayout from '../MediumLayout';
|
|
||||||
import SmallLayout from '../SmallLayout';
|
|
||||||
|
|
||||||
describe('ScreenLayout', () => {
|
describe('Default Layout tests', () => {
|
||||||
it('should display the form, pass as a child in SmallScreenLayout', () => {
|
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||||
const smallScreen = mount(
|
const smallScreen = mount(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<div>
|
<div>
|
||||||
<SmallLayout />
|
<DefaultSmallLayout />
|
||||||
<form>
|
<form>
|
||||||
<input type="text" />
|
<input type="text" />
|
||||||
</form>
|
</form>
|
||||||
@@ -22,11 +20,11 @@ describe('ScreenLayout', () => {
|
|||||||
expect(smallScreen.find('form').exists()).toEqual(true);
|
expect(smallScreen.find('form').exists()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the form, pass as a child in MediumScreenLayout', () => {
|
it('should display the form passed as a child in MediumScreenLayout', () => {
|
||||||
const mediumScreen = mount(
|
const mediumScreen = mount(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<div>
|
<div>
|
||||||
<MediumLayout />
|
<DefaultMediumLayout />
|
||||||
<form>
|
<form>
|
||||||
<input type="text" />
|
<input type="text" />
|
||||||
</form>
|
</form>
|
||||||
@@ -36,11 +34,11 @@ describe('ScreenLayout', () => {
|
|||||||
expect(mediumScreen.find('form').exists()).toEqual(true);
|
expect(mediumScreen.find('form').exists()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the form, pass as a child in LargeScreenLayout', () => {
|
it('should display the form passed as a child in LargeScreenLayout', () => {
|
||||||
const largeScreen = mount(
|
const largeScreen = mount(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<div>
|
<div>
|
||||||
<LargeLayout />
|
<DefaultLargeLayout />
|
||||||
<form>
|
<form>
|
||||||
<input type="text" />
|
<input type="text" />
|
||||||
</form>
|
</form>
|
||||||
45
src/base-container/components/default-layout/LargeLayout.jsx
Normal file
45
src/base-container/components/default-layout/LargeLayout.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const LargeLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-50 d-flex">
|
||||||
|
<div className="col-md-9 bg-primary-400">
|
||||||
|
<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': getConfig().SITE_NAME === 'edX' })} />
|
||||||
|
<h1
|
||||||
|
className={classNames(
|
||||||
|
'display-2 text-white mw-xs',
|
||||||
|
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatMessage(messages['start.learning'])}
|
||||||
|
<div className="text-accent-a">
|
||||||
|
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3 bg-white p-0">
|
||||||
|
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||||
|
<g transform="skewX(171.6)">
|
||||||
|
<rect x="0" y="0" height="100%" width="100%" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LargeLayout;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const MediumLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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={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': getConfig().SITE_NAME === 'edX' })} />
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
className={classNames(
|
||||||
|
'display-1 text-white mt-5 mb-5 mr-2',
|
||||||
|
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
|
||||||
|
<span className="text-accent-a d-inline-block">
|
||||||
|
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2 bg-white p-0">
|
||||||
|
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||||
|
<g transform="skewX(168)">
|
||||||
|
<rect x="0" y="0" height="100%" width="100%" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediumLayout;
|
||||||
39
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
39
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const SmallLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="bg-primary-400 w-100">
|
||||||
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
|
<div>
|
||||||
|
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||||
|
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||||
|
</Hyperlink>
|
||||||
|
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||||
|
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||||
|
<h1
|
||||||
|
className={classNames(
|
||||||
|
'text-white mt-3.5 mb-3.5',
|
||||||
|
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{formatMessage(messages['start.learning'])}</span>
|
||||||
|
<span className="text-accent-a d-inline-block">
|
||||||
|
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SmallLayout;
|
||||||
3
src/base-container/components/default-layout/index.jsx
Normal file
3
src/base-container/components/default-layout/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as DefaultLargeLayout } from './LargeLayout';
|
||||||
|
export { default as DefaultMediumLayout } from './MediumLayout';
|
||||||
|
export { default as DefaultSmallLayout } from './SmallLayout';
|
||||||
16
src/base-container/components/default-layout/messages.js
Normal file
16
src/base-container/components/default-layout/messages.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
'start.learning': {
|
||||||
|
id: 'start.learning',
|
||||||
|
defaultMessage: 'Start learning',
|
||||||
|
description: 'Header text for logistration MFE pages',
|
||||||
|
},
|
||||||
|
'with.site.name': {
|
||||||
|
id: 'with.site.name',
|
||||||
|
defaultMessage: 'with {siteName}',
|
||||||
|
description: 'Header text with site name for logistration MFE pages',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const ExtraSmallLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
||||||
|
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<span className="text-light-500">
|
||||||
|
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||||
|
</span>
|
||||||
|
<span className="text-warning-300">
|
||||||
|
{formatMessage(messages['is.here'])}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtraSmallLayout;
|
||||||
35
src/base-container/components/image-layout/LargeLayout.jsx
Normal file
35
src/base-container/components/image-layout/LargeLayout.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const LargeLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-50 bg-primary-500 banner__image large-layout"
|
||||||
|
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<span className="text-light-500">
|
||||||
|
{formatMessage(messages['your.career.turning.point'])}
|
||||||
|
</span>
|
||||||
|
<span className="text-warning-300">
|
||||||
|
{formatMessage(messages['is.here'])}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LargeLayout;
|
||||||
35
src/base-container/components/image-layout/MediumLayout.jsx
Normal file
35
src/base-container/components/image-layout/MediumLayout.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const MediumLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
||||||
|
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<span className="text-light-500">
|
||||||
|
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||||
|
</span>
|
||||||
|
<span className="text-warning-300 d-inline-block">
|
||||||
|
{formatMessage(messages['is.here'])}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediumLayout;
|
||||||
34
src/base-container/components/image-layout/SmallLayout.jsx
Normal file
34
src/base-container/components/image-layout/SmallLayout.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const SmallLayout = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="w-100 bg-primary-500 banner__image small-layout"
|
||||||
|
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<span className="text-light-500">
|
||||||
|
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||||
|
</span>
|
||||||
|
<span className="text-warning-300">
|
||||||
|
{formatMessage(messages['is.here'])}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SmallLayout;
|
||||||
4
src/base-container/components/image-layout/index.jsx
Normal file
4
src/base-container/components/image-layout/index.jsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as ImageLargeLayout } from './LargeLayout';
|
||||||
|
export { default as ImageMediumLayout } from './MediumLayout';
|
||||||
|
export { default as ImageSmallLayout } from './SmallLayout';
|
||||||
|
export { default as ImageExtraSmallLayout } from './ExtraSmallLayout';
|
||||||
37
src/base-container/components/image-layout/index.scss
Normal file
37
src/base-container/components/image-layout/index.scss
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
.company-logo {
|
||||||
|
width: 71px;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.company-logo {
|
||||||
|
width: 44.67px;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner__image {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 464px) and (max-width: 575.98px) {
|
||||||
|
.banner__heading {
|
||||||
|
font-size: 60px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 60px;
|
||||||
|
letter-spacing: -1.2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 800px) {
|
||||||
|
.banner__heading {
|
||||||
|
font-size: 60px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
line-height: 60px !important;
|
||||||
|
letter-spacing: -2px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/base-container/components/image-layout/messages.js
Normal file
16
src/base-container/components/image-layout/messages.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
'your.career.turning.point': {
|
||||||
|
id: 'your.career.turning.point',
|
||||||
|
defaultMessage: 'Your career turning point',
|
||||||
|
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||||
|
},
|
||||||
|
'is.here': {
|
||||||
|
id: 'is.here',
|
||||||
|
defaultMessage: 'is here.',
|
||||||
|
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const LargeLayout = ({ username }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-50 d-flex">
|
||||||
|
<div className="col-md-10 bg-light-200 p-0">
|
||||||
|
<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: getConfig().SITE_NAME, username })}
|
||||||
|
</h1>
|
||||||
|
<h2 className="complete-your-profile">
|
||||||
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
<div className="text-accent-a">
|
||||||
|
{formatMessage(messages['complete.your.profile.2'])}
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2 bg-white p-0">
|
||||||
|
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||||
|
<g transform="skewX(171.6)">
|
||||||
|
<rect x="0" y="0" height="100%" width="100%" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LargeLayout.propTypes = {
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LargeLayout;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const MediumLayout = ({ username }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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={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: getConfig().SITE_NAME, username })}
|
||||||
|
</h1>
|
||||||
|
<h2 className="display-1">
|
||||||
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
<div className="text-accent-a">
|
||||||
|
{formatMessage(messages['complete.your.profile.2'])}
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2 bg-white p-0">
|
||||||
|
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||||
|
<g transform="skewX(168)">
|
||||||
|
<rect x="0" y="0" height="100%" width="100%" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MediumLayout.propTypes = {
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MediumLayout;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const SmallLayout = ({ username }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-vw-100 bg-light-200">
|
||||||
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
|
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||||
|
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||||
|
</Hyperlink>
|
||||||
|
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||||
|
<div className="small-yellow-line mt-4.5" />
|
||||||
|
<div>
|
||||||
|
<h1 className="h5 data-hj-suppress">
|
||||||
|
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||||
|
</h1>
|
||||||
|
<h2 className="h1">
|
||||||
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
<div className="text-accent-a">
|
||||||
|
{formatMessage(messages['complete.your.profile.2'])}
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SmallLayout.propTypes = {
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SmallLayout;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as AuthLargeLayout } from './LargeLayout';
|
||||||
|
export { default as AuthMediumLayout } from './MediumLayout';
|
||||||
|
export { default as AuthSmallLayout } from './SmallLayout';
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
'start.learning': {
|
'welcome.to.platform': {
|
||||||
id: 'start.learning',
|
id: 'welcome.to.platform',
|
||||||
defaultMessage: 'Start learning',
|
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||||
description: 'Header text for logistration MFE pages',
|
description: 'Welcome message that appears on progressive profile page',
|
||||||
},
|
},
|
||||||
'with.site.name': {
|
|
||||||
id: 'with.site.name',
|
|
||||||
defaultMessage: 'with {siteName}',
|
|
||||||
description: 'Header text with site name for logistration MFE pages',
|
|
||||||
},
|
|
||||||
// authenticated user base component text
|
|
||||||
'complete.your.profile.1': {
|
'complete.your.profile.1': {
|
||||||
id: 'complete.your.profile.1',
|
id: 'complete.your.profile.1',
|
||||||
defaultMessage: 'Complete',
|
defaultMessage: 'Complete',
|
||||||
@@ -22,11 +16,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'your profile',
|
defaultMessage: 'your profile',
|
||||||
description: 'part of text "complete your profile"',
|
description: 'part of text "complete your profile"',
|
||||||
},
|
},
|
||||||
'welcome.to.platform': {
|
|
||||||
id: 'welcome.to.platform',
|
|
||||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
|
||||||
description: 'Welcome message that appears on progressive profile page',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
4
src/base-container/data/constants.js
Normal file
4
src/base-container/data/constants.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const IMAGE_LAYOUT = 'image-layout';
|
||||||
|
const DEFAULT_LAYOUT = 'default-layout';
|
||||||
|
|
||||||
|
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };
|
||||||
87
src/base-container/index.jsx
Normal file
87
src/base-container/index.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { breakpoints } from '@edx/paragon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import MediaQuery from 'react-responsive';
|
||||||
|
|
||||||
|
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './components/default-layout';
|
||||||
|
import {
|
||||||
|
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
|
||||||
|
} from './components/image-layout';
|
||||||
|
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||||
|
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
|
||||||
|
|
||||||
|
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||||
|
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initRebrandExperiment = () => {
|
||||||
|
if (window.experiments?.rebrandExperiment) {
|
||||||
|
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||||
|
} else {
|
||||||
|
window.experiments = window.experiments || {};
|
||||||
|
window.experiments.rebrandExperiment = {};
|
||||||
|
window.experiments.rebrandExperiment.handleLoaded = () => {
|
||||||
|
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initRebrandExperiment();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (baseContainerVersion === IMAGE_LAYOUT) {
|
||||||
|
return (
|
||||||
|
<div className="layout">
|
||||||
|
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
|
||||||
|
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
|
||||||
|
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||||
|
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||||
|
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <ImageLargeLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||||
|
<div className="layout">
|
||||||
|
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||||
|
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <DefaultSmallLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||||
|
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||||
|
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
|
||||||
|
</MediaQuery>
|
||||||
|
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseContainer.defaultProps = {
|
||||||
|
showWelcomeBanner: false,
|
||||||
|
username: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseContainer.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
showWelcomeBanner: PropTypes.bool,
|
||||||
|
username: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseContainer;
|
||||||
43
src/base-container/tests/BaseContainer.test.jsx
Normal file
43
src/base-container/tests/BaseContainer.test.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
|
||||||
|
import BaseContainer from '../index';
|
||||||
|
|
||||||
|
const LargeScreen = {
|
||||||
|
wrappingComponent: ResponsiveContext.Provider,
|
||||||
|
wrappingComponentProps: { value: { width: 1200 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Base component tests', () => {
|
||||||
|
it('should should default layout', () => {
|
||||||
|
const baseContainer = mount(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<BaseContainer />
|
||||||
|
</IntlProvider>,
|
||||||
|
LargeScreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
|
||||||
|
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('[experiment] should show image layout for treatment group', () => {
|
||||||
|
window.experiments = {
|
||||||
|
rebrandExperiment: {
|
||||||
|
variation: 'image-layout',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseContainer = mount(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<BaseContainer />
|
||||||
|
</IntlProvider>,
|
||||||
|
LargeScreen,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/common-components/EmbeddedRegistrationRoute.jsx
Normal file
28
src/common-components/EmbeddedRegistrationRoute.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PAGE_NOT_FOUND } from '../data/constants';
|
||||||
|
import { isHostAvailableInQueryParams } from '../data/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This wrapper redirects the requester to embedded register page only if host
|
||||||
|
* query param is present.
|
||||||
|
*/
|
||||||
|
const EmbeddedRegistrationRoute = ({ children }) => {
|
||||||
|
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||||
|
|
||||||
|
// Show registration page for embedded experience even if the user is authenticated
|
||||||
|
if (registrationEmbedded) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Navigate to={PAGE_NOT_FOUND} replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbeddedRegistrationRoute.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbeddedRegistrationRoute;
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Button, Form,
|
Button, Form,
|
||||||
|
Icon,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
import { Login } from '@edx/paragon/icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
|
||||||
import messages from './messages';
|
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
|
||||||
|
* */
|
||||||
const EnterpriseSSO = (props) => {
|
const EnterpriseSSO = (props) => {
|
||||||
const { intl } = props;
|
const { formatMessage } = useIntl();
|
||||||
const tpaProvider = props.provider;
|
const tpaProvider = props.provider;
|
||||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ const EnterpriseSSO = (props) => {
|
|||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<div className="mw-450">
|
<div className="mw-450">
|
||||||
<Form className="m-0">
|
<Form className="m-0">
|
||||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||||
<Button
|
<Button
|
||||||
id={tpaProvider.id}
|
id={tpaProvider.id}
|
||||||
key={tpaProvider.id}
|
key={tpaProvider.id}
|
||||||
@@ -44,16 +48,18 @@ const EnterpriseSSO = (props) => {
|
|||||||
>
|
>
|
||||||
{tpaProvider.iconImage ? (
|
{tpaProvider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<div className="font-container" aria-hidden="true">
|
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||||
<FontAwesomeIcon
|
{SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
|
||||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
<FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
|
||||||
/>
|
: (
|
||||||
|
<Icon className="h-75" src={Login} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||||
</>
|
</>
|
||||||
@@ -69,8 +75,8 @@ const EnterpriseSSO = (props) => {
|
|||||||
onClick={(e) => handleClick(e)}
|
onClick={(e) => handleClick(e)}
|
||||||
>
|
>
|
||||||
{disablePublicAccountCreation
|
{disablePublicAccountCreation
|
||||||
? intl.formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||||
: intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +107,6 @@ EnterpriseSSO.propTypes = {
|
|||||||
loginUrl: PropTypes.string,
|
loginUrl: PropTypes.string,
|
||||||
registerUrl: PropTypes.string,
|
registerUrl: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(EnterpriseSSO);
|
export default EnterpriseSSO;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const FormGroup = (props) => {
|
|||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
type={props.type}
|
type={props.type}
|
||||||
aria-invalid={props.errorMessage !== ''}
|
aria-invalid={props.errorMessage !== ''}
|
||||||
className="form-field"
|
className="form-group__form-field"
|
||||||
autoComplete={props.autoComplete}
|
autoComplete={props.autoComplete}
|
||||||
spellCheck={props.spellCheck}
|
spellCheck={props.spellCheck}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
@@ -37,7 +37,6 @@ const FormGroup = (props) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onChange={props.handleChange}
|
onChange={props.handleChange}
|
||||||
controlClassName={props.borderClass}
|
controlClassName={props.borderClass}
|
||||||
|
|
||||||
trailingElement={props.trailingElement}
|
trailingElement={props.trailingElement}
|
||||||
floatingLabel={props.floatingLabel}
|
floatingLabel={props.floatingLabel}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||||
import { Institution } from '@edx/paragon/icons';
|
import { Institution } from '@edx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the Institution login button
|
||||||
|
* */
|
||||||
export const RenderInstitutionButton = props => {
|
export const RenderInstitutionButton = props => {
|
||||||
const { onSubmitHandler, buttonTitle } = props;
|
const { onSubmitHandler, buttonTitle } = props;
|
||||||
|
|
||||||
@@ -24,10 +27,13 @@ export const RenderInstitutionButton = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the page list of available institutions for login
|
||||||
|
* */
|
||||||
const InstitutionLogistration = props => {
|
const InstitutionLogistration = props => {
|
||||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const {
|
const {
|
||||||
intl,
|
|
||||||
secondaryProviders,
|
secondaryProviders,
|
||||||
headingTitle,
|
headingTitle,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -36,11 +42,11 @@ const InstitutionLogistration = props => {
|
|||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-left mb-4 mt-2">
|
<div className="d-flex justify-content-left mb-4 mt-2">
|
||||||
<div className="flex-column">
|
<div className="flex-column">
|
||||||
<h4 className="mb-2 font-weight-bold institute-heading">
|
<h4 className="mb-2 font-weight-bold institutions__heading">
|
||||||
{headingTitle}
|
{headingTitle}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +57,7 @@ const InstitutionLogistration = props => {
|
|||||||
<tr key={provider} className="pgn__data-table-row">
|
<tr key={provider} className="pgn__data-table-row">
|
||||||
<td>
|
<td>
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
className="btn nav-item p-0 mb-1 secondary-provider-link"
|
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||||
destination={lmsBaseUrl + provider.loginUrl}
|
destination={lmsBaseUrl + provider.loginUrl}
|
||||||
>
|
>
|
||||||
{provider.name}
|
{provider.name}
|
||||||
@@ -89,7 +95,6 @@ RenderInstitutionButton.defaultProps = {
|
|||||||
|
|
||||||
InstitutionLogistration.propTypes = {
|
InstitutionLogistration.propTypes = {
|
||||||
...LogistrationProps,
|
...LogistrationProps,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
headingTitle: PropTypes.string,
|
headingTitle: PropTypes.string,
|
||||||
};
|
};
|
||||||
InstitutionLogistration.defaultProps = {
|
InstitutionLogistration.defaultProps = {
|
||||||
@@ -97,4 +102,4 @@ InstitutionLogistration.defaultProps = {
|
|||||||
headingTitle: '',
|
headingTitle: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(InstitutionLogistration);
|
export default InstitutionLogistration;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
const NotFoundPage = () => (
|
||||||
return (
|
|
||||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||||
<p className="my-0 py-5 text-muted mw-32em">
|
<p className="my-0 py-5 text-muted mw-32em">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -14,4 +13,5 @@ export default function NotFoundPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
export default NotFoundPage;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
@@ -9,33 +10,94 @@ import {
|
|||||||
} from '@edx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
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 PasswordField = (props) => {
|
||||||
const { formatMessage } = props.intl;
|
const { formatMessage } = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
const handleBlur = (e) => {
|
const handleBlur = (e) => {
|
||||||
if (props.handleBlur) { props.handleBlur(e); }
|
const { name, value } = e.target;
|
||||||
|
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||||
|
return; // Do not run validations on password icon click
|
||||||
|
}
|
||||||
|
|
||||||
|
let passwordValue = value;
|
||||||
|
if (name === 'passwordIcon') {
|
||||||
|
// To validate actual password value when onBlur is triggered by focusing out the password icon
|
||||||
|
passwordValue = props.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.handleBlur) {
|
||||||
|
props.handleBlur({
|
||||||
|
target: {
|
||||||
|
name: props.name,
|
||||||
|
value: passwordValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setShowTooltip(props.showRequirements && false);
|
setShowTooltip(props.showRequirements && false);
|
||||||
|
if (props.handleErrorChange) { // If rendering from register page
|
||||||
|
const fieldError = validatePasswordField(passwordValue, formatMessage);
|
||||||
|
if (fieldError) {
|
||||||
|
props.handleErrorChange('password', fieldError);
|
||||||
|
} else if (!validationApiRateLimited) {
|
||||||
|
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = (e) => {
|
const handleFocus = (e) => {
|
||||||
|
if (e.target?.name === 'passwordIcon') {
|
||||||
|
return; // Do not clear error on password icon focus
|
||||||
|
}
|
||||||
|
|
||||||
if (props.handleFocus) {
|
if (props.handleFocus) {
|
||||||
props.handleFocus(e);
|
props.handleFocus(e);
|
||||||
}
|
}
|
||||||
|
if (props.handleErrorChange) {
|
||||||
|
props.handleErrorChange('password', '');
|
||||||
|
dispatch(clearRegistrationBackendError('password'));
|
||||||
|
}
|
||||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HideButton = (
|
const HideButton = (
|
||||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
<IconButton
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
name="passwordIcon"
|
||||||
|
src={VisibilityOff}
|
||||||
|
iconAs={Icon}
|
||||||
|
onClick={setHiddenTrue}
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
alt={formatMessage(messages['hide.password'])}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ShowButton = (
|
const ShowButton = (
|
||||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
<IconButton
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
name="passwordIcon"
|
||||||
|
src={Visibility}
|
||||||
|
iconAs={Icon}
|
||||||
|
onClick={setHiddenFalse}
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
alt={formatMessage(messages['show.password'])}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Tooltip id={`password-requirement-${placement}`}>
|
<Tooltip id={`password-requirement-${placement}`}>
|
||||||
@@ -59,7 +121,7 @@ const PasswordField = (props) => {
|
|||||||
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="input"
|
as="input"
|
||||||
className="form-field"
|
className="form-group__form-field"
|
||||||
type={isPasswordHidden ? 'password' : 'text'}
|
type={isPasswordHidden ? 'password' : 'text'}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
@@ -89,6 +151,7 @@ PasswordField.defaultProps = {
|
|||||||
handleBlur: null,
|
handleBlur: null,
|
||||||
handleFocus: null,
|
handleFocus: null,
|
||||||
handleChange: () => {},
|
handleChange: () => {},
|
||||||
|
handleErrorChange: null,
|
||||||
showRequirements: true,
|
showRequirements: true,
|
||||||
autoComplete: null,
|
autoComplete: null,
|
||||||
};
|
};
|
||||||
@@ -100,11 +163,11 @@ PasswordField.propTypes = {
|
|||||||
handleBlur: PropTypes.func,
|
handleBlur: PropTypes.func,
|
||||||
handleFocus: PropTypes.func,
|
handleFocus: PropTypes.func,
|
||||||
handleChange: PropTypes.func,
|
handleChange: PropTypes.func,
|
||||||
intl: intlShape.isRequired,
|
handleErrorChange: PropTypes.func,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
showRequirements: PropTypes.bool,
|
showRequirements: PropTypes.bool,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
autoComplete: PropTypes.string,
|
autoComplete: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(PasswordField);
|
export default PasswordField;
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { WELCOME_PAGE } from '../data/constants';
|
import {
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
|
||||||
|
} from '../data/constants';
|
||||||
import { setCookie } from '../data/utils';
|
import { setCookie } from '../data/utils';
|
||||||
|
|
||||||
function RedirectLogistration(props) {
|
const RedirectLogistration = (props) => {
|
||||||
const {
|
const {
|
||||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success, optionalFields,
|
authenticatedUser,
|
||||||
|
finishAuthUrl,
|
||||||
|
redirectUrl,
|
||||||
|
redirectToProgressiveProfilingPage,
|
||||||
|
success,
|
||||||
|
optionalFields,
|
||||||
|
redirectToRecommendationsPage,
|
||||||
|
educationLevel,
|
||||||
|
userId,
|
||||||
} = props;
|
} = props;
|
||||||
let finalRedirectUrl = '';
|
let finalRedirectUrl = '';
|
||||||
|
|
||||||
@@ -24,42 +32,69 @@ function RedirectLogistration(props) {
|
|||||||
finalRedirectUrl = redirectUrl;
|
finalRedirectUrl = redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectToWelcomePage) {
|
// Redirect to Progressive Profiling after successful registration
|
||||||
|
if (redirectToProgressiveProfilingPage) {
|
||||||
|
// TODO: Do we still need this cookie?
|
||||||
setCookie('van-504-returning-user', true);
|
setCookie('van-504-returning-user', true);
|
||||||
// use this component to redirect WelcomePage after successful registration
|
|
||||||
// return <Redirect to={WELCOME_PAGE} />;
|
|
||||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||||
return (
|
return (
|
||||||
<Redirect to={{
|
<Navigate
|
||||||
pathname: WELCOME_PAGE,
|
to={AUTHN_PROGRESSIVE_PROFILING}
|
||||||
state: {
|
state={{
|
||||||
registrationResult,
|
registrationResult,
|
||||||
optionalFields,
|
optionalFields,
|
||||||
},
|
authenticatedUser,
|
||||||
}}
|
}}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to Recommendation page
|
||||||
|
if (redirectToRecommendationsPage) {
|
||||||
|
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to={RECOMMENDATIONS}
|
||||||
|
state={{
|
||||||
|
registrationResult,
|
||||||
|
educationLevel,
|
||||||
|
userId,
|
||||||
|
}}
|
||||||
|
replace
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = finalRedirectUrl;
|
window.location.href = finalRedirectUrl;
|
||||||
}
|
}
|
||||||
return <></>;
|
|
||||||
}
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
RedirectLogistration.defaultProps = {
|
RedirectLogistration.defaultProps = {
|
||||||
|
authenticatedUser: {},
|
||||||
|
educationLevel: null,
|
||||||
finishAuthUrl: null,
|
finishAuthUrl: null,
|
||||||
success: false,
|
success: false,
|
||||||
redirectUrl: '',
|
redirectUrl: '',
|
||||||
redirectToWelcomePage: false,
|
redirectToProgressiveProfilingPage: false,
|
||||||
optionalFields: {},
|
optionalFields: {},
|
||||||
|
redirectToRecommendationsPage: false,
|
||||||
|
userId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
RedirectLogistration.propTypes = {
|
RedirectLogistration.propTypes = {
|
||||||
|
authenticatedUser: PropTypes.shape({}),
|
||||||
|
educationLevel: PropTypes.string,
|
||||||
finishAuthUrl: PropTypes.string,
|
finishAuthUrl: PropTypes.string,
|
||||||
success: PropTypes.bool,
|
success: PropTypes.bool,
|
||||||
redirectUrl: PropTypes.string,
|
redirectUrl: PropTypes.string,
|
||||||
redirectToWelcomePage: PropTypes.bool,
|
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||||
optionalFields: PropTypes.shape({}),
|
optionalFields: PropTypes.shape({}),
|
||||||
|
redirectToRecommendationsPage: PropTypes.bool,
|
||||||
|
userId: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RedirectLogistration;
|
export default RedirectLogistration;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
import { Icon } from '@edx/paragon';
|
||||||
|
import { Login } from '@edx/paragon/icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||||
|
|
||||||
function SocialAuthProviders(props) {
|
const SocialAuthProviders = (props) => {
|
||||||
const { intl, referrer, socialAuthProviders } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { referrer, socialAuthProviders } = props;
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -30,29 +32,30 @@ function SocialAuthProviders(props) {
|
|||||||
>
|
>
|
||||||
{provider.iconImage ? (
|
{provider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
<img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||||
<div className="font-container" aria-hidden="true">
|
{SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={['fab', provider.iconClass]} />)
|
||||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
: (
|
||||||
/>
|
<Icon className="h-75" src={Login} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{referrer === LOGIN_PAGE
|
{referrer === LOGIN_PAGE
|
||||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{socialAuth}</>;
|
return <>{socialAuth}</>;
|
||||||
}
|
};
|
||||||
|
|
||||||
SocialAuthProviders.defaultProps = {
|
SocialAuthProviders.defaultProps = {
|
||||||
referrer: LOGIN_PAGE,
|
referrer: LOGIN_PAGE,
|
||||||
@@ -60,7 +63,6 @@ SocialAuthProviders.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SocialAuthProviders.propTypes = {
|
SocialAuthProviders.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
referrer: PropTypes.string,
|
referrer: PropTypes.string,
|
||||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
@@ -69,7 +71,8 @@ SocialAuthProviders.propTypes = {
|
|||||||
iconImage: PropTypes.string,
|
iconImage: PropTypes.string,
|
||||||
loginUrl: PropTypes.string,
|
loginUrl: PropTypes.string,
|
||||||
registerUrl: PropTypes.string,
|
registerUrl: PropTypes.string,
|
||||||
|
skipRegistrationForm: PropTypes.bool,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SocialAuthProviders);
|
export default SocialAuthProviders;
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { TransitionReplace } from '@edx/paragon';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const onChildExit = (htmlNode) => {
|
|
||||||
// If the leaving child has focus, take control and redirect it
|
|
||||||
if (htmlNode.contains(document.activeElement)) {
|
|
||||||
// Get the newly entering sibling.
|
|
||||||
// It's the previousSibling, but not for any explicit reason. So checking for both.
|
|
||||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
|
||||||
|
|
||||||
// There's no replacement, do nothing.
|
|
||||||
if (!enteringChild) return; // eslint-disable-line curly
|
|
||||||
|
|
||||||
// Get all the focusable elements in the entering child and focus the first one
|
|
||||||
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
||||||
if (focusableElements.length) {
|
|
||||||
focusableElements[0].focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function SwitchContent({ expression, cases, className }) {
|
|
||||||
const getContent = (caseKey) => {
|
|
||||||
if (cases[caseKey]) {
|
|
||||||
if (typeof cases[caseKey] === 'string') {
|
|
||||||
return getContent(cases[caseKey]);
|
|
||||||
}
|
|
||||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
|
||||||
} else if (cases.default) { // eslint-disable-line no-else-return
|
|
||||||
if (typeof cases.default === 'string') {
|
|
||||||
return getContent(cases.default);
|
|
||||||
}
|
|
||||||
React.cloneElement(cases.default, { key: 'default' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TransitionReplace
|
|
||||||
className={className}
|
|
||||||
onChildExit={onChildExit}
|
|
||||||
>
|
|
||||||
{getContent(expression)}
|
|
||||||
</TransitionReplace>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchContent.propTypes = {
|
|
||||||
expression: PropTypes.string,
|
|
||||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
SwitchContent.defaultProps = {
|
|
||||||
expression: null,
|
|
||||||
className: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SwitchContent;
|
|
||||||
@@ -1,42 +1,52 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||||
|
|
||||||
const ThirdPartyAuthAlert = (props) => {
|
const ThirdPartyAuthAlert = (props) => {
|
||||||
const { currentProvider, intl, referrer } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { currentProvider, referrer } = props;
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
let message;
|
let message;
|
||||||
|
|
||||||
if (referrer === LOGIN_PAGE) {
|
if (referrer === LOGIN_PAGE) {
|
||||||
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||||
} else {
|
} else {
|
||||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentProvider) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
<>
|
||||||
|
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
|
||||||
{referrer === REGISTER_PAGE ? (
|
{referrer === REGISTER_PAGE ? (
|
||||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||||
) : null}
|
) : null}
|
||||||
<p>{ message }</p>
|
<p>{ message }</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
{referrer === REGISTER_PAGE ? (
|
||||||
|
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ThirdPartyAuthAlert.defaultProps = {
|
ThirdPartyAuthAlert.defaultProps = {
|
||||||
|
currentProvider: '',
|
||||||
referrer: LOGIN_PAGE,
|
referrer: LOGIN_PAGE,
|
||||||
};
|
};
|
||||||
|
|
||||||
ThirdPartyAuthAlert.propTypes = {
|
ThirdPartyAuthAlert.propTypes = {
|
||||||
currentProvider: PropTypes.string.isRequired,
|
currentProvider: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
referrer: PropTypes.string,
|
referrer: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ThirdPartyAuthAlert);
|
export default ThirdPartyAuthAlert;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { Route } from 'react-router-dom';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
import {
|
||||||
|
DEFAULT_REDIRECT_URL,
|
||||||
|
} from '../data/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This wrapper redirects the requester to our default redirect url if they are
|
* This wrapper redirects the requester to our default redirect url if they are
|
||||||
* already authenticated.
|
* already authenticated.
|
||||||
*/
|
*/
|
||||||
const UnAuthOnlyRoute = (props) => {
|
const UnAuthOnlyRoute = ({ children }) => {
|
||||||
const [authUser, setAuthUser] = useState({});
|
const [authUser, setAuthUser] = useState({});
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
@@ -27,10 +29,14 @@ const UnAuthOnlyRoute = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Route {...props} />;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <></>;
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
UnAuthOnlyRoute.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnAuthOnlyRoute;
|
export default UnAuthOnlyRoute;
|
||||||
|
|||||||
61
src/common-components/Zendesk.jsx
Normal file
61
src/common-components/Zendesk.jsx
Normal 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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AsyncActionType } from '../../data/utils';
|
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 = 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
|
// Third party auth context
|
||||||
export const getThirdPartyAuthContext = (urlParams) => ({
|
export const getThirdPartyAuthContext = (urlParams) => ({
|
||||||
@@ -20,3 +21,7 @@ export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalField
|
|||||||
export const getThirdPartyAuthContextFailure = () => ({
|
export const getThirdPartyAuthContextFailure = () => ({
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||||
|
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,33 +1,61 @@
|
|||||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
extendedProfile: [],
|
fieldDescriptions: {
|
||||||
fieldDescriptions: {},
|
fields: {},
|
||||||
optionalFields: {},
|
},
|
||||||
|
optionalFields: {
|
||||||
|
fields: {},
|
||||||
|
extended_profile: [],
|
||||||
|
},
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
|
thirdPartyAuthContext: {
|
||||||
|
autoSubmitRegForm: false,
|
||||||
|
currentProvider: null,
|
||||||
|
finishAuthUrl: null,
|
||||||
|
countryCode: null,
|
||||||
|
providers: [],
|
||||||
|
secondaryProviders: [],
|
||||||
|
pipelineUserDetails: null,
|
||||||
|
errorMessage: null,
|
||||||
|
welcomePageRedirectUrl: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = defaultState, action) => {
|
const reducer = (state = defaultState, action = {}) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||||
};
|
};
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
|
|
||||||
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||||
optionalFields: action.payload.optionalFields,
|
optionalFields: action.payload.optionalFields,
|
||||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
thirdPartyAuthApiStatus: COMPLETE_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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { logError } from '@edx/frontend-platform/logging';
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
|
||||||
import {
|
import {
|
||||||
getThirdPartyAuthContextBegin,
|
getThirdPartyAuthContextBegin,
|
||||||
getThirdPartyAuthContextFailure,
|
getThirdPartyAuthContextFailure,
|
||||||
@@ -11,18 +10,17 @@ import {
|
|||||||
import {
|
import {
|
||||||
getThirdPartyAuthContext,
|
getThirdPartyAuthContext,
|
||||||
} from './service';
|
} from './service';
|
||||||
|
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||||
|
|
||||||
export function* fetchThirdPartyAuthContext(action) {
|
export function* fetchThirdPartyAuthContext(action) {
|
||||||
try {
|
try {
|
||||||
yield put(getThirdPartyAuthContextBegin());
|
yield put(getThirdPartyAuthContextBegin());
|
||||||
const { fieldDescriptions, optionalFields, thirdPartyAuthContext } = yield call(
|
const {
|
||||||
getThirdPartyAuthContext, action.payload.urlParams,
|
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||||
);
|
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||||
|
|
||||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||||
yield put(getThirdPartyAuthContextSuccess(
|
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(getThirdPartyAuthContextFailure());
|
yield put(getThirdPartyAuthContextFailure());
|
||||||
logError(e);
|
logError(e);
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ export const fieldDescriptionSelector = createSelector(
|
|||||||
commonComponents => commonComponents.fieldDescriptions,
|
commonComponents => commonComponents.fieldDescriptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const extendedProfileSelector = createSelector(
|
|
||||||
commonComponentsSelector,
|
|
||||||
commonComponents => commonComponents.extendedProfile,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const optionalFieldsSelector = createSelector(
|
export const optionalFieldsSelector = createSelector(
|
||||||
commonComponentsSelector,
|
commonComponentsSelector,
|
||||||
commonComponents => commonComponents.optionalFields,
|
commonComponents => commonComponents.optionalFields,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const tpaProvidersSelector = createSelector(
|
||||||
|
commonComponentsSelector,
|
||||||
|
commonComponents => ({
|
||||||
|
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||||
|
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
@@ -18,12 +18,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
|||||||
throw (e);
|
throw (e);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
fieldDescriptions: data.registration_fields || {},
|
fieldDescriptions: data.registrationFields || {},
|
||||||
optionalFields: data.optional_fields || {},
|
optionalFields: data.optionalFields || {},
|
||||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
thirdPartyAuthContext: data.contextData || {},
|
||||||
// and deployed update it to use data.context_data
|
|
||||||
thirdPartyAuthContext: camelCaseObject(
|
|
||||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/common-components/data/tests/reducer.test.js
Normal file
82
src/common-components/data/tests/reducer.test.js
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as RedirectLogistration } from './RedirectLogistration';
|
export { default as RedirectLogistration } from './RedirectLogistration';
|
||||||
export { default as registerIcons } from './RegisterFaIcons';
|
export { default as registerIcons } from './RegisterFaIcons';
|
||||||
|
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||||
export { default as NotFoundPage } from './NotFoundPage';
|
export { default as NotFoundPage } from './NotFoundPage';
|
||||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||||
@@ -11,4 +12,4 @@ export { default as saga } from './data/sagas';
|
|||||||
export { storeName } from './data/selectors';
|
export { storeName } from './data/selectors';
|
||||||
export { default as FormGroup } from './FormGroup';
|
export { default as FormGroup } from './FormGroup';
|
||||||
export { default as PasswordField } from './PasswordField';
|
export { default as PasswordField } from './PasswordField';
|
||||||
export { default as Logistration } from './Logistration';
|
export { default as Zendesk } from './Zendesk';
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
// institution login strings
|
||||||
'institution.login.page.sub.heading': {
|
'institution.login.page.sub.heading': {
|
||||||
id: 'institution.login.page.sub.heading',
|
id: 'institution.login.page.sub.heading',
|
||||||
defaultMessage: 'Choose your institution from the list below',
|
defaultMessage: 'Choose your institution from the list below',
|
||||||
description: 'Heading of the institutions list',
|
description: 'Heading of the institutions list',
|
||||||
},
|
},
|
||||||
// Confirmation Alert Message
|
// logistration strings
|
||||||
'forgot.password.confirmation.title': {
|
|
||||||
id: 'forgot.password.confirmation.title',
|
|
||||||
defaultMessage: 'Check your email',
|
|
||||||
description: 'Forgot password confirmation message title',
|
|
||||||
},
|
|
||||||
'forgot.password.confirmation.support.link': {
|
|
||||||
id: 'forgot.password.confirmation.support.link',
|
|
||||||
defaultMessage: 'contact technical support',
|
|
||||||
description: 'Technical support link text',
|
|
||||||
},
|
|
||||||
'forgot.password.confirmation.info': {
|
|
||||||
id: 'forgot.password.confirmation.info',
|
|
||||||
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
|
|
||||||
+ 'email address, or check your spam folder.',
|
|
||||||
description: 'Part of message that appears after user requests password change',
|
|
||||||
},
|
|
||||||
// Logistration strinsg
|
|
||||||
'logistration.sign.in': {
|
'logistration.sign.in': {
|
||||||
id: 'logistration.sign.in',
|
id: 'logistration.sign.in',
|
||||||
defaultMessage: 'Sign in',
|
defaultMessage: 'Sign in',
|
||||||
@@ -34,27 +18,12 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Register',
|
defaultMessage: 'Register',
|
||||||
description: 'Text that appears on the tab to switch between login and register',
|
description: 'Text that appears on the tab to switch between login and register',
|
||||||
},
|
},
|
||||||
'internal.server.error.message': {
|
|
||||||
id: 'internal.server.error.message',
|
|
||||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
|
||||||
description: 'Error message that appears when server responds with 500 error code',
|
|
||||||
},
|
|
||||||
'server.ratelimit.error.message': {
|
|
||||||
id: 'server.ratelimit.error.message',
|
|
||||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
|
||||||
description: 'Error message that appears when server responds with 429 error code',
|
|
||||||
},
|
|
||||||
// enterprise sso strings
|
// enterprise sso strings
|
||||||
'enterprisetpa.title.heading': {
|
'enterprisetpa.title.heading': {
|
||||||
id: 'enterprisetpa.title.heading',
|
id: 'enterprisetpa.title.heading',
|
||||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||||
description: 'Header text used in enterprise third party authentication',
|
description: 'Header text used in enterprise third party authentication',
|
||||||
},
|
},
|
||||||
'enterprisetpa.sso.button.title': {
|
|
||||||
id: 'enterprisetpa.sso.button.title',
|
|
||||||
defaultMessage: 'Sign in using {providerName}',
|
|
||||||
description: 'Text for third party auth provider buttons',
|
|
||||||
},
|
|
||||||
'enterprisetpa.login.button.text': {
|
'enterprisetpa.login.button.text': {
|
||||||
id: 'enterprisetpa.login.button.text',
|
id: 'enterprisetpa.login.button.text',
|
||||||
defaultMessage: 'Show me other ways to sign in or register',
|
defaultMessage: 'Show me other ways to sign in or register',
|
||||||
@@ -128,6 +97,21 @@ const messages = defineMessages({
|
|||||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||||
+ 'but no associated platform account exists',
|
+ 'but no associated platform account exists',
|
||||||
},
|
},
|
||||||
|
'registration.using.tpa.form.heading': {
|
||||||
|
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:',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable import/no-import-module-exports */
|
||||||
|
/* eslint-disable react/function-component-definition */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
||||||
|
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
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>;
|
||||||
|
module.exports = RRD;
|
||||||
|
|
||||||
|
const TestApp = () => (
|
||||||
|
<Router>
|
||||||
|
<div>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={REGISTER_EMBEDDED_PAGE}
|
||||||
|
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('EmbeddedRegistrationRoute', () => {
|
||||||
|
const routerWrapper = () => (
|
||||||
|
<MemoryRouter initialEntries={[REGISTER_EMBEDDED_PAGE]}>
|
||||||
|
<TestApp />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render embedded register page if host query param is not available in the url', async () => {
|
||||||
|
let embeddedRegistrationPage = null;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
embeddedRegistrationPage = await mount(routerWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
||||||
|
delete window.location;
|
||||||
|
window.location = {
|
||||||
|
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE),
|
||||||
|
search: '?host=http://localhost/host-websit',
|
||||||
|
};
|
||||||
|
|
||||||
|
let embeddedRegistrationPage = null;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
embeddedRegistrationPage = await mount(routerWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
|
||||||
|
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
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 FormGroup from '../FormGroup';
|
||||||
import PasswordField from '../PasswordField';
|
import PasswordField from '../PasswordField';
|
||||||
|
|
||||||
@@ -26,10 +30,27 @@ describe('FormGroup', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PasswordField', () => {
|
describe('PasswordField', () => {
|
||||||
|
const mockStore = configureStore();
|
||||||
const IntlPasswordField = injectIntl(PasswordField);
|
const IntlPasswordField = injectIntl(PasswordField);
|
||||||
let props = {};
|
let props = {};
|
||||||
|
let store = {};
|
||||||
|
|
||||||
|
const reduxWrapper = children => (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<MemoryRouter>
|
||||||
|
<Provider store={store}>{children}</Provider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
register: {
|
||||||
|
validationApiRateLimited: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
store = mockStore(initialState);
|
||||||
props = {
|
props = {
|
||||||
floatingLabel: 'Password',
|
floatingLabel: 'Password',
|
||||||
name: 'password',
|
name: 'password',
|
||||||
@@ -39,7 +60,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show/hide password on icon click', () => {
|
it('should show/hide password on icon click', () => {
|
||||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
passwordField.find('button[aria-label="Show password"]').simulate('click');
|
passwordField.find('button[aria-label="Show password"]').simulate('click');
|
||||||
expect(passwordField.find('input').prop('type')).toEqual('text');
|
expect(passwordField.find('input').prop('type')).toEqual('text');
|
||||||
@@ -49,7 +70,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show password requirement tooltip on focus', async () => {
|
it('should show password requirement tooltip on focus', async () => {
|
||||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
passwordField.find('input').simulate('focus');
|
passwordField.find('input').simulate('focus');
|
||||||
@@ -67,7 +88,7 @@ describe('PasswordField', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
passwordField.find('input').simulate('focus');
|
passwordField.find('input').simulate('focus');
|
||||||
jest.runAllTimers();
|
jest.runAllTimers();
|
||||||
@@ -80,7 +101,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update password requirement checks', async () => {
|
it('should update password requirement checks', async () => {
|
||||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
passwordField.find('input').simulate('focus');
|
passwordField.find('input').simulate('focus');
|
||||||
@@ -92,4 +113,142 @@ describe('PasswordField', () => {
|
|||||||
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||||
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not run validations when blur is fired on password icon click', () => {
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: 'invalid',
|
||||||
|
},
|
||||||
|
relatedTarget: {
|
||||||
|
name: 'passwordIcon',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call props handle blur if available', () => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
handleBlur: jest.fn(),
|
||||||
|
};
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('input#password').simulate('blur', {
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.handleBlur).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run validations on blur event when rendered from register page', () => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
handleErrorChange: jest.fn(),
|
||||||
|
};
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('input#password').simulate('blur', {
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||||
|
'password',
|
||||||
|
'Password criteria has not been met',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clear error when focus is fired on password icon click when rendered from register page', () => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
handleErrorChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||||
|
target: {
|
||||||
|
name: 'passwordIcon',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear error when focus is fired on password icon click when rendered from register page', () => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
handleErrorChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: 'invalid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||||
|
'password',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
handleErrorChange: jest.fn(),
|
||||||
|
};
|
||||||
|
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: '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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||||
|
|
||||||
|
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||||
|
target: {
|
||||||
|
name: 'passwordIcon',
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(props.handleBlur).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.handleBlur).toHaveBeenCalledWith({
|
||||||
|
target: {
|
||||||
|
name: 'password',
|
||||||
|
value: 'testPassword',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
/* eslint-disable import/no-import-module-exports */
|
||||||
|
/* eslint-disable react/function-component-definition */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import * as auth from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { UnAuthOnlyRoute } from '..';
|
import { UnAuthOnlyRoute } from '..';
|
||||||
import { LOGIN_PAGE } from '../../data/constants';
|
import { REGISTER_PAGE } from '../../data/constants';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/auth');
|
import {
|
||||||
|
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(),
|
||||||
|
fetchAuthenticatedUser: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const RRD = require('react-router-dom');
|
const RRD = require('react-router-dom');
|
||||||
// Just render plain div with its children
|
// Just render plain div with its children
|
||||||
@@ -18,16 +27,16 @@ module.exports = RRD;
|
|||||||
const TestApp = () => (
|
const TestApp = () => (
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Routes>
|
||||||
<UnAuthOnlyRoute path={LOGIN_PAGE} render={() => (<span>Login Page</span>)} />
|
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><span>Register Page</span></UnAuthOnlyRoute>} />
|
||||||
</Switch>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('UnAuthOnlyRoute', () => {
|
describe('UnAuthOnlyRoute', () => {
|
||||||
const routerWrapper = () => (
|
const routerWrapper = () => (
|
||||||
<MemoryRouter initialEntries={[LOGIN_PAGE]}>
|
<MemoryRouter initialEntries={[REGISTER_PAGE]}>
|
||||||
<TestApp />
|
<TestApp />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -36,25 +45,30 @@ describe('UnAuthOnlyRoute', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have called with forceRefresh true', () => {
|
it('should have called with forceRefresh true', async () => {
|
||||||
const user = {
|
const user = {
|
||||||
username: 'gonzo',
|
username: 'gonzo',
|
||||||
other: 'data',
|
other: 'data',
|
||||||
};
|
};
|
||||||
auth.getAuthenticatedUser = jest.fn(() => user);
|
|
||||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
|
||||||
|
|
||||||
mount(routerWrapper());
|
getAuthenticatedUser.mockReturnValue(user);
|
||||||
|
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
|
||||||
|
|
||||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
await act(async () => {
|
||||||
|
await mount(routerWrapper());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have called with forceRefresh false', () => {
|
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||||
auth.getAuthenticatedUser = jest.fn(() => null);
|
});
|
||||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
|
||||||
|
|
||||||
mount(routerWrapper());
|
it('should have called with forceRefresh false', async () => {
|
||||||
|
getAuthenticatedUser.mockReturnValue(null);
|
||||||
|
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
|
||||||
|
|
||||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
await act(async () => {
|
||||||
|
await mount(routerWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
17
src/common-components/tests/Zendesk.test.jsx
Normal file
17
src/common-components/tests/Zendesk.test.jsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,25 +10,27 @@ exports[`SocialAuthProviders should match social auth provider with default icon
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="font-container"
|
className="btn-tpa__font-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="pgn__icon h-75"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden={true}
|
||||||
className="svg-inline--fa fa-right-to-bracket "
|
fill="none"
|
||||||
data-icon="right-to-bracket"
|
focusable={false}
|
||||||
data-prefix="fas"
|
height={24}
|
||||||
focusable="false"
|
|
||||||
role="img"
|
role="img"
|
||||||
style={Object {}}
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 512 512"
|
width={24}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M352 96h64c17.7 0 32 14.3 32 32V384c0 17.7-14.3 32-32 32H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h64c53 0 96-43 96-96V128c0-53-43-96-96-96H352c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-7.5 177.4c4.8-4.5 7.5-10.8 7.5-17.4s-2.7-12.9-7.5-17.4l-144-136c-7-6.6-17.2-8.4-26-4.6s-14.5 12.5-14.5 22v72H32c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32H160v72c0 9.6 5.7 18.2 14.5 22s19 2 26-4.6l144-136z"
|
d="M11 7 9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h10V3H12v2h8v14z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
style={Object {}}
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -55,7 +57,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="font-container"
|
className="btn-tpa__font-container"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -104,7 +106,7 @@ Array [
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="icon Apple"
|
alt="icon Apple"
|
||||||
className="icon-image"
|
className="btn-tpa__image-icon"
|
||||||
src="https://edx.devstack.lms/logo.png"
|
src="https://edx.devstack.lms/logo.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +135,7 @@ Array [
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="icon Facebook"
|
alt="icon Facebook"
|
||||||
className="icon-image"
|
className="btn-tpa__image-icon"
|
||||||
src="https://edx.devstack.lms/facebook-logo.png"
|
src="https://edx.devstack.lms/facebook-logo.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="fade alert-content alert-warning mt-n2 alert show"
|
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||||
id="tpa-alert"
|
id="tpa-alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
@@ -21,8 +21,9 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||||
|
Array [
|
||||||
<div
|
<div
|
||||||
className="fade alert-content alert-success mt-n2 alert show"
|
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||||
id="tpa-alert"
|
id="tpa-alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
@@ -42,5 +43,11 @@ exports[`ThirdPartyAuthAlert should match register page third party auth alert m
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
<h4
|
||||||
|
className="mt-4 mb-4"
|
||||||
|
>
|
||||||
|
Finish creating your account
|
||||||
|
</h4>,
|
||||||
|
]
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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={
|
||||||
|
Object {
|
||||||
|
"answerBot": Object {
|
||||||
|
"avatar": Object {
|
||||||
|
"name": Object {
|
||||||
|
"*": "edX Support",
|
||||||
|
},
|
||||||
|
"url": undefined,
|
||||||
|
},
|
||||||
|
"contactOnlyAfterQuery": true,
|
||||||
|
"suppress": false,
|
||||||
|
"title": Object {
|
||||||
|
"*": "edX Support",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"chat": Object {
|
||||||
|
"departments": Object {
|
||||||
|
"enabled": Array [
|
||||||
|
"account settings",
|
||||||
|
"billing and payments",
|
||||||
|
"certificates",
|
||||||
|
"deadlines",
|
||||||
|
"errors and technical issues",
|
||||||
|
"other",
|
||||||
|
"proctoring",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"suppress": false,
|
||||||
|
},
|
||||||
|
"contactForm": Object {
|
||||||
|
"attachments": true,
|
||||||
|
"selectTicketForm": Object {
|
||||||
|
"*": "Please choose your request type:",
|
||||||
|
},
|
||||||
|
"ticketForms": Array [
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"id": "description",
|
||||||
|
"prefill": Object {
|
||||||
|
"*": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"id": 360003368814,
|
||||||
|
"subject": false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"contactOptions": Object {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
"helpCenter": Object {
|
||||||
|
"originalArticleButton": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
37
src/config/index.js
Normal file
37
src/config/index.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const configuration = {
|
||||||
|
// Cookies related configs
|
||||||
|
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
||||||
|
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
|
||||||
|
// Features
|
||||||
|
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||||
|
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,
|
||||||
|
// Links
|
||||||
|
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
|
||||||
|
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
|
||||||
|
POST_REGISTRATION_REDIRECT_URL: process.env.POST_REGISTRATION_REDIRECT_URL || '',
|
||||||
|
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
||||||
|
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||||
|
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
||||||
|
TOS_LINK: process.env.TOS_LINK || null,
|
||||||
|
// Base container images
|
||||||
|
BANNER_IMAGE_LARGE: process.env.BANNER_IMAGE_LARGE || '',
|
||||||
|
BANNER_IMAGE_MEDIUM: process.env.BANNER_IMAGE_MEDIUM || '',
|
||||||
|
BANNER_IMAGE_SMALL: process.env.BANNER_IMAGE_SMALL || '',
|
||||||
|
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
|
||||||
|
// Recommendation constants
|
||||||
|
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
|
||||||
|
// Miscellaneous
|
||||||
|
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
||||||
|
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||||
|
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
||||||
|
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
|
||||||
|
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default configuration;
|
||||||
20
src/data/algolia.js
Normal file
20
src/data/algolia.js
Normal 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,
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// URL Paths
|
// URL Paths
|
||||||
export const LOGIN_PAGE = '/login';
|
export const LOGIN_PAGE = '/login';
|
||||||
export const REGISTER_PAGE = '/register';
|
export const REGISTER_PAGE = '/register';
|
||||||
|
export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
||||||
export const RESET_PAGE = '/reset';
|
export const RESET_PAGE = '/reset';
|
||||||
export const WELCOME_PAGE = '/welcome';
|
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||||
|
export const RECOMMENDATIONS = '/recommendations';
|
||||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||||
export const PAGE_NOT_FOUND = '/notfound';
|
export const PAGE_NOT_FOUND = '/notfound';
|
||||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||||
@@ -22,16 +24,16 @@ export const PENDING_STATE = 'pending';
|
|||||||
export const COMPLETE_STATE = 'complete';
|
export const COMPLETE_STATE = 'complete';
|
||||||
export const FAILURE_STATE = 'failure';
|
export const FAILURE_STATE = 'failure';
|
||||||
export const FORBIDDEN_STATE = 'forbidden';
|
export const FORBIDDEN_STATE = 'forbidden';
|
||||||
|
export const EMBEDDED = 'embedded';
|
||||||
|
|
||||||
// Regex
|
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||||
|
export const NUMBER_REGEX = /\d/;
|
||||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
|
||||||
export const NUMBER_REGEX = /\d/;
|
|
||||||
export const VALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
|
||||||
|
|
||||||
// Query string parameters that can be passed to LMS to manage
|
// Query string parameters that can be passed to LMS to manage
|
||||||
// things like auto-enrollment upon login and registration.
|
// things like auto-enrollment upon login and registration.
|
||||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free'];
|
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';
|
||||||
|
|||||||
3
src/data/oneTrust.js
Normal file
3
src/data/oneTrust.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const isOneTrustFunctionalCookieEnabled = () => !!window?.OnetrustActiveGroups?.includes('C0003');
|
||||||
|
|
||||||
|
export default isOneTrustFunctionalCookieEnabled;
|
||||||
17
src/data/optimizely.js
Normal file
17
src/data/optimizely.js
Normal 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();
|
||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
reducer as loginReducer,
|
reducer as loginReducer,
|
||||||
storeName as loginStoreName,
|
storeName as loginStoreName,
|
||||||
} from '../login';
|
} from '../login';
|
||||||
|
import {
|
||||||
|
reducer as authnProgressiveProfilingReducers,
|
||||||
|
storeName as authnProgressiveProfilingStoreName,
|
||||||
|
} from '../progressive-profiling';
|
||||||
import {
|
import {
|
||||||
reducer as registerReducer,
|
reducer as registerReducer,
|
||||||
storeName as registerStoreName,
|
storeName as registerStoreName,
|
||||||
@@ -20,10 +24,6 @@ import {
|
|||||||
reducer as resetPasswordReducer,
|
reducer as resetPasswordReducer,
|
||||||
storeName as resetPasswordStoreName,
|
storeName as resetPasswordStoreName,
|
||||||
} from '../reset-password';
|
} from '../reset-password';
|
||||||
import {
|
|
||||||
reducer as welcomePageReducers,
|
|
||||||
storeName as welcomePageStoreName,
|
|
||||||
} from '../welcome';
|
|
||||||
|
|
||||||
const createRootReducer = () => combineReducers({
|
const createRootReducer = () => combineReducers({
|
||||||
[loginStoreName]: loginReducer,
|
[loginStoreName]: loginReducer,
|
||||||
@@ -31,6 +31,6 @@ const createRootReducer = () => combineReducers({
|
|||||||
[commonComponentsStoreName]: commonComponentsReducer,
|
[commonComponentsStoreName]: commonComponentsReducer,
|
||||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||||
[resetPasswordStoreName]: resetPasswordReducer,
|
[resetPasswordStoreName]: resetPasswordReducer,
|
||||||
[welcomePageStoreName]: welcomePageReducers,
|
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||||
});
|
});
|
||||||
export default createRootReducer;
|
export default createRootReducer;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { all } from 'redux-saga/effects';
|
|||||||
import { saga as commonComponentsSaga } from '../common-components';
|
import { saga as commonComponentsSaga } from '../common-components';
|
||||||
import { saga as forgotPasswordSaga } from '../forgot-password';
|
import { saga as forgotPasswordSaga } from '../forgot-password';
|
||||||
import { saga as loginSaga } from '../login';
|
import { saga as loginSaga } from '../login';
|
||||||
|
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
|
||||||
import { saga as registrationSaga } from '../register';
|
import { saga as registrationSaga } from '../register';
|
||||||
import { saga as resetPasswordSaga } from '../reset-password';
|
import { saga as resetPasswordSaga } from '../reset-password';
|
||||||
import { saga as welcomePageSaga } from '../welcome';
|
|
||||||
|
|
||||||
export default function* rootSaga() {
|
export default function* rootSaga() {
|
||||||
yield all([
|
yield all([
|
||||||
@@ -14,6 +14,6 @@ export default function* rootSaga() {
|
|||||||
commonComponentsSaga(),
|
commonComponentsSaga(),
|
||||||
forgotPasswordSaga(),
|
forgotPasswordSaga(),
|
||||||
resetPasswordSaga(),
|
resetPasswordSaga(),
|
||||||
welcomePageSaga(),
|
authnProgressiveProfilingSaga(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/data/tests/algolia.test.js
Normal file
16
src/data/tests/algolia.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LOGIN_PAGE } from '../constants';
|
import { LOGIN_PAGE } from '../constants';
|
||||||
import { updatePathWithQueryParams } from './dataUtils';
|
import { updatePathWithQueryParams } from '../utils/dataUtils';
|
||||||
|
|
||||||
describe('updatePathWithQueryParams', () => {
|
describe('updatePathWithQueryParams', () => {
|
||||||
it('should append query params into the path', () => {
|
it('should append query params into the path', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import AsyncActionType from './reduxUtils';
|
import AsyncActionType from '../utils/reduxUtils';
|
||||||
|
|
||||||
describe('AsyncActionType', () => {
|
describe('AsyncActionType', () => {
|
||||||
it('should return well formatted action strings', () => {
|
it('should return well formatted action strings', () => {
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import Cookies from 'universal-cookie';
|
import Cookies from 'universal-cookie';
|
||||||
|
|
||||||
export function setCookie(cookieName, cookieValue, cookieExpiry) {
|
export default function setCookie(cookieName, cookieValue, cookieExpiry) {
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
|
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||||
if (cookieExpiry) {
|
if (cookieExpiry) {
|
||||||
options.expires = cookieExpiry;
|
options.expires = cookieExpiry;
|
||||||
}
|
}
|
||||||
cookies.set(cookieName, cookieValue, options);
|
cookies.set(cookieName, cookieValue, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function setSurveyCookie(surveyType) {
|
|
||||||
const cookieName = getConfig().USER_SURVEY_COOKIE_NAME;
|
|
||||||
if (cookieName) {
|
|
||||||
const signupTimestamp = (new Date()).getTime();
|
|
||||||
// set expiry to exactly 24 hours from now
|
|
||||||
const cookieExpiry = new Date(signupTimestamp + 1 * 864e5);
|
|
||||||
setCookie(cookieName, surveyType, cookieExpiry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ export const updatePathWithQueryParams = (path) => {
|
|||||||
return `${path}${queryParams}`;
|
return `${path}${queryParams}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllPossibleQueryParam = () => {
|
export const getAllPossibleQueryParams = (locationURl = null) => {
|
||||||
const urlParams = QueryString.parse(window.location.search);
|
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
||||||
const params = {};
|
const params = {};
|
||||||
Object.entries(urlParams).forEach(([key, value]) => {
|
Object.entries(urlParams).forEach(([key, value]) => {
|
||||||
if (AUTH_PARAMS.indexOf(key) > -1) {
|
if (AUTH_PARAMS.indexOf(key) > -1) {
|
||||||
@@ -76,3 +76,8 @@ export const windowScrollTo = (options) => {
|
|||||||
|
|
||||||
return window.scrollTo(options.top, options.left);
|
return window.scrollTo(options.top, options.left);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isHostAvailableInQueryParams = () => {
|
||||||
|
const queryParams = getAllPossibleQueryParams();
|
||||||
|
return 'host' in queryParams;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export {
|
export {
|
||||||
getTpaProvider,
|
getTpaProvider,
|
||||||
getTpaHint,
|
getTpaHint,
|
||||||
updatePathWithQueryParams,
|
getAllPossibleQueryParams,
|
||||||
getAllPossibleQueryParam,
|
|
||||||
getActivationStatus,
|
getActivationStatus,
|
||||||
|
isHostAvailableInQueryParams,
|
||||||
|
updatePathWithQueryParams,
|
||||||
windowScrollTo,
|
windowScrollTo,
|
||||||
} from './dataUtils';
|
} from './dataUtils';
|
||||||
export { default as AsyncActionType } from './reduxUtils';
|
export { default as AsyncActionType } from './reduxUtils';
|
||||||
export { default as setSurveyCookie, setCookie } from './cookies';
|
export { default as setCookie } from './cookies';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
|||||||
const FormFieldRenderer = (props) => {
|
const FormFieldRenderer = (props) => {
|
||||||
let formField = null;
|
let formField = null;
|
||||||
const {
|
const {
|
||||||
errorMessage, fieldData, onChangeHandler, isRequired, value,
|
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const handleFocus = (e) => {
|
const handleFocus = (e) => {
|
||||||
@@ -26,6 +26,7 @@ const FormFieldRenderer = (props) => {
|
|||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
as="select"
|
as="select"
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -54,6 +55,7 @@ const FormFieldRenderer = (props) => {
|
|||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
as="textarea"
|
as="textarea"
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -76,6 +78,7 @@ const FormFieldRenderer = (props) => {
|
|||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||||
@@ -97,6 +100,7 @@ const FormFieldRenderer = (props) => {
|
|||||||
formField = (
|
formField = (
|
||||||
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
|
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
|
className={className}
|
||||||
id={fieldData.name}
|
id={fieldData.name}
|
||||||
checked={!!value}
|
checked={!!value}
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
@@ -124,6 +128,7 @@ const FormFieldRenderer = (props) => {
|
|||||||
return formField;
|
return formField;
|
||||||
};
|
};
|
||||||
FormFieldRenderer.defaultProps = {
|
FormFieldRenderer.defaultProps = {
|
||||||
|
className: '',
|
||||||
value: '',
|
value: '',
|
||||||
handleBlur: null,
|
handleBlur: null,
|
||||||
handleFocus: null,
|
handleFocus: null,
|
||||||
@@ -132,17 +137,22 @@ FormFieldRenderer.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
FormFieldRenderer.propTypes = {
|
FormFieldRenderer.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
fieldData: PropTypes.shape({
|
fieldData: PropTypes.shape({
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
onChangeHandler: PropTypes.func.isRequired,
|
onChangeHandler: PropTypes.func.isRequired,
|
||||||
handleBlur: PropTypes.func,
|
handleBlur: PropTypes.func,
|
||||||
handleFocus: PropTypes.func,
|
handleFocus: PropTypes.func,
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
isRequired: PropTypes.bool,
|
isRequired: PropTypes.bool,
|
||||||
value: PropTypes.string,
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.bool,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormFieldRenderer;
|
export default FormFieldRenderer;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './FieldRenderer';
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export { default as FormFieldRenderer } from './FieldRenderer';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import FieldRenderer from '../FieldRenderer';
|
import FieldRenderer from '../FieldRenderer';
|
||||||
@@ -24,7 +25,7 @@ describe('FieldRendererTests', () => {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
label: 'Year of Birth',
|
label: 'Year of Birth',
|
||||||
name: 'yob-field',
|
name: 'yob-field',
|
||||||
options: [['1997', 1997], ['1998', 1998]],
|
options: [['1997', '1997'], ['1998', '1998']],
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||||
@@ -82,7 +83,7 @@ describe('FieldRendererTests', () => {
|
|||||||
it('should render checkbox field', () => {
|
it('should render checkbox field', () => {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: 'I agree that edX may send me marketing messages.',
|
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
|
||||||
name: 'marketing-emails-opt-in-field',
|
name: 'marketing-emails-opt-in-field',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ describe('FieldRendererTests', () => {
|
|||||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||||
|
|
||||||
expect(field.prop('type')).toEqual('checkbox');
|
expect(field.prop('type')).toEqual('checkbox');
|
||||||
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
|
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
|
||||||
expect(value).toEqual(true);
|
expect(value).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
import {
|
import {
|
||||||
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
const ForgotPasswordAlert = (props) => {
|
const ForgotPasswordAlert = (props) => {
|
||||||
const { email, emailError, intl } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { email, emailError } = props;
|
||||||
let message = '';
|
let message = '';
|
||||||
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
|
let heading = formatMessage(messages['forgot.password.error.alert.title']);
|
||||||
let { status } = props;
|
let { status } = props;
|
||||||
|
|
||||||
if (emailError) {
|
if (emailError) {
|
||||||
@@ -24,7 +25,7 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case COMPLETE_STATE:
|
case COMPLETE_STATE:
|
||||||
heading = intl.formatMessage(messages['confirmation.message.title']);
|
heading = formatMessage(messages['confirmation.message.title']);
|
||||||
message = (
|
message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="forgot.password.confirmation.message"
|
id="forgot.password.confirmation.message"
|
||||||
@@ -36,7 +37,7 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
email: <span className="data-hj-suppress">{email}</span>,
|
email: <span className="data-hj-suppress">{email}</span>,
|
||||||
supportLink: (
|
supportLink: (
|
||||||
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
||||||
{intl.formatMessage(messages['confirmation.support.link'])}
|
{formatMessage(messages['confirmation.support.link'])}
|
||||||
</Alert.Link>
|
</Alert.Link>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -44,26 +45,26 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case INTERNAL_SERVER_ERROR:
|
case INTERNAL_SERVER_ERROR:
|
||||||
message = intl.formatMessage(messages['internal.server.error']);
|
message = formatMessage(messages['internal.server.error']);
|
||||||
break;
|
break;
|
||||||
case FORBIDDEN_STATE:
|
case FORBIDDEN_STATE:
|
||||||
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
|
heading = formatMessage(messages['forgot.password.error.message.title']);
|
||||||
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
|
message = formatMessage(messages['forgot.password.request.in.progress.message']);
|
||||||
break;
|
break;
|
||||||
case FORM_SUBMISSION_ERROR:
|
case FORM_SUBMISSION_ERROR:
|
||||||
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
|
message = formatMessage(messages['extend.field.errors'], { emailError });
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.INVALID_TOKEN:
|
case PASSWORD_RESET.INVALID_TOKEN:
|
||||||
heading = intl.formatMessage(messages['invalid.token.heading']);
|
heading = formatMessage(messages['invalid.token.heading']);
|
||||||
message = intl.formatMessage(messages['invalid.token.error.message']);
|
message = formatMessage(messages['invalid.token.error.message']);
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||||
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
|
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
|
||||||
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
|
message = formatMessage(messages['token.validation.rate.limit.error']);
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||||
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
|
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
|
||||||
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
|
message = formatMessage(messages['token.validation.internal.sever.error']);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -93,8 +94,7 @@ ForgotPasswordAlert.defaultProps = {
|
|||||||
ForgotPasswordAlert.propTypes = {
|
ForgotPasswordAlert.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
emailError: PropTypes.string,
|
emailError: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ForgotPasswordAlert);
|
export default ForgotPasswordAlert;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Hyperlink,
|
Hyperlink,
|
||||||
@@ -15,29 +15,30 @@ import {
|
|||||||
import { ChevronLeft } from '@edx/paragon/icons';
|
import { ChevronLeft } from '@edx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import BaseComponent from '../base-component';
|
|
||||||
import { FormGroup } from '../common-components';
|
|
||||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
|
||||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
|
||||||
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
|
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
|
||||||
import { forgotPasswordResultSelector } from './data/selectors';
|
import { forgotPasswordResultSelector } from './data/selectors';
|
||||||
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import BaseContainer from '../base-container';
|
||||||
|
import { FormGroup } from '../common-components';
|
||||||
|
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||||
|
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||||
|
|
||||||
const ForgotPasswordPage = (props) => {
|
const ForgotPasswordPage = (props) => {
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||||
const {
|
const {
|
||||||
intl, status, submitState, emailValidationError,
|
status, submitState, emailValidationError,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const [email, setEmail] = useState(props.email);
|
const [email, setEmail] = useState(props.email);
|
||||||
const [bannerEmail, setBannerEmail] = useState('');
|
const [bannerEmail, setBannerEmail] = useState('');
|
||||||
const [formErrors, setFormErrors] = useState('');
|
const [formErrors, setFormErrors] = useState('');
|
||||||
const [validationError, setValidationError] = useState(emailValidationError);
|
const [validationError, setValidationError] = useState(emailValidationError);
|
||||||
const [key, setKey] = useState('');
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendPageEvent('login_and_registration', 'reset');
|
sendPageEvent('login_and_registration', 'reset');
|
||||||
@@ -58,9 +59,9 @@ const ForgotPasswordPage = (props) => {
|
|||||||
let error = '';
|
let error = '';
|
||||||
|
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
error = intl.formatMessage(messages['forgot.password.empty.email.field.error']);
|
error = formatMessage(messages['forgot.password.empty.email.field.error']);
|
||||||
} else if (!emailRegex.test(value)) {
|
} else if (!emailRegex.test(value)) {
|
||||||
error = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
|
error = formatMessage(messages['forgot.password.page.invalid.email.message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
@@ -89,35 +90,32 @@ const ForgotPasswordPage = (props) => {
|
|||||||
const tabTitle = (
|
const tabTitle = (
|
||||||
<div className="d-inline-flex flex-wrap align-items-center">
|
<div className="d-inline-flex flex-wrap align-items-center">
|
||||||
<Icon src={ChevronLeft} />
|
<Icon src={ChevronLeft} />
|
||||||
<span className="ml-2">{intl.formatMessage(messages['sign.in.text'])}</span>
|
<span className="ml-2">{formatMessage(messages['sign.in.text'])}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseComponent>
|
<BaseContainer>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{intl.formatMessage(messages['forgot.password.page.title'],
|
<title>{formatMessage(messages['forgot.password.page.title'],
|
||||||
{ siteName: getConfig().SITE_NAME })}
|
{ siteName: getConfig().SITE_NAME })}
|
||||||
</title>
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div>
|
<div>
|
||||||
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
|
<Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
|
||||||
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{ key && (
|
|
||||||
<Redirect to={updatePathWithQueryParams(key)} />
|
|
||||||
)}
|
|
||||||
<div id="main-content" className="main-content">
|
<div id="main-content" className="main-content">
|
||||||
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
|
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
|
||||||
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
|
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
|
||||||
<h2 className="h4">
|
<h2 className="h4">
|
||||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
{formatMessage(messages['forgot.password.page.heading'])}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
{formatMessage(messages['forgot.password.page.instructions'])}
|
||||||
</p>
|
</p>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
floatingLabel={formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||||
name="email"
|
name="email"
|
||||||
value={email}
|
value={email}
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
@@ -125,17 +123,17 @@ const ForgotPasswordPage = (props) => {
|
|||||||
handleChange={(e) => setEmail(e.target.value)}
|
handleChange={(e) => setEmail(e.target.value)}
|
||||||
handleBlur={handleBlur}
|
handleBlur={handleBlur}
|
||||||
handleFocus={handleFocus}
|
handleFocus={handleFocus}
|
||||||
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
helpText={[formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||||
/>
|
/>
|
||||||
<StatefulButton
|
<StatefulButton
|
||||||
id="submit-forget-password"
|
id="submit-forget-password"
|
||||||
name="submit-forget-password"
|
name="submit-forget-password"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="brand"
|
variant="brand"
|
||||||
className="forgot-password-button-width"
|
className="forgot-password--button"
|
||||||
state={submitState}
|
state={submitState}
|
||||||
labels={{
|
labels={{
|
||||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
default: formatMessage(messages['forgot.password.page.submit.button']),
|
||||||
pending: '',
|
pending: '',
|
||||||
}}
|
}}
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
@@ -150,11 +148,11 @@ const ForgotPasswordPage = (props) => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
showLaunchIcon={false}
|
showLaunchIcon={false}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
{formatMessage(messages['need.help.sign.in.text'])}
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
)}
|
)}
|
||||||
<p className="mt-5.5 small text-gray-700">
|
<p className="mt-5.5 small text-gray-700">
|
||||||
{intl.formatMessage(messages['additional.help.text'], { platformName })}
|
{formatMessage(messages['additional.help.text'], { platformName })}
|
||||||
<span>
|
<span>
|
||||||
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||||
</span>
|
</span>
|
||||||
@@ -162,7 +160,7 @@ const ForgotPasswordPage = (props) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseComponent>
|
</BaseContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,7 +168,6 @@ ForgotPasswordPage.propTypes = {
|
|||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
emailValidationError: PropTypes.string,
|
emailValidationError: PropTypes.string,
|
||||||
forgotPassword: PropTypes.func.isRequired,
|
forgotPassword: PropTypes.func.isRequired,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
setForgotPasswordFormData: PropTypes.func.isRequired,
|
setForgotPasswordFormData: PropTypes.func.isRequired,
|
||||||
status: PropTypes.string,
|
status: PropTypes.string,
|
||||||
submitState: PropTypes.string,
|
submitState: PropTypes.string,
|
||||||
@@ -189,4 +186,4 @@ export default connect(
|
|||||||
forgotPassword,
|
forgotPassword,
|
||||||
setForgotPasswordFormData,
|
setForgotPasswordFormData,
|
||||||
},
|
},
|
||||||
)(injectIntl(ForgotPasswordPage));
|
)(ForgotPasswordPage);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
|
||||||
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
||||||
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
||||||
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
|
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
status: '',
|
status: '',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default } from './ForgotPasswordPage';
|
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
|
||||||
export { default as reducer } from './data/reducers';
|
export { default as reducer } from './data/reducers';
|
||||||
export { FORGOT_PASSWORD } from './data/actions';
|
export { FORGOT_PASSWORD } from './data/actions';
|
||||||
export { default as saga } from './data/sagas';
|
export { default as saga } from './data/sagas';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user